pi-crew 0.9.67 → 0.10.1

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 (222) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/agents/analyst.md +1 -1
  3. package/agents/cold-verifier.md +3 -1
  4. package/agents/critic.md +1 -1
  5. package/agents/executor.md +1 -1
  6. package/agents/explorer.md +1 -1
  7. package/agents/planner.md +1 -1
  8. package/agents/reviewer.md +1 -1
  9. package/agents/security-reviewer.md +1 -1
  10. package/agents/test-engineer.md +1 -1
  11. package/agents/verifier.md +1 -1
  12. package/agents/writer.md +1 -1
  13. package/dist/index.mjs +65847 -60331
  14. package/docs/README.md +2 -0
  15. package/docs/actions-reference.md +31 -0
  16. package/docs/commands-reference.md +17 -6
  17. package/docs/resource-formats.md +13 -0
  18. package/package.json +9 -6
  19. package/schema.json +15 -1
  20. package/scripts/resource-sampler.mjs +36 -2
  21. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  22. package/skills/widget-rendering/SKILL.md +7 -7
  23. package/src/agents/agent-config.ts +36 -14
  24. package/src/agents/discover-agents.ts +23 -14
  25. package/src/config/config-merge.ts +183 -0
  26. package/src/config/config-validation.ts +681 -0
  27. package/src/config/config.ts +22 -864
  28. package/src/config/defaults.ts +35 -1
  29. package/src/config/drift-detector.ts +1 -1
  30. package/src/config/env-vars.ts +691 -0
  31. package/src/config/role-tools.ts +8 -8
  32. package/src/config/sanitize-project-config.ts +172 -0
  33. package/src/config/types.ts +30 -0
  34. package/src/extension/async-notifier.ts +25 -2
  35. package/src/extension/crew-vibes/config.ts +2 -1
  36. package/src/extension/plan-orchestrate.ts +132 -0
  37. package/src/extension/registration/commands/dashboard.ts +158 -0
  38. package/src/extension/registration/commands/index.ts +35 -0
  39. package/src/extension/registration/commands/manage.ts +303 -0
  40. package/src/extension/registration/commands/run.ts +214 -0
  41. package/src/extension/registration/commands/shared.ts +631 -0
  42. package/src/extension/registration/commands/status.ts +60 -0
  43. package/src/extension/registration/commands.ts +13 -1224
  44. package/src/extension/registration/lifecycle-handlers.ts +126 -15
  45. package/src/extension/registration/subagent-tools.ts +218 -9
  46. package/src/extension/rpc-hmac.ts +5 -3
  47. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  48. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  49. package/src/extension/team-tool/api/task-claims.ts +109 -40
  50. package/src/extension/team-tool/cancel.ts +84 -50
  51. package/src/extension/team-tool/dispatch/index.ts +1 -0
  52. package/src/extension/team-tool/dispatch/run.ts +4 -1
  53. package/src/extension/team-tool/doctor.ts +103 -1
  54. package/src/extension/team-tool/orchestrate.ts +66 -1
  55. package/src/extension/team-tool/plans.ts +192 -0
  56. package/src/extension/team-tool/respond.ts +197 -65
  57. package/src/extension/team-tool/run-deadline.ts +16 -1
  58. package/src/extension/team-tool/run-intent.ts +63 -0
  59. package/src/extension/team-tool/run.ts +48 -18
  60. package/src/extension/team-tool/status.ts +84 -26
  61. package/src/extension/team-tool.ts +11 -2
  62. package/src/hooks/registry.ts +1 -6
  63. package/src/i18n.ts +9 -0
  64. package/src/prompt/prompt-runtime.ts +521 -2
  65. package/src/prompt/worker-events-channel.ts +173 -0
  66. package/src/runtime/README.md +8 -8
  67. package/src/runtime/async-runner.ts +7 -3
  68. package/src/runtime/background-runner.ts +42 -14
  69. package/src/runtime/broker/broker-issuer.ts +9 -2
  70. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  71. package/src/runtime/broker/crew-broker.ts +838 -10
  72. package/src/runtime/broker/wait-status-cache.ts +157 -0
  73. package/src/runtime/budget-enforcement.ts +281 -0
  74. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  75. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  76. package/src/runtime/child-pi/child-pi.ts +97 -201
  77. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  78. package/src/runtime/crew-agent-records.ts +259 -14
  79. package/src/runtime/delegate-spawn.ts +148 -0
  80. package/src/runtime/deterministic-ast.ts +2 -1
  81. package/src/runtime/dispatch-batch.ts +945 -0
  82. package/src/runtime/finalize-run.ts +557 -0
  83. package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
  84. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
  85. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  86. package/src/runtime/group-join.ts +11 -125
  87. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  88. package/src/runtime/merge-gate.ts +7 -1
  89. package/src/runtime/merge-loop.ts +130 -0
  90. package/src/runtime/model/model-budget-summary.ts +53 -0
  91. package/src/runtime/model/model-fallback.ts +32 -2
  92. package/src/runtime/model/pi-args.ts +10 -0
  93. package/src/runtime/model/provider-extensions.ts +10 -0
  94. package/src/runtime/orphan-worker-registry.ts +1 -1
  95. package/src/runtime/output/output-validator.ts +45 -0
  96. package/src/runtime/parent-guard.ts +3 -1
  97. package/src/runtime/peer-dep.ts +2 -1
  98. package/src/runtime/per-write-validator.ts +0 -5
  99. package/src/runtime/pi-spawn.ts +61 -15
  100. package/src/runtime/plan-approval.ts +125 -0
  101. package/src/runtime/plan-replan.ts +151 -0
  102. package/src/runtime/recovery/checkpoint.ts +0 -18
  103. package/src/runtime/recovery/crash-recovery.ts +95 -46
  104. package/src/runtime/scheduler-context.ts +98 -0
  105. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  106. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  107. package/src/runtime/scheduling/nested-slots.ts +70 -0
  108. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  109. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  110. package/src/runtime/scratchpad/README.md +10 -5
  111. package/src/runtime/scratchpad/guest.ts +103 -2
  112. package/src/runtime/scratchpad/protocol.ts +13 -1
  113. package/src/runtime/scratchpad/transform.ts +206 -12
  114. package/src/runtime/settings-store.ts +219 -0
  115. package/src/runtime/spawn-policy.ts +217 -0
  116. package/src/runtime/stale-reconciler.ts +87 -6
  117. package/src/runtime/subagent-manager.ts +25 -1
  118. package/src/runtime/task-output-context.ts +230 -9
  119. package/src/runtime/task-packet.ts +23 -1
  120. package/src/runtime/task-runner/child-executor.ts +106 -7
  121. package/src/runtime/task-runner/post-execution.ts +125 -1
  122. package/src/runtime/task-runner/pre-execution.ts +39 -1
  123. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  124. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  125. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  126. package/src/runtime/task-runner/state-helpers.ts +26 -24
  127. package/src/runtime/task-runner.ts +11 -0
  128. package/src/runtime/team-runner.ts +129 -1671
  129. package/src/runtime/verification/spec-sandbox.ts +255 -0
  130. package/src/runtime/verification/verification-gates.ts +3 -2
  131. package/src/runtime/verification/verification-worktree.ts +2 -1
  132. package/src/runtime/workflow-phase-advance.ts +100 -0
  133. package/src/runtime/workspace-tree.ts +9 -0
  134. package/src/schema/config-schema.ts +65 -25
  135. package/src/schema/sensitive-config-paths.ts +64 -0
  136. package/src/schema/team-tool-schema.ts +13 -3
  137. package/src/state/README.md +4 -10
  138. package/src/state/atomic-write.ts +20 -3
  139. package/src/state/contracts.ts +38 -0
  140. package/src/state/coordination/mailbox.ts +12 -2
  141. package/src/state/event-log/cursor.ts +223 -0
  142. package/src/state/event-log/event-log-rotation.ts +12 -4
  143. package/src/state/event-log/event-log.ts +152 -369
  144. package/src/state/event-log/sequence-cache.ts +373 -0
  145. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  146. package/src/state/stores/active-run-registry.ts +3 -2
  147. package/src/state/stores/manifest-io.ts +237 -0
  148. package/src/state/stores/ownership-map.ts +162 -0
  149. package/src/state/stores/plan-store.ts +241 -0
  150. package/src/state/stores/run-cache.ts +0 -90
  151. package/src/state/stores/spec-store.ts +189 -0
  152. package/src/state/stores/state-store.ts +66 -229
  153. package/src/state/types.ts +197 -0
  154. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  155. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  156. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  157. package/src/ui/heartbeat-aggregator.ts +9 -1
  158. package/src/ui/keybinding-map.ts +54 -13
  159. package/src/ui/powerbar-publisher.ts +52 -1
  160. package/src/ui/run-dashboard.ts +31 -5
  161. package/src/ui/run-snapshot-cache.ts +57 -30
  162. package/src/ui/snapshot-types.ts +6 -1
  163. package/src/ui/widget/widget-renderer.ts +9 -1
  164. package/src/utils/file-coalescer.ts +0 -4
  165. package/src/utils/fs-errno.ts +66 -0
  166. package/src/utils/fs-watch.ts +1 -1
  167. package/src/utils/internal-error.ts +3 -1
  168. package/src/utils/paths.ts +11 -3
  169. package/src/utils/task-name-generator.ts +1 -8
  170. package/src/workflows/discover-workflows.ts +19 -2
  171. package/src/workflows/validate-workflow.ts +7 -1
  172. package/src/workflows/workflow-config.ts +10 -0
  173. package/src/workflows/workflow-serializer.ts +3 -0
  174. package/src/worktree/worktree-manager.ts +22 -0
  175. package/src/agents/agent-search.ts +0 -98
  176. package/src/benchmark/benchmark-runner.ts +0 -313
  177. package/src/benchmark/feedback-loop.ts +0 -73
  178. package/src/config/resilient-parser.ts +0 -117
  179. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  180. package/src/extension/result-watcher.ts +0 -139
  181. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  182. package/src/observability/metric-retention.ts +0 -64
  183. package/src/runtime/compaction/compaction-summary.ts +0 -278
  184. package/src/runtime/errors/crew-errors.ts +0 -162
  185. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  186. package/src/runtime/loop-gates.ts +0 -128
  187. package/src/runtime/metric-parser.ts +0 -36
  188. package/src/runtime/output/stream-preview.ts +0 -184
  189. package/src/runtime/output/tool-progress.ts +0 -278
  190. package/src/runtime/phase-tracker.ts +0 -385
  191. package/src/runtime/pipeline-runner.ts +0 -523
  192. package/src/runtime/process/process-lifecycle.ts +0 -491
  193. package/src/runtime/recovery/retry-runner.ts +0 -330
  194. package/src/runtime/run-drift.ts +0 -219
  195. package/src/runtime/scratchpad/snapshot-hmac.ts +0 -167
  196. package/src/runtime/task-quality.ts +0 -199
  197. package/src/runtime/task-runner/run-projection.ts +0 -128
  198. package/src/runtime/verification/post-checks.ts +0 -142
  199. package/src/state/coordination/schedule.ts +0 -166
  200. package/src/state/event-log/jsonl-writer.ts +0 -115
  201. package/src/state/hook-instinct-bridge.ts +0 -94
  202. package/src/state/hook-integrations.ts +0 -51
  203. package/src/state/session-state-map.ts +0 -51
  204. package/src/state/stores/blob-store.ts +0 -308
  205. package/src/state/stores/instinct-store.ts +0 -275
  206. package/src/state/stores/observation-store.ts +0 -176
  207. package/src/state/tiered-eval.ts +0 -480
  208. package/src/state/types-eval.ts +0 -58
  209. package/src/tools/safe-bash-extension.ts +0 -54
  210. package/src/tools/safe-bash.ts +0 -505
  211. package/src/ui/agent-management-overlay.ts +0 -160
  212. package/src/ui/crew-footer.ts +0 -102
  213. package/src/ui/crew-select-list.ts +0 -114
  214. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  215. package/src/ui/transcript-entries.ts +0 -256
  216. package/src/utils/conflict-detect.ts +0 -721
  217. package/src/utils/fingerprint.ts +0 -180
  218. package/src/utils/gh-protocol.ts +0 -556
  219. package/src/utils/project-detector.ts +0 -160
  220. package/src/utils/sse-parser.ts +0 -131
  221. package/src/workflows/cost-estimator.ts +0 -34
  222. package/src/workflows/intermediate-store.ts +0 -166
