@lucascouts/claude-agent-tui 0.1.0

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.
Files changed (112) hide show
  1. package/LICENSE +191 -0
  2. package/NOTICE +14 -0
  3. package/README.md +50 -0
  4. package/dist/acp-agent.d.ts +594 -0
  5. package/dist/acp-agent.d.ts.map +1 -0
  6. package/dist/acp-agent.js +2139 -0
  7. package/dist/ansi-mirror.d.ts +42 -0
  8. package/dist/ansi-mirror.d.ts.map +1 -0
  9. package/dist/ansi-mirror.js +61 -0
  10. package/dist/besteffort.d.ts +44 -0
  11. package/dist/besteffort.d.ts.map +1 -0
  12. package/dist/besteffort.js +100 -0
  13. package/dist/billing/entrypoint-guard.d.ts +97 -0
  14. package/dist/billing/entrypoint-guard.d.ts.map +1 -0
  15. package/dist/billing/entrypoint-guard.js +166 -0
  16. package/dist/claude-path.d.ts +12 -0
  17. package/dist/claude-path.d.ts.map +1 -0
  18. package/dist/claude-path.js +61 -0
  19. package/dist/diff-enriched-reader.d.ts +41 -0
  20. package/dist/diff-enriched-reader.d.ts.map +1 -0
  21. package/dist/diff-enriched-reader.js +106 -0
  22. package/dist/diff-source.d.ts +104 -0
  23. package/dist/diff-source.d.ts.map +1 -0
  24. package/dist/diff-source.js +164 -0
  25. package/dist/end-of-turn.d.ts +172 -0
  26. package/dist/end-of-turn.d.ts.map +1 -0
  27. package/dist/end-of-turn.js +415 -0
  28. package/dist/engine-lifecycle.d.ts +222 -0
  29. package/dist/engine-lifecycle.d.ts.map +1 -0
  30. package/dist/engine-lifecycle.js +236 -0
  31. package/dist/engine-pty.d.ts +143 -0
  32. package/dist/engine-pty.d.ts.map +1 -0
  33. package/dist/engine-pty.js +222 -0
  34. package/dist/engine-watcher.d.ts +83 -0
  35. package/dist/engine-watcher.d.ts.map +1 -0
  36. package/dist/engine-watcher.js +173 -0
  37. package/dist/engine.d.ts +30 -0
  38. package/dist/engine.d.ts.map +1 -0
  39. package/dist/engine.js +34 -0
  40. package/dist/event-switch.d.ts +164 -0
  41. package/dist/event-switch.d.ts.map +1 -0
  42. package/dist/event-switch.js +206 -0
  43. package/dist/gate/port.d.ts +38 -0
  44. package/dist/gate/port.d.ts.map +1 -0
  45. package/dist/gate/port.js +126 -0
  46. package/dist/gate/settings-writer.d.ts +130 -0
  47. package/dist/gate/settings-writer.d.ts.map +1 -0
  48. package/dist/gate/settings-writer.js +349 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +106 -0
  52. package/dist/jsonl.d.ts +267 -0
  53. package/dist/jsonl.d.ts.map +1 -0
  54. package/dist/jsonl.js +527 -0
  55. package/dist/lib.d.ts +6 -0
  56. package/dist/lib.d.ts.map +1 -0
  57. package/dist/lib.js +5 -0
  58. package/dist/linearize.d.ts +219 -0
  59. package/dist/linearize.d.ts.map +1 -0
  60. package/dist/linearize.js +444 -0
  61. package/dist/live-diff-env.d.ts +7 -0
  62. package/dist/live-diff-env.d.ts.map +1 -0
  63. package/dist/live-diff-env.js +18 -0
  64. package/dist/live-subagent-env.d.ts +7 -0
  65. package/dist/live-subagent-env.d.ts.map +1 -0
  66. package/dist/live-subagent-env.js +19 -0
  67. package/dist/permissions/allow-inject.d.ts +67 -0
  68. package/dist/permissions/allow-inject.d.ts.map +1 -0
  69. package/dist/permissions/allow-inject.js +85 -0
  70. package/dist/permissions/deny.d.ts +60 -0
  71. package/dist/permissions/deny.d.ts.map +1 -0
  72. package/dist/permissions/deny.js +81 -0
  73. package/dist/permissions/gate-wiring.d.ts +112 -0
  74. package/dist/permissions/gate-wiring.d.ts.map +1 -0
  75. package/dist/permissions/gate-wiring.js +350 -0
  76. package/dist/permissions/hook-server.d.ts +72 -0
  77. package/dist/permissions/hook-server.d.ts.map +1 -0
  78. package/dist/permissions/hook-server.js +179 -0
  79. package/dist/permissions/permission-mode.d.ts +67 -0
  80. package/dist/permissions/permission-mode.d.ts.map +1 -0
  81. package/dist/permissions/permission-mode.js +100 -0
  82. package/dist/permissions/request-permission.d.ts +102 -0
  83. package/dist/permissions/request-permission.d.ts.map +1 -0
  84. package/dist/permissions/request-permission.js +124 -0
  85. package/dist/settings.d.ts +68 -0
  86. package/dist/settings.d.ts.map +1 -0
  87. package/dist/settings.js +182 -0
  88. package/dist/stop-reason-map.d.ts +17 -0
  89. package/dist/stop-reason-map.d.ts.map +1 -0
  90. package/dist/stop-reason-map.js +33 -0
  91. package/dist/subagent-source.d.ts +63 -0
  92. package/dist/subagent-source.d.ts.map +1 -0
  93. package/dist/subagent-source.js +132 -0
  94. package/dist/subagent-watcher.d.ts +40 -0
  95. package/dist/subagent-watcher.d.ts.map +1 -0
  96. package/dist/subagent-watcher.js +108 -0
  97. package/dist/tools.d.ts +119 -0
  98. package/dist/tools.d.ts.map +1 -0
  99. package/dist/tools.js +729 -0
  100. package/dist/usage-env.d.ts +7 -0
  101. package/dist/usage-env.d.ts.map +1 -0
  102. package/dist/usage-env.js +16 -0
  103. package/dist/usage.d.ts +54 -0
  104. package/dist/usage.d.ts.map +1 -0
  105. package/dist/usage.js +53 -0
  106. package/dist/utils.d.ts +16 -0
  107. package/dist/utils.d.ts.map +1 -0
  108. package/dist/utils.js +83 -0
  109. package/dist/zed-register.d.ts +26 -0
  110. package/dist/zed-register.d.ts.map +1 -0
  111. package/dist/zed-register.js +106 -0
  112. package/package.json +79 -0
