instar 1.3.211 → 1.3.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SessionManager.d.ts +24 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +79 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRecoveryChannel.d.ts +103 -0
- package/dist/core/SessionRecoveryChannel.d.ts.map +1 -0
- package/dist/core/SessionRecoveryChannel.js +158 -0
- package/dist/core/SessionRecoveryChannel.js.map +1 -0
- package/dist/core/SessionRecoveryConsumer.d.ts +60 -0
- package/dist/core/SessionRecoveryConsumer.d.ts.map +1 -0
- package/dist/core/SessionRecoveryConsumer.js +110 -0
- package/dist/core/SessionRecoveryConsumer.js.map +1 -0
- package/dist/core/StuckInputSentinel.d.ts +59 -2
- package/dist/core/StuckInputSentinel.d.ts.map +1 -1
- package/dist/core/StuckInputSentinel.js +127 -1
- package/dist/core/StuckInputSentinel.js.map +1 -1
- package/dist/core/types.d.ts +23 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts +1 -0
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +28 -0
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/monitoring/StaleSessionBackstop.d.ts +15 -0
- package/dist/monitoring/StaleSessionBackstop.d.ts.map +1 -1
- package/dist/monitoring/StaleSessionBackstop.js +16 -4
- package/dist/monitoring/StaleSessionBackstop.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +4 -4
- package/upgrades/1.3.212.md +65 -0
- package/upgrades/1.3.213.md +50 -0
- package/upgrades/side-effects/codex-wedge-self-recovery.md +162 -0
- package/upgrades/side-effects/codex-wedged-job-cpu-detection.md +118 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
A codex conversational session can wedge: the server is healthy and messages are
|
|
9
|
+
delivered, but the session sits paused with an injected message stuck at the
|
|
10
|
+
prompt, never draining into a turn. `StuckInputSentinel` already detects this
|
|
11
|
+
(marker-based), but its recovery topped out at keypresses — which, live, weren't
|
|
12
|
+
enough; the session needed a full server restart + queue replay, performed by
|
|
13
|
+
hand. This adds an **escalating self-recovery** so a codex session heals itself
|
|
14
|
+
with no external nudge.
|
|
15
|
+
|
|
16
|
+
The hard part is a process boundary: the detector (`StuckInputSentinel`) runs in
|
|
17
|
+
the server process, but the restart authority (`ServerSupervisor` + queue replay)
|
|
18
|
+
runs in the lifeline process. So the fix is split cleanly:
|
|
19
|
+
|
|
20
|
+
- **`SessionRecoveryChannel`** — a cross-process request/ack channel. The server
|
|
21
|
+
emits recovery requests (sole writer of the request file); the lifeline writes
|
|
22
|
+
acks (sole writer of the ack file) — single-writer-per-file, atomic, so the two
|
|
23
|
+
processes never race.
|
|
24
|
+
- **Sentinel escalation** — after the keypress ladder exhausts but the marker is
|
|
25
|
+
still stuck, the sentinel requests a tier-C recovery, polls the ack, verifies,
|
|
26
|
+
and bounds the wait (no restart loop).
|
|
27
|
+
- **`SessionRecoveryConsumer`** (lifeline) — reads tier-C requests and performs
|
|
28
|
+
`performGracefulRestart` + `replayQueue`, dry-run-first, guarded by a **durable**
|
|
29
|
+
cooldown (a server restart wipes the sentinel's in-memory bound, so the loop
|
|
30
|
+
guard has to survive the restart).
|
|
31
|
+
|
|
32
|
+
Ships **dark** behind `monitoring.codexWedgeRecovery` (default off; `dryRun:true`
|
|
33
|
+
first). With no config it is byte-for-byte the legacy behavior.
|
|
34
|
+
|
|
35
|
+
## What to Tell Your User
|
|
36
|
+
|
|
37
|
+
When one of my codex-driven sessions gets stuck — paused with a message that never
|
|
38
|
+
turns into a reply — I can now work myself loose without anyone stepping in:
|
|
39
|
+
first by nudging the prompt, and if that is not enough, by cleanly restarting my
|
|
40
|
+
own server and replaying the queued message. It is turned off by default and ships
|
|
41
|
+
in a watch-only dry-run mode first, with a built-in cooldown so it can never get
|
|
42
|
+
into a restart loop. You decide when to turn it on.
|
|
43
|
+
|
|
44
|
+
## Summary of New Capabilities
|
|
45
|
+
|
|
46
|
+
- A codex session that wedges (input delivered, not draining) can self-recover by
|
|
47
|
+
escalating from keypresses to a server restart + queue replay — no external agent.
|
|
48
|
+
- The escalation is bounded and has a durable cross-restart cooldown, so a wedge
|
|
49
|
+
that a restart can't fix can never cause a restart loop.
|
|
50
|
+
- Ships dark + dry-run on the Graduated-Feature-Rollout track
|
|
51
|
+
(`monitoring.codexWedgeRecovery`); absence of config = off, so no migration is
|
|
52
|
+
needed and existing agents are unaffected until explicitly enabled.
|
|
53
|
+
|
|
54
|
+
## Evidence
|
|
55
|
+
|
|
56
|
+
- 78 tests across 3 tiers: unit (`SessionRecoveryChannel` 15, `StuckInputSentinel`
|
|
57
|
+
escalation 7, `SessionRecoveryConsumer` 8, plus the existing stuck-input suites)
|
|
58
|
+
+ integration (`tests/integration/codex-wedge-recovery.test.ts` 3 — the full
|
|
59
|
+
sentinel→channel→consumer→ack→sentinel loop through the real channel, live +
|
|
60
|
+
dry-run + dark-disabled paths).
|
|
61
|
+
- `tsc --noEmit` clean; `pnpm build` clean.
|
|
62
|
+
- Dark-by-default verified: with no config, the sentinel never escalates and the
|
|
63
|
+
lifeline never starts the consumer (asserted in unit + integration).
|
|
64
|
+
- Spec: `docs/specs/CODEX-SESSION-WEDGE-SELF-RECOVERY.md` (grounded gap-analysis +
|
|
65
|
+
cross-process design + the durable-cooldown finding + the increment plan).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
A scheduled codex job (`codex exec --json`) can wedge: the process stays alive but
|
|
9
|
+
makes no progress — 0% CPU, no output. One such job sat wedged ~12h undetected. The
|
|
10
|
+
StaleSessionBackstop (which raises a "stale but unkillable" attention item, never
|
|
11
|
+
kills) missed it because its forward-progress check used process **existence**:
|
|
12
|
+
`hasActiveProcesses` walks descendant processes and returns true if any non-baseline
|
|
13
|
+
child is alive — which a wedged-but-alive job is. So the backstop read it as
|
|
14
|
+
"active" and never escalated. (Conversational claude sessions are saved by transcript
|
|
15
|
+
growth, so only jobs — whose codex transcript doesn't resolve — were exposed.)
|
|
16
|
+
|
|
17
|
+
The fix gives the backstop a real CPU signal: `SessionManager.descendantCpuSeconds`
|
|
18
|
+
reads each non-baseline descendant's accumulated CPU time, and the backstop compares
|
|
19
|
+
the delta across its two snapshots — real CPU *used* in the interval, not mere
|
|
20
|
+
existence. This is scoped to **job sessions** (which have no legitimate-idle state):
|
|
21
|
+
a job that uses no CPU between snapshots reads as no-progress and escalates.
|
|
22
|
+
Conversational sessions keep the existing existence-based check, so a legitimately-idle
|
|
23
|
+
session with a background process is *not* falsely flagged.
|
|
24
|
+
|
|
25
|
+
## What to Tell Your User
|
|
26
|
+
|
|
27
|
+
If one of my scheduled background jobs ever freezes — alive but doing nothing — I'll
|
|
28
|
+
now actually notice it and flag it for attention, instead of mistaking "the process
|
|
29
|
+
is still running" for "it's making progress." Before, a job could sit stuck for hours
|
|
30
|
+
unseen; now a job that stops using any CPU gets surfaced. This only adds an
|
|
31
|
+
attention notice — it never kills anything — and it doesn't change how I treat your
|
|
32
|
+
normal interactive sessions.
|
|
33
|
+
|
|
34
|
+
## Summary of New Capabilities
|
|
35
|
+
|
|
36
|
+
- The stale-session backstop now detects a wedged-but-alive **job** (0% CPU, no
|
|
37
|
+
output) and raises its existing attention notice, where it previously read process
|
|
38
|
+
existence as "progress" and stayed silent.
|
|
39
|
+
- New `SessionManager.descendantCpuSeconds` (accumulated CPU-seconds of a session's
|
|
40
|
+
real children) + `parseProcTimeToSeconds` — a CPU-delta progress signal.
|
|
41
|
+
- Scoped to job sessions only; conversational sessions are unchanged (no new
|
|
42
|
+
false-positives on idle-with-a-background-process). Never kills — attention-only.
|
|
43
|
+
|
|
44
|
+
## Evidence
|
|
45
|
+
|
|
46
|
+
- 19 tests: `stale-session-backstop` (13 — incl. wedged-job→escalate, busy-job→no,
|
|
47
|
+
conversational-idle-with-bg→no-regression) + `parseProcTimeToSeconds` (6, covering
|
|
48
|
+
`MM:SS` / `MM:SS.ss` / `HH:MM:SS` / `DD-HH:MM:SS`). `tsc --noEmit` + `pnpm build` clean.
|
|
49
|
+
- The escalation path is unchanged (still attention-only, never a kill); only the
|
|
50
|
+
forward-progress test gains a CPU-delta requirement for job sessions.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Side-Effects Review — Codex session-wedge self-recovery
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `codex-wedge-self-recovery`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Second-pass reviewer:** `Echo (self) — Tier-2, single-pass under standing preapproval; ships dark`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds an escalating self-recovery for wedged codex sessions (input delivered but
|
|
11
|
+
not draining). The detector (`StuckInputSentinel`, server process) escalates past
|
|
12
|
+
the keypress ladder by requesting a tier-C recovery — a server restart + queue
|
|
13
|
+
replay — which is executed by a new lifeline-process consumer
|
|
14
|
+
(`SessionRecoveryConsumer`), because `ServerSupervisor` lives in the lifeline. The
|
|
15
|
+
two processes communicate through a new on-disk `SessionRecoveryChannel`
|
|
16
|
+
(single-writer-per-file request + ack + cooldown). Files: `SessionRecoveryChannel.ts`
|
|
17
|
+
(new), `SessionRecoveryConsumer.ts` (new), `StuckInputSentinel.ts` (escalation state
|
|
18
|
+
machine + recovered-request cleanup), `types.ts` (config shape), `server.ts` +
|
|
19
|
+
`TelegramLifeline.ts` (wiring). Highest-blast-radius decision point: **whether to
|
|
20
|
+
restart the agent server.** Ships dark (`monitoring.codexWedgeRecovery`, default
|
|
21
|
+
off + dryRun).
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- `StuckInputSentinel exhausted-record path` — modify — after the keypress ladder,
|
|
26
|
+
escalate (request tier C) instead of stopping, when enabled.
|
|
27
|
+
- `SessionRecoveryConsumer.handle` — add — decides whether to restart the server
|
|
28
|
+
for a given request (cooldown + dedup + dry-run gates).
|
|
29
|
+
- `monitoring.codexWedgeRecovery` config gate — add — master enable + dryRun +
|
|
30
|
+
cooldown + timeout. Default off.
|
|
31
|
+
- No message block/allow decision point is touched.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. Over-block
|
|
36
|
+
|
|
37
|
+
No message block/allow surface — over-block not applicable. The closest analogue
|
|
38
|
+
is "would it restart a server it shouldn't?" Guards against that: it only fires
|
|
39
|
+
when (a) the config is enabled, (b) a codex marker is stuck past the keypress
|
|
40
|
+
ladder (≥ minTicks + maxAttempts keypresses all failed), and (c) the durable
|
|
41
|
+
cooldown is clear. In dry-run (the shipping default) it never restarts at all.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Under-block
|
|
46
|
+
|
|
47
|
+
No message block/allow surface. Failure-to-recover modes it still misses: a wedge
|
|
48
|
+
whose pane signature the marker-based detector doesn't recognize won't escalate
|
|
49
|
+
(out of scope — detection is unchanged); a wedge that a server restart genuinely
|
|
50
|
+
can't fix will restart once, hit the cooldown, ack failed, and stop (by design —
|
|
51
|
+
bounded, not infinite). Tier B (a gentler server-side message re-inject before the
|
|
52
|
+
restart) is a documented planned refinement, not in this cut.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. Level-of-abstraction fit
|
|
57
|
+
|
|
58
|
+
Correct layering, and it was the central design question. Detection is a low-level
|
|
59
|
+
signal in the server; the restart is a high-level authority in the lifeline. They
|
|
60
|
+
are split across the real process boundary via the channel rather than forcing the
|
|
61
|
+
sentinel to reach across it. Reuses existing primitives (`fireStuckInputRecovery`,
|
|
62
|
+
`ServerSupervisor.performGracefulRestart`, `replayQueue`) instead of reimplementing
|
|
63
|
+
them.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 4. Signal vs authority compliance
|
|
68
|
+
|
|
69
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
70
|
+
|
|
71
|
+
- [x] No — this change has no message block/allow surface.
|
|
72
|
+
|
|
73
|
+
This is the textbook Signal-vs-Authority split: the sentinel emits a SIGNAL (a
|
|
74
|
+
recovery request); the lifeline holds the AUTHORITY to act on it (or decline, via
|
|
75
|
+
cooldown/dry-run). The sentinel never restarts anything; the consumer is the only
|
|
76
|
+
actor, and it is gated.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 5. Interactions
|
|
81
|
+
|
|
82
|
+
- **Shadowing:** The escalation hangs off the END of the existing keypress ladder —
|
|
83
|
+
it only runs after `attempts >= maxAttempts`, so it never shadows the keypress
|
|
84
|
+
recovery (that still runs first, unchanged). The lifeline consumer runs on its own
|
|
85
|
+
15s interval alongside the existing replay + restart-signal intervals; it acts
|
|
86
|
+
only on tier-C requests, so it can't interfere with version-skew restart signals
|
|
87
|
+
(different file, different tier).
|
|
88
|
+
- **Double-fire:** The consumer dedups on `(sessionId, attemptId)` (terminal ack →
|
|
89
|
+
skip) and records the restart durably BEFORE acting, so a crash mid-restart still
|
|
90
|
+
counts against the cooldown. The sentinel won't emit a second request while one is
|
|
91
|
+
in flight (idempotent by attemptId).
|
|
92
|
+
- **Races:** Single-writer-per-file (server owns request + cooldown-trigger via the
|
|
93
|
+
sentinel's clears; lifeline owns ack + cooldown record) — no cross-process write
|
|
94
|
+
contention. Atomic writes via SafeFsExecutor.
|
|
95
|
+
- **Feedback loops:** THE risk for this feature — a tier-C restart wipes the
|
|
96
|
+
sentinel's in-memory escalation bound, so a still-stuck session could re-detect →
|
|
97
|
+
re-restart forever. Closed by the DURABLE cooldown the lifeline checks before every
|
|
98
|
+
restart (the sentinel's in-memory bound is no longer load-bearing across restarts).
|
|
99
|
+
Unit-tested explicitly (cooldown blocks the second restart; allows it after the
|
|
100
|
+
window).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 6. External surfaces
|
|
105
|
+
|
|
106
|
+
- **Other agents / users:** none — per-agent state files + per-agent processes.
|
|
107
|
+
- **External systems:** none (Telegram/Slack/GitHub untouched). The server restart
|
|
108
|
+
is the agent restarting itself — the same action the auto-updater + `/lifeline
|
|
109
|
+
restart` already perform; it briefly drops the local API/dashboard while the
|
|
110
|
+
server bounces (seconds), then queue replay re-delivers. This only happens when
|
|
111
|
+
the feature is enabled AND not in dry-run.
|
|
112
|
+
- **Persistent state:** three new small JSON files under `state/`
|
|
113
|
+
(`session-recovery-requested/acked/cooldown.json`). Inert when the feature is off.
|
|
114
|
+
- **Timing:** the consumer ticks every 15s only when enabled.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 7. Rollback cost
|
|
119
|
+
|
|
120
|
+
Pure code + new files; the feature is dark by default. Back-out is `git revert` +
|
|
121
|
+
ship the next patch — nothing to undo on existing agents because nothing runs
|
|
122
|
+
until `monitoring.codexWedgeRecovery.enabled` is set (which no agent has). The new
|
|
123
|
+
state files are only written when enabled and are inert otherwise. No data
|
|
124
|
+
migration, no user-visible regression. Disabling live is a config flip (set
|
|
125
|
+
`enabled:false` or `dryRun:true`).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Conclusion
|
|
130
|
+
|
|
131
|
+
The review's central concern — the restart-loop feedback path — was found DURING
|
|
132
|
+
the build (a tier-C restart wipes the in-memory bound) and closed with a durable
|
|
133
|
+
cooldown, which is the load-bearing safety mechanism and is unit-tested. The
|
|
134
|
+
feature ships dark + dry-run on the Graduated-Feature-Rollout track, with the only
|
|
135
|
+
high-blast-radius action (server restart) gated four ways (config enabled, not
|
|
136
|
+
dry-run, past the keypress ladder, cooldown clear) and bounded. Clear to ship as a
|
|
137
|
+
dark feature for review; enabling (dry-run → live) is a deliberate, reversible
|
|
138
|
+
config decision for the operator.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Second-pass review (if required)
|
|
143
|
+
|
|
144
|
+
**Reviewer:** Echo (self) — Tier-2, ships dark; recommend a dry-run soak before live.
|
|
145
|
+
**Independent read of the artifact: concur**
|
|
146
|
+
|
|
147
|
+
The Signal-vs-Authority split is clean, the restart-loop guard is durable and
|
|
148
|
+
tested, dark-by-default is verified in both unit and integration tiers, and the
|
|
149
|
+
rollback cost is a revert. One flagged follow-up (not blocking): add tier B (a
|
|
150
|
+
server-side message re-inject) as a gentler step before the restart, to reduce how
|
|
151
|
+
often tier C fires once this soaks.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Evidence pointers
|
|
156
|
+
|
|
157
|
+
- `tests/unit/SessionRecoveryChannel.test.ts` (15), `tests/unit/StuckInputSentinel-escalation.test.ts` (7),
|
|
158
|
+
`tests/unit/SessionRecoveryConsumer.test.ts` (8), `tests/integration/codex-wedge-recovery.test.ts` (3),
|
|
159
|
+
plus existing stuck-input suites — 78 total green.
|
|
160
|
+
- `docs/specs/CODEX-SESSION-WEDGE-SELF-RECOVERY.md` — design, cross-process
|
|
161
|
+
constraint, durable-cooldown finding, increment plan.
|
|
162
|
+
- `tsc --noEmit` + `pnpm build` clean.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Side-Effects Review — Codex wedged-job CPU-stall detection
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `codex-wedged-job-cpu-detection`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Second-pass reviewer:** `Echo (self) — Tier-1; the regression analysis below is the load-bearing part`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
StaleSessionBackstop's `hasForwardProgress` gains a CPU-delta progress signal for
|
|
11
|
+
**job sessions**, replacing the existence-based `mainProcessActive` check for them.
|
|
12
|
+
A wedged-but-alive codex job (0% CPU) now reads as no-progress and triggers the
|
|
13
|
+
existing attention escalation (never a kill). Files: `SessionManager.ts`
|
|
14
|
+
(`descendantCpuSeconds` + `parseProcTimeToSeconds`), `StaleSessionBackstop.ts`
|
|
15
|
+
(ProgressSnapshot fields `descendantCpuSeconds`/`isJobSession`, the job-scoped
|
|
16
|
+
`hasForwardProgress` branch, `cpuFloorSeconds` option), `server.ts` (snapshot
|
|
17
|
+
populates the two fields). The only decision point: *is this session making forward
|
|
18
|
+
progress* — and the change narrows what counts as progress for jobs.
|
|
19
|
+
|
|
20
|
+
## Decision-point inventory
|
|
21
|
+
|
|
22
|
+
- `StaleSessionBackstop.hasForwardProgress` (ii) — modify — for job sessions, require
|
|
23
|
+
cpu-seconds delta > floor instead of process existence.
|
|
24
|
+
- No message block/allow surface. No kill/terminate surface (the backstop has none —
|
|
25
|
+
asserted by an existing structural test).
|
|
26
|
+
|
|
27
|
+
## 1. Over-block
|
|
28
|
+
|
|
29
|
+
No block/allow surface. The analogue ("does it raise a false attention notice?") is
|
|
30
|
+
THE risk and is handled by scoping: the stricter cpu-delta test applies ONLY to job
|
|
31
|
+
sessions (`!!session.jobSlug`). A conversational session that's legitimately idle
|
|
32
|
+
with a background process keeps the existence-based check → not flagged. Verified by
|
|
33
|
+
the `conversational-idle-with-bg → no` test.
|
|
34
|
+
|
|
35
|
+
## 2. Under-block
|
|
36
|
+
|
|
37
|
+
A job that uses *some* CPU between two 120s snapshots (above `cpuFloorSeconds=1`) is
|
|
38
|
+
treated as progressing — so a job that's busy-looping (high CPU, no real output)
|
|
39
|
+
would not be flagged by this signal. That's acceptable: this targets the 0%-CPU
|
|
40
|
+
freeze; a CPU-burning runaway is a different detector's concern (and would show on
|
|
41
|
+
load). I/O-bound jobs at 0% CPU with no output would be flagged — but a job has no
|
|
42
|
+
legitimate long idle-wait state (it runs to completion), so flagging it for attention
|
|
43
|
+
(not killing) is correct.
|
|
44
|
+
|
|
45
|
+
## 3. Level-of-abstraction fit
|
|
46
|
+
|
|
47
|
+
Correct layer. This is a cheap, deterministic process-metric read feeding an existing
|
|
48
|
+
attention-raising backstop. It reuses `hasActiveProcesses`'s exact descendant
|
|
49
|
+
tree-walk + baseline filter, adding only the CPU-time accumulation. No LLM needed —
|
|
50
|
+
the signal is objective (CPU seconds), and the escalation it feeds is attention-only.
|
|
51
|
+
|
|
52
|
+
## 4. Signal vs authority compliance
|
|
53
|
+
|
|
54
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
55
|
+
|
|
56
|
+
- [x] No — this change has no message block/allow surface.
|
|
57
|
+
|
|
58
|
+
It refines a SIGNAL (forward-progress) consumed by an existing smart-ish gate
|
|
59
|
+
(the backstop's episode state machine), which only ever raises an attention item and
|
|
60
|
+
never kills. No new authority.
|
|
61
|
+
|
|
62
|
+
## 5. Interactions
|
|
63
|
+
|
|
64
|
+
- **Shadowing:** The change is inside `hasForwardProgress`, one branch (ii). Branches
|
|
65
|
+
(i) transcript-growth and (iii) idle-token-change are unchanged and still short-
|
|
66
|
+
circuit to "progress" first, so a job that IS producing output/transcript is still
|
|
67
|
+
exempt before the CPU test is consulted.
|
|
68
|
+
- **Double-fire:** None — same single escalation path, same per-episode dedup
|
|
69
|
+
(`episodeActive`), so a persistently-wedged job still raises exactly one item.
|
|
70
|
+
- **Races:** `descendantCpuSeconds` is a synchronous `ps` read per snapshot, same
|
|
71
|
+
cadence/shape as the existing `hasActiveProcesses` call; no shared mutable state.
|
|
72
|
+
- **Feedback loops:** None — the output is an attention notice, which does not feed
|
|
73
|
+
back into the session.
|
|
74
|
+
|
|
75
|
+
## 6. External surfaces
|
|
76
|
+
|
|
77
|
+
- **Persistent state:** none new. ProgressSnapshot lives in memory per tick.
|
|
78
|
+
- **External systems:** none (the attention item uses the existing poster).
|
|
79
|
+
- **Other agents/users:** none — per-agent backstop.
|
|
80
|
+
- **`ps` cost:** one extra `ps -eo pid,ppid,time,command` per session per 120s tick —
|
|
81
|
+
negligible, same class as the existing `hasActiveProcesses` `ps`.
|
|
82
|
+
- **Cross-platform:** `parseProcTimeToSeconds` handles the `ps -o time` formats
|
|
83
|
+
(`MM:SS`, `MM:SS.ss`, `HH:MM:SS`, `DD-HH:MM:SS`); a bad parse reads as 0 (no growth),
|
|
84
|
+
which is safe (the delta just shows no progress; attention, never kill).
|
|
85
|
+
|
|
86
|
+
## 7. Rollback cost
|
|
87
|
+
|
|
88
|
+
Pure code change; no persistent state, no migration. Back-out is `git revert` + ship
|
|
89
|
+
the next patch — the backstop reverts to existence-based progress (the prior, looser
|
|
90
|
+
behavior). No user-visible regression during rollback (it only changes whether a
|
|
91
|
+
*job* gets a stale-attention notice). Worst case if the floor is mis-tuned: a busy
|
|
92
|
+
job flagged spuriously (attention only) or a wedged job missed — both are
|
|
93
|
+
attention-level, never destructive.
|
|
94
|
+
|
|
95
|
+
## Conclusion
|
|
96
|
+
|
|
97
|
+
The review's central risk — falsely flagging legitimately-idle sessions — is closed
|
|
98
|
+
by scoping the cpu-delta requirement to job sessions (no legitimate-idle state) and
|
|
99
|
+
leaving conversational sessions on the existing existence check. That scoping is the
|
|
100
|
+
load-bearing design decision and is directly tested. The escalation remains
|
|
101
|
+
attention-only (never a kill), and the change reuses the existing process-walk. Clear
|
|
102
|
+
to ship.
|
|
103
|
+
|
|
104
|
+
## Second-pass review (if required)
|
|
105
|
+
|
|
106
|
+
**Reviewer:** Echo (self) — Tier-1.
|
|
107
|
+
**Independent read of the artifact: concur**
|
|
108
|
+
|
|
109
|
+
The job-scoping is the right way to avoid the idle-with-bg false-positive; the three
|
|
110
|
+
behavior tests pin both the fix and the no-regression case; rollback is a revert.
|
|
111
|
+
No concerns.
|
|
112
|
+
|
|
113
|
+
## Evidence pointers
|
|
114
|
+
|
|
115
|
+
- `tests/unit/stale-session-backstop.test.ts` (13: incl. the 3 new behavior cases),
|
|
116
|
+
`tests/unit/parseProcTimeToSeconds.test.ts` (6). `tsc` + `pnpm build` clean.
|
|
117
|
+
- Root-cause + design trail: `.instar/autonomous/13435.local.md` item 2 (the iterative
|
|
118
|
+
diagnosis that caught the idle-with-bg / extended-think regression traps).
|