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.
- package/CHANGELOG.md +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- 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
|
+
}
|