pi-crew 0.9.68 → 0.10.2

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 (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. 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
+ }