@@ -0,0 +1,324 @@
1
+ /**
2
+ * child-pi-timers.ts — Timer constructs for the runChildPi Promise body.
3
+ *
4
+ * Extracted from child-pi.ts (Phase 2.3 of the maintainability refactor).
5
+ * Pure code motion — zero behavior change. The six timer constructs that were
6
+ * local to the Promise constructor inside runChildPi now live here, owned by a
7
+ * single factory instance:
8
+ *
9
+ * 1. noResponseTimer — unresponsive-worker guard (restartNoResponseTimer)
10
+ * 2. finalDrainTimer — 5 s drain ceiling after the final assistant event
11
+ * 3. hardKillTimer — SIGTERM → SIGKILL escalation after final drain
12
+ * 4. safetyTimer — #3 hardening: bounded settle if the child is immortal
13
+ * 5. cancelHardKill — 200 ms fast-escalate on explicit cancel (R6-F1)
14
+ * 6. pollHandle — F12 quiet-window interval (early drain on stdout silence)
15
+ *
16
+ * The callbacks close over many runChildPi locals (child, settled, childExited,
17
+ * stderrTail, stdoutTail, hardKilled, forcedFinalDrain, responseTimeoutHit,
18
+ * lastStdoutActivityMonotonicMs, finalDrainFiredMonotonicMs, input callbacks,
19
+ * logInternalError, killProcessTree, settle, ...). Those are threaded through
20
+ * `deps` (values) + `state` (getters/setters over the runChildPi-local flags)
21
+ * so behavior is preserved byte-for-byte.
22
+ *
23
+ * R6-F1 (refactor-plan.review.md §ROUND 6): clearAll() MUST cover all six
24
+ * constructs — previously cancelHardKill was a local const inside abort() and
25
+ * was NOT cleared by clearChildPiTimeouts(), leaking a 200 ms unref'd timer per
26
+ * cancel on short-lived children.
27
+ */
28
+ import type { ChildProcess } from "node:child_process";
29
+ import { DEFAULT_CHILD_PI } from "../../config/defaults.ts";
30
+ import { logInternalError } from "../../utils/internal-error.ts";
31
+ import type { BoundedTail } from "../compaction/compact-stages/bounded-tail.ts";
32
+ import type { ChildPiRunInput, ChildPiRunResult } from "./child-pi.ts";
33
+ import { HARD_KILL_MS } from "./child-pi-constants.ts";
34
+ import { killProcessTree } from "./child-pi-kill.ts";
35
+
36
+ /** Mutable run state shared with runChildPi via getters/setters. */
37
+ export interface ChildPiTimersState {
38
+ getSettled(): boolean;
39
+ getChildExited(): boolean;
40
+ setResponseTimeoutHit(value: boolean): void;
41
+ getHardKilled(): boolean;
42
+ setHardKilled(value: boolean): void;
43
+ setForcedFinalDrain(value: boolean): void;
44
+ getLastStdoutActivityMonotonicMs(): number;
45
+ setFinalDrainFiredMonotonicMs(value: number): void;
46
+ getAbortRequested(): boolean;
47
+ }
48
+
49
+ export interface ChildPiTimersDeps {
50
+ child: ChildProcess;
51
+ /** Raw run input — only the timer-relevant fields are read. */
52
+ input: Pick<ChildPiRunInput, "onLifecycleEvent" | "finalDrainMs" | "finalDrainQuietMs">;
53
+ /** Computed timings (identical formulas to runChildPi). */
54
+ responseTimeoutMs: number;
55
+ finalDrainMs: number;
56
+ hardKillMs: number;
57
+ /** Bounded accumulators (P0-1) — snapshotted inside timer callbacks. */
58
+ stdoutTail: BoundedTail;
59
+ stderrTail: BoundedTail;
60
+ /** Cleanup-error accumulator shared with settle(). */
61
+ cleanupErrors: string[];
62
+ /** settle() — the run finalizer, also called from the safety path.
63
+ * Provided as a getter because `settle` is declared later in the runChildPi
64
+ * Promise body than this factory is constructed; the getter is only
65
+ * invoked from the safety timer (≥ HARD_KILL_MS + 2 s after a response
66
+ * timeout), by which point the binding is initialized. */
67
+ getSettle: () => (result: ChildPiRunResult) => Promise<void>;
68
+ /** SEC-1 redaction helper (defined in child-pi.ts, passed in to avoid a
69
+ * runtime import cycle). */
70
+ redactStderrExcerpt: (stderr: string, maxChars: number) => string;
71
+ state: ChildPiTimersState;
72
+ }
73
+
74
+ export interface ChildPiTimers {
75
+ restartNoResponseTimer(): void;
76
+ clearNoResponseTimer(): void;
77
+ /** F12: arm the final-drain ceiling timer plus (optionally) the quiet-window
78
+ * poller that fires the drain early when stdout goes quiet. */
79
+ armFinalDrain(): void;
80
+ /** True when the 5 s drain ceiling timer is currently armed. */
81
+ hasFinalDrainTimer(): boolean;
82
+ /** 3.5 — arm the 200 ms fast-escalate SIGKILL for explicit cancel. */
83
+ armCancelHardKill(): void;
84
+ clearFinalDrainTimers(): void;
85
+ /** Clear ALL six timer constructs (R6-F1: incl. cancelHardKill). */
86
+ clearAll(): void;
87
+ }
88
+
89
+ export function createChildPiTimers(deps: ChildPiTimersDeps): ChildPiTimers {
90
+ let finalDrainTimer: NodeJS.Timeout | undefined;
91
+ let hardKillTimer: NodeJS.Timeout | undefined;
92
+ let noResponseTimer: NodeJS.Timeout | undefined;
93
+ let safetyTimer: NodeJS.Timeout | undefined;
94
+ let cancelHardKillTimer: NodeJS.Timeout | undefined;
95
+ let pollHandle: NodeJS.Timeout | undefined;
96
+
97
+ const restartNoResponseTimer = (): void => {
98
+ if (deps.responseTimeoutMs <= 0) return;
99
+ if (noResponseTimer) clearTimeout(noResponseTimer);
100
+ noResponseTimer = setTimeout(() => {
101
+ deps.state.setResponseTimeoutHit(true);
102
+ // P0-1: snapshot the bounded stderr accumulator once for this timer fire.
103
+ const stderr = deps.stderrTail.value();
104
+ // Capture stderr at timeout moment for debugging
105
+ // SEC-1: redact secrets before embedding in lifecycle event so
106
+ // worker-emitted secrets (API keys etc.) don't bypass redaction.
107
+ const timeoutStderr = deps.redactStderrExcerpt(stderr, 1024); // Last 1KB of stderr (redacted, SEC-1)
108
+ deps.input.onLifecycleEvent?.({
109
+ type: "response_timeout",
110
+ pid: deps.child.pid,
111
+ error: `No output for ${deps.responseTimeoutMs}ms`,
112
+ ts: new Date().toISOString(),
113
+ stderr: timeoutStderr || undefined,
114
+ });
115
+ killProcessTree(deps.child.pid, deps.child);
116
+ try {
117
+ deps.child.kill(process.platform === "win32" ? undefined : "SIGTERM");
118
+ } catch (error) {
119
+ logInternalError("child-pi.response-timeout-term", error, `pid=${deps.child.pid}`);
120
+ }
121
+ // #3 hardening: if the child never exits (zombie) and neither the
122
+ // 'exit' nor 'close' event ever fires, the promise would hang forever.
123
+ // SIGKILL fires ~3s after SIGTERM via hardKillTimer in killProcessPid,
124
+ // but on platforms where SIGKILL also fails (e.g. permission issues),
125
+ // add a bounded safety settle so the promise always resolves. Using
126
+ // hardKillMs + 2s as the safety window: enough for SIGKILL to work
127
+ // normally, but forces settle if the process is truly immortal.
128
+ // NOTE: we do NOT clear hardKillTimer here (that would defeat its purpose);
129
+ // we intentionally add a parallel safety path.
130
+ const SAFETY_SETTLE_MS = HARD_KILL_MS + 2000;
131
+ safetyTimer = setTimeout(() => {
132
+ if (deps.state.getSettled() || deps.state.getChildExited()) return;
133
+ logInternalError(
134
+ "child-pi.settle-safety-fired",
135
+ new Error(`Child did not exit within ${SAFETY_SETTLE_MS}ms of kill; forcing settle`),
136
+ `pid=${deps.child.pid}, responseTimeoutMs=${deps.responseTimeoutMs}`,
137
+ );
138
+ // Verify the child is still alive before forcing settle.
139
+ // If it somehow exited between childExited=false and here, the
140
+ // settled/childExited guard prevents double-settle (harmless but noisy).
141
+ try {
142
+ process.kill(deps.child.pid!, 0);
143
+ // Child still alive — force settle with timeout error.
144
+ const timeoutErr = `Child Pi produced no new output for ${deps.responseTimeoutMs}ms; killed but did not exit within ${SAFETY_SETTLE_MS}ms (possible zombie).`;
145
+ void deps.getSettle()({
146
+ exitCode: null,
147
+ stdout: deps.stdoutTail.value(),
148
+ stderr: deps.stderrTail.value(),
149
+ error: timeoutErr,
150
+ exitStatus: {
151
+ exitCode: null,
152
+ cancelled: deps.state.getAbortRequested(),
153
+ timedOut: true,
154
+ killed: deps.state.getHardKilled(),
155
+ cleanupErrors: deps.cleanupErrors,
156
+ finalDrainMs: deps.finalDrainMs,
157
+ crashClass: "timeout",
158
+ },
159
+ });
160
+ } catch {
161
+ // ESRCH / EPERM — child is already gone. The 'exit'/'close' handler
162
+ // will fire shortly (or already fired in a race). Let it settle normally.
163
+ }
164
+ }, SAFETY_SETTLE_MS);
165
+ safetyTimer.unref();
166
+ }, deps.responseTimeoutMs);
167
+ noResponseTimer.unref();
168
+ };
169
+ const clearNoResponseTimer = (): void => {
170
+ if (noResponseTimer) clearTimeout(noResponseTimer);
171
+ noResponseTimer = undefined;
172
+ };
173
+ const clearFinalDrainTimers = (): void => {
174
+ if (finalDrainTimer) clearTimeout(finalDrainTimer);
175
+ if (hardKillTimer) clearTimeout(hardKillTimer);
176
+ finalDrainTimer = undefined;
177
+ hardKillTimer = undefined;
178
+ };
179
+
180
+ return {
181
+ restartNoResponseTimer,
182
+ clearNoResponseTimer,
183
+ clearFinalDrainTimers,
184
+ armFinalDrain(): void {
185
+ // F12: alongside the 5 s ceiling timer, start a polling watcher
186
+ // that fires the drain early if stdout goes quiet for `quietMs`
187
+ // after the final assistant event. Heavy children that emit a
188
+ // stopReason=stop message_end and then sit idle will exit in
189
+ // ~quietMs (default 800 ms) instead of up to up to 5 s. unref() so
190
+ // the poller never holds the event loop on shutdown.
191
+ // NOTE: The polling watcher is NOT explicitly cleared on process exit.
192
+ // This is safe because: (1) it's unref()'d, so it won't prevent exit;
193
+ // (2) the `settled || childExited` guard at the top prevents firing
194
+ // after the child has exited; (3) sending SIGTERM to an already-
195
+ // exiting process is harmless. The `finalDrainQuietMs` config allows
196
+ // disabling this behavior (set >= finalDrainMs, e.g., 10000).
197
+ const quietMs = deps.input.finalDrainQuietMs ?? DEFAULT_CHILD_PI.finalDrainQuietMs;
198
+ if (quietMs < (deps.input.finalDrainMs ?? DEFAULT_CHILD_PI.finalDrainMs)) {
199
+ pollHandle = setInterval(() => {
200
+ if (deps.state.getSettled() || deps.state.getChildExited()) {
201
+ if (pollHandle) {
202
+ clearInterval(pollHandle);
203
+ pollHandle.unref();
204
+ }
205
+ pollHandle = undefined;
206
+ return;
207
+ }
208
+ const sinceLast = performance.now() - deps.state.getLastStdoutActivityMonotonicMs();
209
+ if (sinceLast >= quietMs) {
210
+ if (pollHandle) {
211
+ clearInterval(pollHandle);
212
+ pollHandle.unref();
213
+ }
214
+ pollHandle = undefined;
215
+ // Trigger the same drain path as the 5 s timer:
216
+ // mark forced, fire final_drain lifecycle, SIGTERM.
217
+ deps.state.setForcedFinalDrain(true);
218
+ deps.state.setFinalDrainFiredMonotonicMs(performance.now());
219
+ deps.input.onLifecycleEvent?.({
220
+ type: "final_drain",
221
+ pid: deps.child.pid,
222
+ ts: new Date().toISOString(),
223
+ reason: "stdout-quiet",
224
+ });
225
+ try {
226
+ deps.child.kill(process.platform === "win32" ? undefined : "SIGTERM");
227
+ } catch (error) {
228
+ logInternalError("child-pi.quiet-drain-term", error, `pid=${deps.child.pid}`);
229
+ }
230
+ // Mark for hard kill fallback so the existing timer is
231
+ // still reaped if it ever fires later.
232
+ hardKillTimer = setTimeout(() => {
233
+ if (deps.state.getSettled() || deps.state.getChildExited()) return;
234
+ try {
235
+ deps.state.setHardKilled(true);
236
+ deps.input.onLifecycleEvent?.({
237
+ type: "hard_kill",
238
+ pid: deps.child.pid,
239
+ ts: new Date().toISOString(),
240
+ });
241
+ deps.child.kill(process.platform === "win32" ? undefined : "SIGKILL");
242
+ } catch (error) {
243
+ logInternalError("child-pi.quiet-drain-hard-kill", error, `pid=${deps.child.pid}`);
244
+ }
245
+ }, deps.hardKillMs);
246
+ hardKillTimer.unref();
247
+ // Cancel the 5 s ceiling so we don't double-fire.
248
+ if (finalDrainTimer) {
249
+ clearTimeout(finalDrainTimer);
250
+ finalDrainTimer = undefined;
251
+ }
252
+ }
253
+ }, 200);
254
+ pollHandle.unref();
255
+ }
256
+ finalDrainTimer = setTimeout(() => {
257
+ if (deps.state.getSettled() || deps.state.getChildExited()) return;
258
+ deps.state.setForcedFinalDrain(true);
259
+ deps.state.setFinalDrainFiredMonotonicMs(performance.now()); // Phase-0 diagnostic: race timing.
260
+ deps.input.onLifecycleEvent?.({
261
+ type: "final_drain",
262
+ pid: deps.child.pid,
263
+ ts: new Date().toISOString(),
264
+ });
265
+ try {
266
+ deps.child.kill(process.platform === "win32" ? undefined : "SIGTERM");
267
+ } catch (error) {
268
+ logInternalError("child-pi.final-drain-term", error, `pid=${deps.child.pid}`);
269
+ }
270
+ hardKillTimer = setTimeout(() => {
271
+ if (deps.state.getSettled() || deps.state.getChildExited()) return;
272
+ try {
273
+ deps.state.setHardKilled(true);
274
+ deps.input.onLifecycleEvent?.({
275
+ type: "hard_kill",
276
+ pid: deps.child.pid,
277
+ ts: new Date().toISOString(),
278
+ });
279
+ deps.child.kill(process.platform === "win32" ? undefined : "SIGKILL");
280
+ } catch (error) {
281
+ logInternalError("child-pi.final-drain-kill", error, `pid=${deps.child.pid}`);
282
+ }
283
+ }, deps.hardKillMs);
284
+ hardKillTimer.unref();
285
+ }, deps.finalDrainMs);
286
+ finalDrainTimer.unref();
287
+ },
288
+ hasFinalDrainTimer(): boolean {
289
+ return finalDrainTimer !== undefined;
290
+ },
291
+ armCancelHardKill(): void {
292
+ // 3.5 — fast-escalate to SIGKILL within 200ms on explicit cancel
293
+ // so /team-cancel completes round-trip well under the operator
294
+ // expectation. The standard finalDrainMs / HARD_KILL_MS paths
295
+ // are for graceful drain, not user-initiated cancel.
296
+ cancelHardKillTimer = setTimeout(() => {
297
+ if (deps.state.getSettled() || deps.state.getChildExited()) return;
298
+ try {
299
+ deps.state.setHardKilled(true);
300
+ deps.child.kill(process.platform === "win32" ? undefined : "SIGKILL");
301
+ } catch (error) {
302
+ logInternalError("child-pi.cancel-fast-kill", error, `pid=${deps.child.pid}`);
303
+ }
304
+ }, 200);
305
+ cancelHardKillTimer.unref();
306
+ },
307
+ clearAll(): void {
308
+ // R6-F1: clear ALL six timer constructs. cancelHardKill was previously
309
+ // a local const inside abort() — unreachable from clearChildPiTimeouts —
310
+ // so repeated cancels on short-lived children accumulated 200 ms timers.
311
+ clearNoResponseTimer();
312
+ if (safetyTimer) clearTimeout(safetyTimer);
313
+ safetyTimer = undefined;
314
+ clearFinalDrainTimers();
315
+ if (cancelHardKillTimer) clearTimeout(cancelHardKillTimer);
316
+ cancelHardKillTimer = undefined;
317
+ if (pollHandle) {
318
+ clearInterval(pollHandle);
319
+ pollHandle.unref();
320
+ }
321
+ pollHandle = undefined;
322
+ },
323
+ };
324
+ }