feat(lanes): auto-advance a codex lane's stage from its rollout detection
A codex session emits no hooks and rarely calls `ccam stage` on its own, so its card sat at `idle` forever while the amber "detected" badge was the only thing that ever moved. `codex-ingest.js` now advances the declared `stage` from its own written detection — the one place in CCAM where an inference moves the blue `current` ring, and only for `codex` lanes. It stays honest: gated on `recordDetection` having written (so forward-only, declared-wins and the detection TTL are inherited unchanged), passes NO evidence (the node renders `passed-no-evidence` amber, never `done`), and no template's last node carries detect rules, so a lane can never be auto-declared `done`. `setStage` grows a `keepDetection` option — its sole caller — so the detection columns survive the advance and the card keeps showing which command moved it instead of looking like an agent's own claim. Two levers then get the evidence auto-advance cannot supply: - `codex-nudge.js` now nudges on a stage held 2+ min with no evidence, instead of on a detection sitting ahead of the declared stage — after auto-advance the old condition can never fire, since the two now match. - `ccam lanes agents install` on a codex lane writes the marker-delimited `<!-- ccam:stage:start -->` block into the lane's own AGENTS.md — the one instruction channel Codex reads — instead of copying agent templates into `.claude/agents/`, which it never loads. Appended, user prose preserved, reinstall replaces exactly that block. Verified with a real `codex exec` run: stage moved to `watching-pr` with `evidence: null` and the detection signal intact. 1364 server tests pass.
This commit is contained in:
+27
-26
@@ -1,35 +1,34 @@
|
||||
/**
|
||||
* @file Safety-net reminder for codex lanes whose auto-detected stage has
|
||||
* clearly moved past the last stage the session actually *declared* (see
|
||||
* `server/lib/stage-detect.js` / `server/lib/codex-ingest.js`). A codex
|
||||
* session only declares a stage when it calls `ccam stage` on its own —
|
||||
* there is no hook or wrapper that does it for it — so a lane can sit with a
|
||||
* stale declared stage indefinitely even while its pipeline map keeps
|
||||
* painting fresh amber "detected" badges.
|
||||
* @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
|
||||
* `ccam 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` or `detected_stage` itself — the
|
||||
* declared-wins / inferred-never-`done` invariants in lanes.js are untouched.
|
||||
* It only types a plain-text reminder into the lane's own live tmux pane
|
||||
* 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 any stage change that results is still a REAL declaration
|
||||
* the codex session made itself.
|
||||
* 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 detected_stage already nudged for, so the 60s
|
||||
// sweep doesn't retype the same reminder every pass. Cleared implicitly: once
|
||||
// the session declares (setStage nulls detected_stage) or detection advances
|
||||
// again, the value here no longer matches and a fresh nudge is due.
|
||||
// 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 fresh detection sits ahead of
|
||||
* its declared stage, has held for at least NUDGE_DELAY_MS, and hasn't
|
||||
* already been nudged for that exact detected stage.
|
||||
* 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() {
|
||||
@@ -47,14 +46,16 @@ function sweepCodexNudges() {
|
||||
let nudged = 0;
|
||||
for (const lane of codexLanes) {
|
||||
try {
|
||||
if (!lane.detected_stage || lane.detected_stage === lane.stage) {
|
||||
// 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;
|
||||
}
|
||||
if (lanes.detectionIsStale(lane.detected_at)) continue;
|
||||
const detectedAt = Date.parse(lane.detected_at);
|
||||
if (!Number.isFinite(detectedAt) || Date.now() - detectedAt < NUDGE_DELAY_MS) continue;
|
||||
if (nudgedFor.get(lane.id) === lane.detected_stage) 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;
|
||||
@@ -64,9 +65,9 @@ function sweepCodexNudges() {
|
||||
|
||||
tmux.sendText(
|
||||
runId,
|
||||
`[ccam] You appear to have moved on to "${lane.detected_stage}" — run: ccam stage ${lane.detected_stage} --tool codex --evidence "..."`
|
||||
`[ccam] Stage "${lane.stage}" was inferred and has no evidence — run: ccam stage ${lane.stage} --tool codex --evidence "..."`
|
||||
);
|
||||
nudgedFor.set(lane.id, lane.detected_stage);
|
||||
nudgedFor.set(lane.id, lane.stage);
|
||||
nudged += 1;
|
||||
} catch {
|
||||
/* one lane's failure must not abort the sweep */
|
||||
|
||||
Reference in New Issue
Block a user