Files
AgentHub/server/lib/codex-nudge.js
T
nntrivi2001 17d56d0e01 refactor!: rebrand CCAM to AgentHub
Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
2026-09-07 12:09:15 +07:00

104 lines
3.8 KiB
JavaScript

/**
* @file Safety-net reminder for codex lanes sitting at a stage nobody has
* backed with evidence. `codex-ingest.js` auto-advances a codex lane's stage
* from its rollout journal — that keeps the pipeline map moving, but an
* inferred stage carries no evidence, so its node renders
* `passed-no-evidence` (amber) rather than `done`. Only the session's own
* `agenthub stage <node> --evidence "..."` call can supply that, and a codex
* session has no hook or wrapper that does it for it.
*
* This module never writes `stage`, `stages` or `detected_stage` itself. It
* only types a plain-text reminder into the lane's own live tmux pane
* (`tmux.sendText`), the same way a human watching the dashboard would nudge
* the agent — so the evidence that results is still a REAL declaration the
* codex session made itself.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const NUDGE_DELAY_MS = 120_000; // give the session 2 min to declare on its own first
// Per-lane memory of the last stage already nudged for, so the 60s sweep
// doesn't retype the same reminder every pass. Cleared implicitly: once the
// session declares with evidence, or the lane moves to another node, the value
// here no longer matches and a fresh nudge is due.
// ponytail: in-memory, resets on server restart — worst case one duplicate
// nudge after a restart, not worth a DB column for.
const nudgedFor = new Map();
/**
* One sweep pass: nudge every codex lane whose CURRENT stage has been held for
* at least NUDGE_DELAY_MS with no evidence recorded, and hasn't already been
* nudged for that exact stage.
* @returns {number} How many lanes were nudged.
*/
function sweepCodexNudges() {
const lanes = require("./lanes");
const tmux = require("./tmux");
const { SHELL_COMMANDS, runIdForLane } = require("./pty-run");
let codexLanes = [];
try {
codexLanes = lanes.listLanes().filter((l) => l.tool === "codex" && l.run_id);
} catch {
return 0;
}
let nudged = 0;
for (const lane of codexLanes) {
try {
// No record under the current stage means the lane never entered a
// pipeline node (a fresh lane sits at `idle`) — nothing to ask about.
const record = (lane.stages || {})[lane.stage];
if (!record || record.evidence) {
nudgedFor.delete(lane.id);
continue;
}
const enteredAt = Date.parse(record.enteredAt);
if (!Number.isFinite(enteredAt) || Date.now() - enteredAt < NUDGE_DELAY_MS) continue;
if (nudgedFor.get(lane.id) === lane.stage) continue;
const runId = runIdForLane(lane.id);
if (!tmux.hasSession(runId)) continue;
// A bare shell prompt has no chat turn to submit into — pressing Enter
// there would run the reminder text as a shell command instead.
if (SHELL_COMMANDS.has(tmux.paneCommand(runId) || "")) continue;
tmux.sendText(
runId,
`[agenthub] Stage "${lane.stage}" was inferred and has no evidence — run: agenthub stage ${lane.stage} --tool codex --evidence "..."`
);
nudgedFor.set(lane.id, lane.stage);
nudged += 1;
} catch {
/* one lane's failure must not abort the sweep */
}
}
return nudged;
}
/** Unref'd so it never blocks shutdown; disable with DASHBOARD_CODEX_NUDGE_MS=0. */
function startCodexNudge() {
const POLL_MS = process.env.DASHBOARD_CODEX_NUDGE_MS
? Number(process.env.DASHBOARD_CODEX_NUDGE_MS)
: 60_000;
if (!Number.isFinite(POLL_MS) || POLL_MS <= 0) return;
const timer = setInterval(() => {
try {
sweepCodexNudges();
} catch {
/* a sweep pass never throws into the event loop */
}
}, POLL_MS);
if (timer.unref) timer.unref();
}
function __resetNudgeState() {
nudgedFor.clear();
}
module.exports = {
sweepCodexNudges,
startCodexNudge,
__resetNudgeState,
};