@@ -0,0 +1,415 @@
1
+ // === Story 024 — End-of-turn detector (E3 augmented predicate + Δt quiescence + turn watchdog) ===
2
+ //
3
+ // The forked engine drives the real `claude` TUI in a PTY and reads turns from the JSONL transcript
4
+ // (story 023). The pure TUI never emits `session_state_changed:state='idle'`, so there is NO native
5
+ // end-of-turn signal. This module implements the E3-derived heuristic that tells the rewritten
6
+ // `prompt()` loop when a turn ends, so it can resolve the ACP `PromptResponse{stopReason}` exactly
7
+ // once per turn (§5 BLOCKER, R2). Δt, the predicate, and the turn watchdog are consumed VERBATIM
8
+ // from the Degrau-0 E3 binding decision (experiments/DEGRAU0-RESULTS.md, Binding decision 2) — this
9
+ // module IMPLEMENTS them, it does NOT re-derive them.
10
+ //
11
+ // Task 1.1 — the terminal-stop predicate: the pure core that identifies the single terminal-stop
12
+ // `assistant` boundary candidate per turn.
13
+ import { TURN_WATCHDOG_MS } from "./linearize.js";
14
+ import { FILE_DISCOVERY_WATCHDOG_MS } from "./jsonl.js";
15
+ import { mapStopReason } from "./stop-reason-map.js";
16
+ // Re-export the two watchdog windows from their OWNING modules so the detector exposes a single
17
+ // source of truth: the turn watchdog (5583 ms) lives in linearize.ts (story 017), the file-discovery
18
+ // watchdog (2000 ms) in jsonl.ts. Re-exporting — never re-declaring — guarantees the two windows can
19
+ // neither drift apart nor be conflated (the §13 R2-vs-R3 Constraint the story forbids breaking).
20
+ export { TURN_WATCHDOG_MS, FILE_DISCOVERY_WATCHDOG_MS };
21
+ // === Story 034 (live-acceptance fix) — STALL semantics ============================================
22
+ //
23
+ // The G1 live acceptance (2026-06-11, session 9cedb4c2) faulted a REAL first turn at 5583 ms: E3
24
+ // calibrated that window on trivial Degrau-0 turns as a TOTAL-turn cap, but live turns legitimately
25
+ // exceed it (cold start, thinking, long tools) and the JSONL writes whole blocks, so long silent
26
+ // gaps are normal mid-turn. Corrected semantics: the watchdog re-arms on every observed event
27
+ // (activity = liveness) and trips only after `watchdogMs` of total SILENCE. TURN_WATCHDOG_MS stays
28
+ // exported as the E3 constant (story 017 cadence + explicit option), but is no longer the default.
29
+ /**
30
+ * Default STALL window (ms): the watchdog trips only after this much transcript silence while a
31
+ * turn is in flight. Generous on purpose — a long text block or tool run writes nothing for tens of
32
+ * seconds; the watchdog exists to fail a DEAD turn loudly (R5.2), not to cap a slow one.
33
+ */
34
+ export const TURN_STALL_WATCHDOG_MS = 120_000;
35
+ /** Env var overriding the default stall window per process (positive integer ms). */
36
+ export const TURN_WATCHDOG_ENV = "FORK_TURN_WATCHDOG_MS";
37
+ /** The default stall window: the {@link TURN_WATCHDOG_ENV} override when valid, else the constant. */
38
+ function resolveDefaultWatchdogMs() {
39
+ const raw = process.env[TURN_WATCHDOG_ENV];
40
+ if (raw !== undefined && raw !== "") {
41
+ const parsed = Number(raw);
42
+ if (Number.isFinite(parsed) && parsed > 0)
43
+ return parsed;
44
+ }
45
+ return TURN_STALL_WATCHDOG_MS;
46
+ }
47
+ /**
48
+ * The TERMINAL `message.stop_reason` values (E3 augmented predicate, Binding decision 2). An
49
+ * `assistant` event whose stop_reason is one of these is an end-of-turn boundary CANDIDATE; a
50
+ * `tool_use` pause (or `null`) is mid-turn and NOT a boundary. Typed `ReadonlySet` so the set
51
+ * cannot drift at a call site.
52
+ */
53
+ export const TERMINAL_STOP_REASONS = new Set([
54
+ "end_turn",
55
+ "stop_sequence",
56
+ "max_tokens",
57
+ ]);
58
+ /** True iff `value` is a non-null, non-array object — the guard before any property read. */
59
+ function isObject(value) {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ }
62
+ /**
63
+ * Whether `event` is a sidechain (subagent) row (story 041, R4.1). A subagent's transcript rows
64
+ * carry a non-null `parent_tool_use_id` (SDK snake_case) / `parentToolUseId` (story-015 typed name)
65
+ * pointing up to the spawning Task `tool_use` — the SAME canonical signal `linearize.ts`
66
+ * (`isSidechainMsg`) uses. A sidechain assistant row's OWN `stop_reason:'end_turn'` is the
67
+ * SUBAGENT's turn ending, NOT the parent's, so it must be excluded from terminal candidacy: it can
68
+ * never resolve the parent turn even if it reaches the detector.
69
+ */
70
+ function isSidechain(event) {
71
+ if (!isObject(event))
72
+ return false;
73
+ const pid = event.parent_tool_use_id ?? event.parentToolUseId;
74
+ return pid !== null && pid !== undefined;
75
+ }
76
+ // === Story 041 / Task 4.2 — an in-flight Task/Agent tool_use holds the turn OPEN (R4.2) ===========
77
+ //
78
+ // A main-chain `Task`/`Agent` `tool_use` spawns a sub-agent whose work can run silently for a long
79
+ // time (cold start, long tools, deep nesting). While that `tool_use` has NO matching `tool_result`
80
+ // yet, the turn is IN-FLIGHT: the silence watchdog must NOT trip and no end-of-turn may resolve,
81
+ // however long the sub-agent runs. The detector feed is main-chain-only (the pump never advances the
82
+ // detector cursor for sidechain rows — R4.1 structural), so this open-Task scan runs over main-chain
83
+ // rows: it reads `tool_use` blocks named `Task`/`Agent` off assistant rows and the `tool_result`
84
+ // blocks (`tool_use_id`) that close them off user rows.
85
+ /** The sub-agent-spawning tool names whose open `tool_use` holds the parent turn open (R4.2). */
86
+ const SUBAGENT_TOOL_NAMES = new Set(["Task", "Agent"]);
87
+ /** The `message.content[]` array of an event when present, else `[]` — read defensively (§6). */
88
+ function contentBlocksOf(event) {
89
+ const message = event.message;
90
+ if (!isObject(message))
91
+ return [];
92
+ const content = message.content;
93
+ return Array.isArray(content) ? content : [];
94
+ }
95
+ /**
96
+ * The ids of sub-agent-spawning (`Task`/`Agent`) `tool_use` blocks in `event` (R4.2). A non-assistant
97
+ * row, a string/missing `content`, or any non-`Task`/`Agent` `tool_use` (e.g. `Bash`) yields none —
98
+ * so an ordinary tool call never holds the turn. Pure; never throws.
99
+ */
100
+ function subagentToolUseIds(event) {
101
+ if (!isObject(event) || event.type !== "assistant")
102
+ return [];
103
+ const ids = [];
104
+ for (const block of contentBlocksOf(event)) {
105
+ if (isObject(block) &&
106
+ block.type === "tool_use" &&
107
+ typeof block.id === "string" &&
108
+ typeof block.name === "string" &&
109
+ SUBAGENT_TOOL_NAMES.has(block.name)) {
110
+ ids.push(block.id);
111
+ }
112
+ }
113
+ return ids;
114
+ }
115
+ /**
116
+ * The `tool_use_id`s closed by `tool_result` blocks in `event` (R4.2) — the back-reference the SDK
117
+ * writes on the user row that carries a sub-agent's result (`tool_result.tool_use_id === id`, the
118
+ * same field `event-switch.ts` models). A non-object/string `content` yields none. Pure; never throws.
119
+ */
120
+ function closedToolUseIds(event) {
121
+ const ids = [];
122
+ for (const block of contentBlocksOf(event)) {
123
+ if (isObject(block) && block.type === "tool_result" && typeof block.tool_use_id === "string") {
124
+ ids.push(block.tool_use_id);
125
+ }
126
+ }
127
+ return ids;
128
+ }
129
+ /**
130
+ * The E3 augmented predicate (Binding decision 2): is `event` an `assistant` event whose
131
+ * `message.stop_reason` is a TERMINAL value? Reads everything defensively — a non-`assistant`
132
+ * `.type`, a missing/string `message`, or a `null`/`undefined`/`tool_use` stop_reason all yield
133
+ * `false`. Pure and total: never throws.
134
+ *
135
+ * Story 041 (R4.1): a SIDECHAIN row (non-null `parent_tool_use_id`) is EXCLUDED — its terminal stop
136
+ * is the subagent's own turn ending and must never end the parent turn.
137
+ */
138
+ export function isTerminalStop(event) {
139
+ if (!isObject(event))
140
+ return false;
141
+ if (event.type !== "assistant")
142
+ return false;
143
+ if (isSidechain(event))
144
+ return false; // R4.1: a sidechain terminal stop never ends the parent turn
145
+ const message = event.message;
146
+ if (!isObject(message))
147
+ return false;
148
+ const stopReason = message.stop_reason;
149
+ return typeof stopReason === "string" && TERMINAL_STOP_REASONS.has(stopReason);
150
+ }
151
+ /**
152
+ * The LAST event in `events` satisfying {@link isTerminalStop} — the single per-turn boundary
153
+ * candidate (E3 picks the LAST terminal `assistant`, so a turn that streamed several assistant
154
+ * blocks collapses to one boundary). Returns `undefined` when no event is terminal (e.g. a
155
+ * `tool_use`-only turn still in flight). Pure; never throws on an unknown `.type` or string content.
156
+ */
157
+ export function findTerminalCandidate(events) {
158
+ for (let i = events.length - 1; i >= 0; i--) {
159
+ if (isTerminalStop(events[i]))
160
+ return events[i];
161
+ }
162
+ return undefined;
163
+ }
164
+ // === Task 2 — Δt quiescence window + queued-prompt confirmation ===================================
165
+ /**
166
+ * The Δt quiescence window (ms) — E3 binding decision 2: the SMALLEST zero-false-positive window
167
+ * (Δt sweep, experiments/DECISION.md). A terminal candidate is confirmed only after Δt of silence
168
+ * with no further user/assistant write. Consumed VERBATIM from E3 — NOT re-derived here.
169
+ */
170
+ export const DELTA_T_MS = 200;
171
+ const defaultSchedule = (fn, ms) => {
172
+ const handle = setTimeout(fn, ms);
173
+ // Don't let an armed turn timer keep the process alive on its own.
174
+ handle.unref?.();
175
+ return () => clearTimeout(handle);
176
+ };
177
+ /**
178
+ * Typed error surfaced when no end-of-turn is detected within the 5583 ms turn watchdog (R5.2). It
179
+ * carries the elapsed window plus last-seen-event diagnostics so the §5 stall is observable for R2
180
+ * runtime telemetry instead of hanging the ACP protocol silently.
181
+ */
182
+ export class TurnTimeoutError extends Error {
183
+ constructor(diag) {
184
+ super(`end-of-turn not detected within ${diag.elapsedMs}ms turn watchdog` +
185
+ (diag.sessionId ? ` (session ${diag.sessionId})` : "") +
186
+ (diag.lastEventType
187
+ ? `; last event type=${diag.lastEventType} stop_reason=${diag.lastStopReason ?? "null"}`
188
+ : ""));
189
+ this.name = "TurnTimeoutError";
190
+ this.elapsedMs = diag.elapsedMs;
191
+ this.lastEventType = diag.lastEventType;
192
+ this.lastStopReason = diag.lastStopReason;
193
+ this.sessionId = diag.sessionId;
194
+ }
195
+ }
196
+ /** The `uuid` of an event when it is a non-empty string — the per-turn `fired` latch key. */
197
+ function eventUuid(event) {
198
+ const uuid = event.uuid;
199
+ return typeof uuid === "string" && uuid.length > 0 ? uuid : undefined;
200
+ }
201
+ /** True for a `user` or `assistant` write — the only events that confirm a queued boundary (R4). */
202
+ function isUserOrAssistantWrite(event) {
203
+ return isObject(event) && (event.type === "user" || event.type === "assistant");
204
+ }
205
+ /** Last-seen-event diagnostics for a {@link TurnTimeoutError} (`.type` + `message.stop_reason`). */
206
+ function eventDiag(event) {
207
+ if (!isObject(event))
208
+ return {};
209
+ const lastEventType = typeof event.type === "string" ? event.type : undefined;
210
+ const message = event.message;
211
+ const lastStopReason = isObject(message) && typeof message.stop_reason === "string" ? message.stop_reason : undefined;
212
+ return { lastEventType, lastStopReason };
213
+ }
214
+ /**
215
+ * The end-of-turn detector: feed it the linearized turn events via {@link EndOfTurnDetector.observe}
216
+ * and it emits exactly one end-of-turn per turn. A terminal candidate (Task 1) arms a Δt timer; the
217
+ * boundary is confirmed on Δt quiescence (here) or immediately on a queued follow-up write (Task 2.2).
218
+ * A `fired` latch keyed on the boundary uuid prevents the same terminal event from firing twice.
219
+ */
220
+ export function createEndOfTurnDetector(opts) {
221
+ const schedule = opts.schedule ?? defaultSchedule;
222
+ const deltaTMs = opts.deltaTMs ?? DELTA_T_MS;
223
+ const watchdogMs = opts.watchdogMs ?? resolveDefaultWatchdogMs();
224
+ let pendingCandidate;
225
+ let cancelDelta;
226
+ let cancelWatchdog;
227
+ let lastEvent; // last event seen — diagnostics for a watchdog timeout
228
+ const firedUuids = new Set();
229
+ // Story 041 (R4.2): main-chain `Task`/`Agent` `tool_use` ids with no matching `tool_result` yet.
230
+ // While this set is non-empty a sub-agent is in flight, so the silence watchdog HOLDS (re-arms)
231
+ // instead of tripping — a long, silent sub-agent run must not cause a false end-of-turn.
232
+ const openTaskIds = new Set();
233
+ let stopped = false;
234
+ let cancelled = false; // single-resolution cancel latch (markCancelled — story 030 seam)
235
+ function clearDelta() {
236
+ cancelDelta?.();
237
+ cancelDelta = undefined;
238
+ }
239
+ /** Clear the armed turn watchdog so it cannot fire for a turn that already resolved (R5.3). */
240
+ function clearWatchdog() {
241
+ cancelWatchdog?.();
242
+ cancelWatchdog = undefined;
243
+ }
244
+ function confirmBoundary() {
245
+ if (cancelled)
246
+ return; // cancel latch (R3.4): never emit an end-of-turn for a cancelled turn
247
+ const boundary = pendingCandidate;
248
+ if (!boundary)
249
+ return;
250
+ // Latch BEFORE emit: clear the pending candidate so a re-entrant observe() inside the
251
+ // onEndOfTurn callback cannot double-confirm the same turn.
252
+ pendingCandidate = undefined;
253
+ clearDelta();
254
+ clearWatchdog(); // end-of-turn resolved → the watchdog must not fire for this turn (R5.3)
255
+ openTaskIds.clear(); // R4.2: a resolved turn starts the next one with no stale open Task
256
+ const uuid = eventUuid(boundary);
257
+ if (uuid)
258
+ firedUuids.add(uuid);
259
+ opts.onEndOfTurn(boundary);
260
+ }
261
+ function armDelta() {
262
+ clearDelta();
263
+ cancelDelta = schedule(() => {
264
+ cancelDelta = undefined;
265
+ confirmBoundary();
266
+ }, deltaTMs);
267
+ }
268
+ /**
269
+ * (Re)arm the stall watchdog: if no end-of-turn confirms within `watchdogMs` of SILENCE, the turn
270
+ * is failed loudly (Task 3.2 / story-034 stall semantics) instead of hanging the ACP protocol
271
+ * (§5 BLOCKER, R5.2). Called at turn start and again on every observed event (activity = liveness).
272
+ */
273
+ function armWatchdog() {
274
+ clearWatchdog();
275
+ cancelWatchdog = schedule(() => {
276
+ cancelWatchdog = undefined;
277
+ // Story 041 (R4.2): a main-chain `Task`/`Agent` `tool_use` is still open (no `tool_result`),
278
+ // so a sub-agent is in flight. The silence is EXPECTED — HOLD the turn by re-arming the
279
+ // watchdog instead of failing it. Without this, a long, silent sub-agent run would falsely
280
+ // trip the stall watchdog and resolve the turn early. The hold is bounded by the eventual
281
+ // `tool_result` (which empties `openTaskIds`) or a real main-chain terminal stop.
282
+ if (openTaskIds.size > 0) {
283
+ armWatchdog();
284
+ return;
285
+ }
286
+ const error = new TurnTimeoutError({
287
+ elapsedMs: watchdogMs,
288
+ sessionId: opts.sessionId,
289
+ ...eventDiag(lastEvent),
290
+ });
291
+ // R2 runtime telemetry — surface loudly via both channels; never swallow the stall.
292
+ opts.logger?.error("[end-of-turn] turn watchdog tripped:", error.message);
293
+ opts.onTurnTimeout?.(error);
294
+ }, watchdogMs);
295
+ }
296
+ return {
297
+ beginTurn() {
298
+ if (stopped || cancelled)
299
+ return;
300
+ armWatchdog();
301
+ },
302
+ observe(event) {
303
+ if (stopped || cancelled)
304
+ return;
305
+ lastEvent = event; // track for watchdog-timeout diagnostics (Task 3.2)
306
+ // Story 041 (R4.2): track open main-chain `Task`/`Agent` `tool_use`s. A sub-agent-spawning
307
+ // `tool_use` ADDS its id (the turn is now in flight on a sub-agent); a `tool_result` REMOVES
308
+ // every id it closes (`tool_use_id`). While `openTaskIds` is non-empty the watchdog HOLDS
309
+ // rather than trips (see armWatchdog). Order matters only across events, not within one row, so
310
+ // closing first then opening is fine: a row is never both the open and its own close.
311
+ for (const id of closedToolUseIds(event))
312
+ openTaskIds.delete(id);
313
+ for (const id of subagentToolUseIds(event))
314
+ openTaskIds.add(id);
315
+ // Story-034 stall semantics: a watched event is transcript ACTIVITY — the turn is alive, so
316
+ // push the silence deadline out. Only while in flight (armed): replay/load observes with no
317
+ // beginTurn and must never start a stall clock, and a cleared watchdog must stay cleared.
318
+ if (cancelWatchdog !== undefined)
319
+ armWatchdog();
320
+ // Queued-prompt / back-to-back (R4): a NEW user/assistant write arriving while a candidate is
321
+ // still pending (before Δt) confirms the PRIOR boundary immediately — cancelling the Δt timer —
322
+ // then a fresh detection cycle begins for `event` below. This is what keeps two back-to-back
323
+ // turns from merging into a single boundary.
324
+ if (pendingCandidate && event !== pendingCandidate && isUserOrAssistantWrite(event)) {
325
+ confirmBoundary();
326
+ }
327
+ if (isTerminalStop(event)) {
328
+ const uuid = eventUuid(event);
329
+ if (uuid && firedUuids.has(uuid))
330
+ return; // duplicate of an already-fired turn → idempotent
331
+ pendingCandidate = event;
332
+ armDelta(); // (re)arm the Δt quiescence timer
333
+ }
334
+ },
335
+ noteActivity() {
336
+ // Story 044 (R1.2, Option B): liveness-only kick from the sub-agent watcher. Mirrors the
337
+ // activity re-arm inside observe() but registers NO event — no lastEvent, no openTaskIds
338
+ // mutation, no pendingCandidate/Δt bookkeeping. Re-arms only while in flight (armed): a
339
+ // replay/load kick never starts a stall clock, and a stopped/cancelled detector stays silent.
340
+ if (stopped || cancelled)
341
+ return;
342
+ if (cancelWatchdog !== undefined)
343
+ armWatchdog();
344
+ },
345
+ markCancelled() {
346
+ // Single-resolution cancel seam (R3.4): set the latch and clear both timers so the detector
347
+ // cannot also fire an end-of-turn for this turn. Read-only — the ACP session/cancel →
348
+ // PromptResponse{stopReason:'cancelled'} resolution and its §8 wiring are story 030, which
349
+ // calls this seam (latch here, resolution there — the two halves stay traceable).
350
+ cancelled = true;
351
+ clearDelta();
352
+ clearWatchdog();
353
+ pendingCandidate = undefined;
354
+ openTaskIds.clear(); // R4.2: a cancelled turn drops any in-flight Task hold
355
+ },
356
+ stop() {
357
+ stopped = true;
358
+ clearDelta();
359
+ clearWatchdog();
360
+ pendingCandidate = undefined;
361
+ openTaskIds.clear(); // R4.2: teardown drops any in-flight Task hold
362
+ },
363
+ };
364
+ }
365
+ // === Task 4.2 — resolve the pending session/prompt once per boundary =============================
366
+ /** Read `event.message.stop_reason` defensively — the raw value {@link mapStopReason} consumes. */
367
+ function readStopReason(event) {
368
+ const message = event.message;
369
+ return isObject(message) ? message.stop_reason : undefined;
370
+ }
371
+ /**
372
+ * Wrap the end-of-turn detector in the awaitable the rewritten `prompt()` loop (story 023) returns
373
+ * (R2). The promise stays pending across every mid-turn event and settles EXACTLY ONCE — resolving
374
+ * with `{ stopReason: mapStopReason(boundary.message.stop_reason) }` on the confirmed boundary, or
375
+ * rejecting with the surfaced {@link TurnTimeoutError} on a watchdog trip. A single-resolution latch
376
+ * makes a detector/watchdog/cancel race settle the prompt exactly once (and stops the detector so a
377
+ * late boundary cannot produce a second settlement).
378
+ */
379
+ export function createTurnResolver(opts = {}) {
380
+ let settled = false;
381
+ let resolveFn;
382
+ let rejectFn;
383
+ const promise = new Promise((res, rej) => {
384
+ resolveFn = res;
385
+ rejectFn = rej;
386
+ });
387
+ // Single-resolution latch: a detector/watchdog/cancel race settles the prompt EXACTLY ONCE. The
388
+ // detector's own latches already clear its timers on confirm/timeout, so any late callback that
389
+ // still reaches `settle` is a harmless no-op.
390
+ function settle(action) {
391
+ if (settled)
392
+ return;
393
+ settled = true;
394
+ action();
395
+ }
396
+ const detector = createEndOfTurnDetector({
397
+ onEndOfTurn: (boundary) => settle(() => resolveFn({ stopReason: mapStopReason(readStopReason(boundary), opts.logger) })),
398
+ onTurnTimeout: (error) => settle(() => rejectFn(error)),
399
+ schedule: opts.schedule,
400
+ sessionId: opts.sessionId,
401
+ logger: opts.logger,
402
+ deltaTMs: opts.deltaTMs,
403
+ watchdogMs: opts.watchdogMs,
404
+ });
405
+ return {
406
+ detector,
407
+ promise,
408
+ cancel() {
409
+ // Claim the single-resolution latch FIRST (R2.2) so a racing detector boundary cannot also
410
+ // resolve, then mark the detector cancelled (story-024 seam, R3.1) to clear its Δt + watchdog.
411
+ settle(() => resolveFn({ stopReason: "cancelled" }));
412
+ detector.markCancelled();
413
+ },
414
+ };
415
+ }
@@ -0,0 +1,222 @@
1
+ import pty from "node-pty";
2
+ import type { PtyEngineHandle } from "./engine-pty.js";
3
+ import type { AnsiMirrorOptions } from "./ansi-mirror.js";
4
+ /**
5
+ * Minimal contract the lifecycle layer depends on from the JSONL tail watcher (story 015,
6
+ * downstream). Lifecycle only needs to STOP it on cleanup — it never reads from it. Defined
7
+ * here as the interface seam so 014 can tear the watcher down before 015 ships the concrete
8
+ * `fs.watch`/tail implementation.
9
+ */
10
+ export interface SessionWatcher {
11
+ /** Tear down the watcher: stop the fs.watch/tail, flush buffers, drop listeners. */
12
+ stop(): void;
13
+ }
14
+ /** Diagnostics passed to the {@link SessionEngineOptions.onCleanup} hook. */
15
+ export interface CleanupInfo {
16
+ sessionId: string;
17
+ /** PTY exit code, when cleanup was triggered by `onExit`. */
18
+ exitCode?: number;
19
+ /** PTY terminating signal, when cleanup was triggered by `onExit`. */
20
+ signal?: number;
21
+ }
22
+ /**
23
+ * Default PTY geometry applied when Zed supplies no terminal size (§5 Spawn defaults). Mirrors
24
+ * the story 013 spawn dimensions so a no-size resize matches a freshly-spawned PTY.
25
+ */
26
+ export declare const DEFAULT_COLS = 120;
27
+ export declare const DEFAULT_ROWS = 40;
28
+ /**
29
+ * Resize debounce window (ms). Rapid resize requests (e.g. a window drag) are coalesced and
30
+ * only the last size is applied after this window elapses, so the TUI is not thrashed (§5).
31
+ */
32
+ export declare const RESIZE_DEBOUNCE_MS = 100;
33
+ /** Construction options for a {@link SessionEngine}. */
34
+ export interface SessionEngineOptions {
35
+ /** The spawned PTY handle from story 013's `spawnClaudePty` (or the story 014 resume spawn). */
36
+ handle: PtyEngineHandle;
37
+ /** The per-session JSONL tail watcher (story 015). Optional until 015 lands. */
38
+ watcher?: SessionWatcher;
39
+ /**
40
+ * The live session registry. The engine registers itself on construction and deletes its
41
+ * entry on cleanup. Injectable so tests can observe the map churn directly.
42
+ */
43
+ sessions?: Map<string, SessionEngine>;
44
+ /** Optional diagnostics hook invoked exactly once, when cleanup runs. */
45
+ onCleanup?: (info: CleanupInfo) => void;
46
+ /** Resize debounce window in ms; defaults to {@link RESIZE_DEBOUNCE_MS}. */
47
+ resizeDebounceMs?: number;
48
+ /** Injectable timer fns (default global setTimeout/clearTimeout); fake-timer tests override. */
49
+ setTimeoutFn?: typeof setTimeout;
50
+ clearTimeoutFn?: typeof clearTimeout;
51
+ /**
52
+ * Story 035 — OPTIONAL cosmetic live ANSI mirror (§5). OFF by default: when absent/disabled the
53
+ * `p.onData` tap is NEVER attached and the engine path is byte-for-byte the read-only Degrau-1
54
+ * behaviour. When enabled (flag on + sink), the engine subscribes a stateless one-way mirror that
55
+ * forwards raw TUI bytes to the sink for §9 gating visibility, and detaches it on {@link cleanup}.
56
+ * The mirror is strictly subordinate to the JSONL tail: no state, no `SessionUpdate` (§2).
57
+ */
58
+ ansiMirror?: AnsiMirrorOptions;
59
+ }
60
+ /**
61
+ * Per-session managed engine: ONE Node process owns the PTY (§2). Task 1 wires the PTY's
62
+ * `onExit` to a single, idempotent `cleanup` that tears down the watcher and releases the
63
+ * per-session connection state exactly once (R1.1–R1.3).
64
+ *
65
+ * `onExit` is only ONE of the possible teardown triggers — an explicit kill (Task 3) routes
66
+ * through the same `cleanup`, so the per-session `disposed` guard makes the second trigger a
67
+ * no-op rather than double-freeing the watcher/session-map handles.
68
+ */
69
+ export declare class SessionEngine {
70
+ /** The pre-generated v4 session id, == the JSONL transcript basename (story 013). */
71
+ readonly sessionId: string;
72
+ /** The live PTY handle whose lifecycle this engine owns. */
73
+ readonly pty: PtyEngineHandle["pty"];
74
+ /** The JSONL tail watcher (story 015); cleared on cleanup. */
75
+ watcher?: SessionWatcher;
76
+ private readonly sessions?;
77
+ private readonly onCleanup?;
78
+ private readonly setTimeoutFn;
79
+ private readonly clearTimeoutFn;
80
+ private readonly debounceMs;
81
+ private resizeTimer?;
82
+ private pendingSize?;
83
+ private disposed;
84
+ /**
85
+ * Story 028: the AbortController for the BACKGROUND fresh-path transcript discovery poll (the
86
+ * unbounded `watchdogMs: Infinity` glob loop that arms the watcher once the transcript appears).
87
+ * Stored here (sub-task 2.1) so the lifecycle owns the handle; `cleanup()` aborts it (sub-task 3.1)
88
+ * so tearing a never-interacted session down cancels the dangling poll instead of leaking it.
89
+ */
90
+ private pendingDiscovery?;
91
+ /**
92
+ * Story 035: the `IDisposable` for the OPTIONAL cosmetic live ANSI mirror's `p.onData` tap.
93
+ * Present ONLY when the mirror flag was enabled; `undefined` on the default OFF path (where no
94
+ * `onData` listener was ever attached). `cleanup()` detaches it so the cosmetic tap does not
95
+ * outlive the session.
96
+ */
97
+ private ansiMirror?;
98
+ constructor(opts: SessionEngineOptions);
99
+ /**
100
+ * Debounced terminal resize (R2.1–R2.3). Stores the latest (cols, rows) and (re)arms a short
101
+ * debounce timer; when it fires, applies ONLY the last size via `p.resize`. Falls back to the
102
+ * 120×40 default when no size is supplied. A resize requested after the PTY has exited is
103
+ * dropped without throwing (the `disposed` guard) rather than writing to a dead handle.
104
+ */
105
+ requestResize(cols?: number, rows?: number): void;
106
+ /**
107
+ * Ctrl+C — write the interrupt byte `\x03` to the PTY to cancel the current turn (R3.1).
108
+ * No-op after the PTY has exited (R3.4) rather than writing to a dead handle.
109
+ *
110
+ * EXPOSED but deliberately NOT wired to any ACP `session/cancel` handler here — the Degrau-1
111
+ * path is read-only; the `session/cancel`→Ctrl+C wiring is story 030.
112
+ */
113
+ interrupt(): void;
114
+ /** Esc — write the escape byte `\x1b` to the PTY (R3.2). No-op after exit (R3.4). */
115
+ escape(): void;
116
+ /**
117
+ * Kill the PTY process (R3.3). No-op after exit (R3.4). In production the subsequent `onExit`
118
+ * routes through the idempotent {@link cleanup}, so no explicit teardown call is needed here.
119
+ */
120
+ kill(): void;
121
+ /**
122
+ * Idempotent teardown (R1.1–R1.3): runs the body exactly once per session. Clears any armed
123
+ * resize timer, stops the watcher, releases the session-map entry, and fires the diagnostics
124
+ * hook. A second call (e.g. explicit kill followed by `onExit`) is a no-op — it leaves no
125
+ * residual timer, watcher subscription, or session-map entry to free again.
126
+ */
127
+ cleanup(info?: {
128
+ exitCode?: number;
129
+ signal?: number;
130
+ }): void;
131
+ /**
132
+ * Store the AbortController for the background fresh-path discovery poll (story 028, sub-task 2.1).
133
+ * `cleanup()` aborts it (sub-task 3.1) so a session torn down before its first interaction cancels
134
+ * the dangling `watchdogMs: Infinity` poll instead of leaking it. The engine owns the handle.
135
+ */
136
+ setPendingDiscovery(ac: AbortController): void;
137
+ /** Whether {@link cleanup} has already run for this session. */
138
+ get isDisposed(): boolean;
139
+ }
140
+ /**
141
+ * Build the robust resume argv (§5, R4.1): `bash -c 'claude --resume "<id>" || claude'`. The
142
+ * `|| claude` makes a FAILED `--resume` fall back to a fresh interactive session rather than
143
+ * hanging. The id is double-quoted so it survives shell word-splitting. There is deliberately
144
+ * NO `-p`/`--print`/`stream-json` — those select the SDK/credit non-interactive path.
145
+ */
146
+ export declare function buildResumeArgv(sessionId: string): [string, string];
147
+ /** Options for {@link spawnResumePty}. */
148
+ export interface SpawnResumeOptions {
149
+ /** The prior session id to reattach to (== the JSONL transcript basename). */
150
+ sessionId: string;
151
+ /** Host working directory the resumed TUI runs in. */
152
+ cwd: string;
153
+ /** Base environment to sanitize; defaults to the parent process env. */
154
+ baseEnv?: Record<string, string | undefined>;
155
+ /** Injectable spawn function (defaults to node-pty's `spawn`); tests pass a fake. */
156
+ spawn?: typeof pty.spawn;
157
+ }
158
+ /**
159
+ * Spawn the resume PTY, reusing story 013's sanitized env (R4.2) so the resumed turn keeps the
160
+ * `entrypoint='cli'` subscription posture. Same env-sanitize ({@link buildSanitizedEnv}),
161
+ * refuse-to-spawn taint guard ({@link assertSpawnEnvUntainted}), shell resolution
162
+ * ({@link resolveShell}), and §5 PTY geometry as the story 013 spawn path — ONLY the argv
163
+ * differs (robust resume vs the fresh `--session-id` launch). Returns the same
164
+ * {@link PtyEngineHandle} shape so a {@link SessionEngine} manages a resumed PTY identically.
165
+ */
166
+ export declare function spawnResumePty(opts: SpawnResumeOptions): PtyEngineHandle;
167
+ /** Options for {@link createSessionEngine}. */
168
+ export interface CreateSessionEngineOptions {
169
+ /** Host working directory the spawned TUI runs in (story 013 spawn). */
170
+ cwd: string;
171
+ /** Base environment to sanitize; defaults to the parent process env. */
172
+ baseEnv?: Record<string, string | undefined>;
173
+ /** Injectable spawn function (defaults to node-pty's `spawn`); tests pass a fake. */
174
+ spawn?: typeof pty.spawn;
175
+ /**
176
+ * Story 034 (§9 hybrid gate): per-session SCRATCH settings file carrying the fork's
177
+ * `PreToolUse` hook, forwarded to {@link spawnClaudePty} as `--settings "<file>"`. The
178
+ * caller MUST have written it (story 032 `injectHook`) BEFORE calling — claude reads
179
+ * settings only at startup (GATE_FINDINGS blocker c). Absent → ungated spawn (pre-034).
180
+ */
181
+ settingsFile?: string;
182
+ /**
183
+ * Factory that starts the per-session JSONL tail watcher (story 015) for the freshly spawned
184
+ * PTY. Invoked exactly once with the spawned session id and PTY. Returns the watcher handle the
185
+ * engine tears down on cleanup. The watcher's JSONL tail — NOT `p.onData` — is the single
186
+ * source of truth for session state (§2). Optional until story 015 lands.
187
+ */
188
+ startWatcher?: (sessionId: string, p: PtyEngineHandle["pty"]) => SessionWatcher;
189
+ /** The live session registry the engine registers into and deletes from on cleanup. */
190
+ sessions?: Map<string, SessionEngine>;
191
+ /** Resize debounce window in ms; defaults to {@link RESIZE_DEBOUNCE_MS}. */
192
+ resizeDebounceMs?: number;
193
+ /** Optional diagnostics hook invoked exactly once, when cleanup runs. */
194
+ onCleanup?: (info: CleanupInfo) => void;
195
+ /** Injectable timer fns (default global setTimeout/clearTimeout); fake-timer tests override. */
196
+ setTimeoutFn?: typeof setTimeout;
197
+ clearTimeoutFn?: typeof clearTimeout;
198
+ /**
199
+ * Story 035 — OPTIONAL cosmetic live ANSI mirror (§5). OFF by default. Forwarded to the
200
+ * {@link SessionEngine}; when absent/disabled NO `p.onData` tap is attached and the binding is
201
+ * byte-for-byte the read-only Degrau-1 path. The mirror is cosmetic only — it produces no
202
+ * `SessionUpdate` and never feeds the structural path (§2: state comes only from the JSONL tail).
203
+ */
204
+ ansiMirror?: AnsiMirrorOptions;
205
+ }
206
+ /**
207
+ * Bind ONE PTY and ONE JSONL watcher into a single per-session {@link SessionEngine}, hosted by
208
+ * THIS one Node process (§2 — no out-of-process supervisor, no `child_process.fork`). The PTY is
209
+ * spawned via story 013's sanitized path; the watcher is started by the injected story-015
210
+ * factory; the returned engine record owns both (PTY handle + watcher handle + resize timer +
211
+ * disposed flag).
212
+ *
213
+ * ── tail-as-source-of-truth fence (§2 ordering) ───────────────────────────────────────────────
214
+ * Session state derives SOLELY from the watcher's JSONL tail. The live ANSI mirror (story 035) is
215
+ * cosmetic and OFF by default: with no `ansiMirror` option, `p.onData` is NEVER subscribed and this
216
+ * binding is byte-for-byte the read-only Degrau-1 path. Even when enabled, the mirror is one-way
217
+ * (bytes → live-view sink) and MUST NOT mutate session state or drive turn transitions. Do not add
218
+ * an `onData`→state path in this binding: structure comes only from the JSONL tail.
219
+ * ──────────────────────────────────────────────────────────────────────────────────────────────
220
+ */
221
+ export declare function createSessionEngine(opts: CreateSessionEngineOptions): SessionEngine;
222
+ //# sourceMappingURL=engine-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-lifecycle.d.ts","sourceRoot":"","sources":["../src/engine-lifecycle.ts"],"names":[],"mappings":"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"}