pi-crew 0.1.45 → 0.1.46
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/README.md +5 -5
- package/agents/analyst.md +1 -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/docs/next-upgrade-roadmap.md +733 -0
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-oh-my-pi-distillation.md +322 -0
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +198 -198
- package/docs/research-phase11-distillation.md +201 -201
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +107 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/install.mjs +52 -8
- package/package.json +1 -1
- package/schema.json +2 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +12 -11
- package/src/config/config.ts +48 -24
- package/src/config/defaults.ts +14 -0
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +11 -9
- package/src/extension/registration/commands.ts +32 -25
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +149 -148
- package/src/extension/registration/team-tool.ts +8 -6
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +13 -5
- package/src/extension/run-maintenance.ts +62 -43
- package/src/extension/team-tool/api.ts +25 -8
- package/src/extension/team-tool/cancel.ts +33 -4
- package/src/extension/team-tool/context.ts +5 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +91 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +37 -17
- package/src/extension/team-tool/run.ts +52 -10
- package/src/extension/team-tool/status.ts +12 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +32 -11
- package/src/i18n.ts +184 -184
- package/src/observability/event-to-metric.ts +8 -1
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +59 -53
- package/src/runtime/cancellation.ts +51 -0
- package/src/runtime/child-pi.ts +457 -444
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +1 -0
- package/src/runtime/crew-agent-records.ts +38 -6
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +46 -25
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +76 -0
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +88 -87
- package/src/runtime/live-agent-manager.ts +103 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +309 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +22 -6
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/subagent-manager.ts +3 -0
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +105 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +3 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +44 -5
- package/src/runtime/team-runner.ts +78 -15
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +3 -3
- package/src/state/active-run-registry.ts +165 -0
- package/src/state/contracts.ts +1 -1
- package/src/state/mailbox.ts +44 -4
- package/src/state/state-store.ts +8 -1
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +44 -2
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-config.ts +1 -0
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +4 -3
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +10 -2
- package/src/ui/snapshot-types.ts +2 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/utils/atomic-write.ts +33 -33
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +4 -2
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +1 -0
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
package/src/runtime/child-pi.ts
CHANGED
|
@@ -1,444 +1,457 @@
|
|
|
1
|
-
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (record
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (record.type === "
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (record.type
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
let
|
|
299
|
-
let
|
|
300
|
-
let
|
|
301
|
-
let
|
|
302
|
-
let
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
1
|
+
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
5
|
+
import type { WorkerExitStatus } from "../state/types.ts";
|
|
6
|
+
import { buildPiWorkerArgs, checkCrewDepth, cleanupTempDir } from "./pi-args.ts";
|
|
7
|
+
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
8
|
+
import { DEFAULT_CHILD_PI } from "../config/defaults.ts";
|
|
9
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
10
|
+
import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
|
|
11
|
+
import { redactJsonLine } from "../utils/redaction.ts";
|
|
12
|
+
|
|
13
|
+
const POST_EXIT_STDIO_GUARD_MS = DEFAULT_CHILD_PI.postExitStdioGuardMs;
|
|
14
|
+
const FINAL_DRAIN_MS = DEFAULT_CHILD_PI.finalDrainMs;
|
|
15
|
+
const HARD_KILL_MS = DEFAULT_CHILD_PI.hardKillMs;
|
|
16
|
+
const RESPONSE_TIMEOUT_MS = DEFAULT_CHILD_PI.responseTimeoutMs;
|
|
17
|
+
const MAX_CAPTURE_BYTES = DEFAULT_CHILD_PI.maxCaptureBytes;
|
|
18
|
+
const MAX_ASSISTANT_TEXT_CHARS = DEFAULT_CHILD_PI.maxAssistantTextChars;
|
|
19
|
+
const MAX_TOOL_RESULT_CHARS = DEFAULT_CHILD_PI.maxToolResultChars;
|
|
20
|
+
const MAX_TOOL_INPUT_CHARS = DEFAULT_CHILD_PI.maxToolInputChars;
|
|
21
|
+
const MAX_COMPACT_CONTENT_CHARS = DEFAULT_CHILD_PI.maxCompactContentChars;
|
|
22
|
+
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
23
|
+
const childHardKillTimers = new Map<number, NodeJS.Timeout>();
|
|
24
|
+
|
|
25
|
+
function appendBoundedTail(current: string, chunk: string, maxBytes = MAX_CAPTURE_BYTES): string {
|
|
26
|
+
const combined = current + chunk;
|
|
27
|
+
if (Buffer.byteLength(combined, "utf-8") <= maxBytes) return combined;
|
|
28
|
+
let tail = combined.slice(Math.max(0, combined.length - maxBytes));
|
|
29
|
+
while (Buffer.byteLength(tail, "utf-8") > maxBytes) tail = tail.slice(1024);
|
|
30
|
+
return `[pi-crew captured output truncated to last ${Math.round(maxBytes / 1024)} KiB]\n${tail}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clearHardKillTimer(pid: number | undefined): void {
|
|
34
|
+
if (!pid) return;
|
|
35
|
+
const timer = childHardKillTimers.get(pid);
|
|
36
|
+
if (!timer) return;
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
childHardKillTimers.delete(pid);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function killProcessTree(pid: number | undefined, child?: ChildProcess): void {
|
|
42
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) return;
|
|
43
|
+
if (child && child.exitCode !== null) return;
|
|
44
|
+
try {
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
process.kill(-pid, "SIGTERM");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logInternalError("child-pi.sigterm", error, `pid=${pid}`);
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, "SIGTERM");
|
|
55
|
+
} catch (fallbackError) {
|
|
56
|
+
logInternalError("child-pi.sigterm-absolute", fallbackError, `pid=${pid}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
clearHardKillTimer(pid);
|
|
60
|
+
const hardKillTimer = setTimeout(() => {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(-pid, "SIGKILL");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logInternalError("child-pi.sigkill", error, `pid=${pid}`);
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, "SIGKILL");
|
|
67
|
+
} catch (fallbackError) {
|
|
68
|
+
logInternalError("child-pi.sigkill-absolute", fallbackError, `pid=${pid}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
childHardKillTimers.delete(pid);
|
|
72
|
+
}, HARD_KILL_MS);
|
|
73
|
+
hardKillTimer.unref();
|
|
74
|
+
child?.once("exit", () => clearHardKillTimer(pid));
|
|
75
|
+
childHardKillTimers.set(pid, hardKillTimer);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logInternalError("child-pi.kill-process-tree", error, `pid=${pid}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function terminateActiveChildPiProcesses(): number {
|
|
82
|
+
const entries = [...activeChildProcesses.entries()];
|
|
83
|
+
for (const [pid, child] of entries) killProcessTree(pid, child);
|
|
84
|
+
return entries.length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ChildPiRunInput {
|
|
88
|
+
cwd: string;
|
|
89
|
+
task: string;
|
|
90
|
+
agent: AgentConfig;
|
|
91
|
+
model?: string;
|
|
92
|
+
skillPaths?: string[];
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
transcriptPath?: string;
|
|
95
|
+
onStdoutLine?: (line: string) => void;
|
|
96
|
+
onJsonEvent?: (event: unknown) => void;
|
|
97
|
+
onSpawn?: (pid: number) => void;
|
|
98
|
+
maxDepth?: number;
|
|
99
|
+
finalDrainMs?: number;
|
|
100
|
+
hardKillMs?: number;
|
|
101
|
+
responseTimeoutMs?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ChildPiRunResult {
|
|
105
|
+
exitCode: number | null;
|
|
106
|
+
stdout: string;
|
|
107
|
+
stderr: string;
|
|
108
|
+
error?: string;
|
|
109
|
+
exitStatus?: WorkerExitStatus;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): SpawnOptions {
|
|
113
|
+
return {
|
|
114
|
+
cwd,
|
|
115
|
+
env,
|
|
116
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
117
|
+
detached: process.platform !== "win32",
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
123
|
+
if (!input.transcriptPath) return;
|
|
124
|
+
fs.mkdirSync(path.dirname(input.transcriptPath), { recursive: true });
|
|
125
|
+
fs.appendFileSync(input.transcriptPath, `${redactJsonLine(line)}\n`, "utf-8");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function compactString(value: string, maxChars = MAX_COMPACT_CONTENT_CHARS): string {
|
|
129
|
+
if (value.length <= maxChars) return value;
|
|
130
|
+
return `${value.slice(0, maxChars)}\n[pi-crew compacted ${value.length - maxChars} chars]`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function compactValue(value: unknown): unknown {
|
|
134
|
+
if (typeof value === "string") return compactString(value);
|
|
135
|
+
if (Array.isArray(value)) return value.slice(0, 20).map(compactValue);
|
|
136
|
+
const record = asRecord(value);
|
|
137
|
+
if (!record) return value;
|
|
138
|
+
const compacted: Record<string, unknown> = {};
|
|
139
|
+
for (const [key, entry] of Object.entries(record).slice(0, 20)) compacted[key] = compactValue(entry);
|
|
140
|
+
return compacted;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compactContentPart(part: unknown): unknown | undefined {
|
|
144
|
+
const record = asRecord(part);
|
|
145
|
+
if (!record) return undefined;
|
|
146
|
+
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? compactString(record.text, MAX_ASSISTANT_TEXT_CHARS) : "" };
|
|
147
|
+
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: compactValue(typeof record.input === "string" ? compactString(record.input, MAX_TOOL_INPUT_CHARS) : record.input) };
|
|
148
|
+
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: compactValue(typeof record.content === "string" ? compactString(record.content, MAX_TOOL_RESULT_CHARS) : record.content) };
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function compactChildPiEvent(event: unknown): unknown | undefined {
|
|
153
|
+
const record = asRecord(event);
|
|
154
|
+
if (!record) return undefined;
|
|
155
|
+
if (record.type === "message_update") return undefined;
|
|
156
|
+
if (record.type === "tool_execution_start" || record.type === "tool_execution_end") {
|
|
157
|
+
return { type: record.type, toolName: record.toolName, args: record.args };
|
|
158
|
+
}
|
|
159
|
+
if (record.type === "tool_result_end" || record.type === "message_end" || record.type === "message") {
|
|
160
|
+
const message = asRecord(record.message);
|
|
161
|
+
if (message?.role === "user" || message?.role === "system") return undefined;
|
|
162
|
+
const content = Array.isArray(message?.content) ? message.content.map(compactContentPart).filter((part) => part !== undefined) : undefined;
|
|
163
|
+
return {
|
|
164
|
+
type: record.type,
|
|
165
|
+
...(typeof record.text === "string" ? { text: record.text } : {}),
|
|
166
|
+
...(message ? { message: { role: message.role, ...(content ? { content } : {}), usage: message.usage, model: message.model, errorMessage: message.errorMessage, stopReason: message.stopReason } } : {}),
|
|
167
|
+
usage: record.usage,
|
|
168
|
+
model: record.model,
|
|
169
|
+
provider: record.provider,
|
|
170
|
+
stopReason: record.stopReason,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return record.type ? { type: record.type } : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function displayTextFromCompactEvent(event: unknown): string | undefined {
|
|
177
|
+
const record = asRecord(event);
|
|
178
|
+
if (!record) return undefined;
|
|
179
|
+
if (record.type === "tool_execution_start") {
|
|
180
|
+
return typeof record.toolName === "string" ? `tool: ${record.toolName}` : "tool started";
|
|
181
|
+
}
|
|
182
|
+
if (record.type !== "message" && record.type !== "message_end") return undefined;
|
|
183
|
+
const message = asRecord(record.message);
|
|
184
|
+
if (message?.role !== undefined && message.role !== "assistant") return undefined;
|
|
185
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
186
|
+
const text = content.flatMap((part) => {
|
|
187
|
+
const item = asRecord(part);
|
|
188
|
+
return item?.type === "text" && typeof item.text === "string" ? [item.text] : [];
|
|
189
|
+
}).join("\n").trim();
|
|
190
|
+
return text || (typeof record.text === "string" ? record.text : undefined);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function compactChildPiLine(line: string): { persistedLine: string; event?: unknown; displayLine?: string; json: boolean } {
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(line);
|
|
196
|
+
const compact = compactChildPiEvent(parsed);
|
|
197
|
+
return { json: true, event: compact, persistedLine: compact ? JSON.stringify(compact) : "", displayLine: displayTextFromCompactEvent(compact) };
|
|
198
|
+
} catch {
|
|
199
|
+
return { json: false, persistedLine: line, displayLine: line };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export class ChildPiLineObserver {
|
|
204
|
+
private buffer = "";
|
|
205
|
+
private readonly input: ChildPiRunInput;
|
|
206
|
+
|
|
207
|
+
constructor(input: ChildPiRunInput) {
|
|
208
|
+
this.input = input;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
observe(text: string): void {
|
|
212
|
+
this.buffer += text;
|
|
213
|
+
const lines = this.buffer.split(/\r?\n/);
|
|
214
|
+
this.buffer = lines.pop() ?? "";
|
|
215
|
+
for (const line of lines) this.emitLine(line);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
flush(): void {
|
|
219
|
+
if (!this.buffer) return;
|
|
220
|
+
const line = this.buffer;
|
|
221
|
+
this.buffer = "";
|
|
222
|
+
this.emitLine(line);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private emitLine(line: string): void {
|
|
226
|
+
if (!line.trim()) return;
|
|
227
|
+
const compact = compactChildPiLine(line);
|
|
228
|
+
if (compact.event !== undefined) {
|
|
229
|
+
try {
|
|
230
|
+
this.input.onJsonEvent?.(compact.event);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
logInternalError("child-pi.on-json-event", error, `line=${compact.persistedLine ?? compact.displayLine ?? ""}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (compact.persistedLine) appendTranscript(this.input, compact.persistedLine);
|
|
236
|
+
if (compact.displayLine?.trim()) {
|
|
237
|
+
try {
|
|
238
|
+
this.input.onStdoutLine?.(compact.displayLine);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
logInternalError("child-pi.on-stdout-line", error, `line=${compact.displayLine}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function observeStdoutChunk(input: ChildPiRunInput, text: string): void {
|
|
247
|
+
const observer = new ChildPiLineObserver(input);
|
|
248
|
+
observer.observe(text);
|
|
249
|
+
observer.flush();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
253
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isFinalAssistantEvent(event: unknown): boolean {
|
|
257
|
+
const obj = asRecord(event);
|
|
258
|
+
if (!obj || obj.type !== "message_end") return false;
|
|
259
|
+
const message = asRecord(obj.message);
|
|
260
|
+
const role = message?.role;
|
|
261
|
+
if (role !== undefined && role !== "assistant") return false;
|
|
262
|
+
const stopReason = typeof message?.stopReason === "string" ? message.stopReason : typeof obj.stopReason === "string" ? obj.stopReason : undefined;
|
|
263
|
+
if (stopReason !== undefined && stopReason !== "stop") return false;
|
|
264
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
265
|
+
return !content.some((part) => asRecord(part)?.type === "toolCall");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResult> {
|
|
269
|
+
const depth = checkCrewDepth(input.maxDepth);
|
|
270
|
+
if (depth.blocked) return { exitCode: 1, stdout: "", stderr: `pi-crew depth guard blocked child worker: depth ${depth.depth} >= max ${depth.maxDepth}` };
|
|
271
|
+
const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
272
|
+
if (mock) {
|
|
273
|
+
if (mock === "success") {
|
|
274
|
+
const stdout = `Mock child Pi success for ${input.agent.name}\n`;
|
|
275
|
+
observeStdoutChunk(input, stdout);
|
|
276
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
277
|
+
}
|
|
278
|
+
if (mock === "json-success" || mock === "adaptive-plan") {
|
|
279
|
+
const text = mock === "adaptive-plan" && input.task.includes("ADAPTIVE_PLAN_JSON_START")
|
|
280
|
+
? `Adaptive mock plan\nADAPTIVE_PLAN_JSON_START\n${JSON.stringify({ phases: [{ name: "research", tasks: [{ role: "explorer", task: "Explore adaptive target" }, { role: "analyst", task: "Analyze adaptive target" }, { role: "planner", task: "Plan adaptive target" }] }, { name: "build", tasks: [{ role: "executor", task: "Implement adaptive target" }] }, { name: "check", tasks: [{ role: "reviewer", task: "Review adaptive target" }, { role: "test-engineer", task: "Test adaptive target" }, { role: "writer", task: "Summarize adaptive target" }] }] })}\nADAPTIVE_PLAN_JSON_END`
|
|
281
|
+
: `Mock JSON success for ${input.agent.name}`;
|
|
282
|
+
const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
|
|
283
|
+
observeStdoutChunk(input, stdout);
|
|
284
|
+
return { exitCode: 0, stdout, stderr: "" };
|
|
285
|
+
}
|
|
286
|
+
if (mock === "retryable-failure") return { exitCode: 1, stdout: "", stderr: "rate limit: mock failure" };
|
|
287
|
+
return { exitCode: 1, stdout: "", stderr: `mock failure: ${mock}` };
|
|
288
|
+
}
|
|
289
|
+
const built = buildPiWorkerArgs({ task: input.task, agent: input.agent, model: input.model, sessionEnabled: false, maxDepth: input.maxDepth, skillPaths: input.skillPaths });
|
|
290
|
+
const spawnSpec = getPiSpawnCommand(built.args);
|
|
291
|
+
try {
|
|
292
|
+
return await new Promise<ChildPiRunResult>((resolve) => {
|
|
293
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, buildChildPiSpawnOptions(input.cwd, { ...process.env, ...built.env }));
|
|
294
|
+
if (child.pid) {
|
|
295
|
+
activeChildProcesses.set(child.pid, child);
|
|
296
|
+
input.onSpawn?.(child.pid);
|
|
297
|
+
}
|
|
298
|
+
let stdout = "";
|
|
299
|
+
let stderr = "";
|
|
300
|
+
let settled = false;
|
|
301
|
+
let childExited = false;
|
|
302
|
+
let postExitGuardCleanup: (() => void) | undefined;
|
|
303
|
+
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
304
|
+
let hardKillTimer: NodeJS.Timeout | undefined;
|
|
305
|
+
let noResponseTimer: NodeJS.Timeout | undefined;
|
|
306
|
+
const finalDrainMs = input.finalDrainMs ?? FINAL_DRAIN_MS;
|
|
307
|
+
const hardKillMs = input.hardKillMs ?? HARD_KILL_MS;
|
|
308
|
+
const responseTimeoutEnv = Number.parseInt(process.env.PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS ?? "", 10);
|
|
309
|
+
const responseTimeoutMs = Number.isFinite(responseTimeoutEnv) && responseTimeoutEnv >= 0 ? responseTimeoutEnv : input.responseTimeoutMs ?? RESPONSE_TIMEOUT_MS;
|
|
310
|
+
let responseTimeoutHit = false;
|
|
311
|
+
let forcedFinalDrain = false;
|
|
312
|
+
let abortRequested = input.signal?.aborted === true;
|
|
313
|
+
let hardKilled = false;
|
|
314
|
+
const cleanupErrors: string[] = [];
|
|
315
|
+
const restartNoResponseTimer = (): void => {
|
|
316
|
+
if (responseTimeoutMs <= 0) return;
|
|
317
|
+
if (noResponseTimer) clearTimeout(noResponseTimer);
|
|
318
|
+
noResponseTimer = setTimeout(() => {
|
|
319
|
+
responseTimeoutHit = true;
|
|
320
|
+
killProcessTree(child.pid, child);
|
|
321
|
+
try {
|
|
322
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logInternalError("child-pi.response-timeout-term", error, `pid=${child.pid}`);
|
|
325
|
+
}
|
|
326
|
+
}, responseTimeoutMs);
|
|
327
|
+
noResponseTimer.unref();
|
|
328
|
+
};
|
|
329
|
+
const clearNoResponseTimer = (): void => {
|
|
330
|
+
if (noResponseTimer) clearTimeout(noResponseTimer);
|
|
331
|
+
noResponseTimer = undefined;
|
|
332
|
+
};
|
|
333
|
+
restartNoResponseTimer();
|
|
334
|
+
const lineObserver = new ChildPiLineObserver({
|
|
335
|
+
...input,
|
|
336
|
+
onStdoutLine: (line) => {
|
|
337
|
+
restartNoResponseTimer();
|
|
338
|
+
stdout = appendBoundedTail(stdout, `${line}\n`);
|
|
339
|
+
input.onStdoutLine?.(line);
|
|
340
|
+
},
|
|
341
|
+
onJsonEvent: (event) => {
|
|
342
|
+
restartNoResponseTimer();
|
|
343
|
+
input.onJsonEvent?.(event);
|
|
344
|
+
if (!isFinalAssistantEvent(event) || childExited || settled || finalDrainTimer) return;
|
|
345
|
+
finalDrainTimer = setTimeout(() => {
|
|
346
|
+
if (settled || childExited) return;
|
|
347
|
+
forcedFinalDrain = true;
|
|
348
|
+
try {
|
|
349
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
350
|
+
} catch (error) {
|
|
351
|
+
logInternalError("child-pi.final-drain-term", error, `pid=${child.pid}`);
|
|
352
|
+
}
|
|
353
|
+
hardKillTimer = setTimeout(() => {
|
|
354
|
+
if (settled || childExited) return;
|
|
355
|
+
try {
|
|
356
|
+
hardKilled = true;
|
|
357
|
+
child.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
|
358
|
+
} catch (error) {
|
|
359
|
+
logInternalError("child-pi.final-drain-kill", error, `pid=${child.pid}`);
|
|
360
|
+
}
|
|
361
|
+
}, hardKillMs);
|
|
362
|
+
hardKillTimer.unref();
|
|
363
|
+
}, finalDrainMs);
|
|
364
|
+
finalDrainTimer.unref();
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const clearFinalDrainTimers = (): void => {
|
|
369
|
+
if (finalDrainTimer) clearTimeout(finalDrainTimer);
|
|
370
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
371
|
+
finalDrainTimer = undefined;
|
|
372
|
+
hardKillTimer = undefined;
|
|
373
|
+
};
|
|
374
|
+
const clearPostExitGuard = (): void => {
|
|
375
|
+
if (postExitGuardCleanup) {
|
|
376
|
+
postExitGuardCleanup();
|
|
377
|
+
postExitGuardCleanup = undefined;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const clearChildPiTimeouts = (): void => {
|
|
381
|
+
clearNoResponseTimer();
|
|
382
|
+
clearFinalDrainTimers();
|
|
383
|
+
clearPostExitGuard();
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const settle = (result: ChildPiRunResult): void => {
|
|
387
|
+
if (settled) return;
|
|
388
|
+
settled = true;
|
|
389
|
+
clearChildPiTimeouts();
|
|
390
|
+
lineObserver.flush();
|
|
391
|
+
input.signal?.removeEventListener("abort", abort);
|
|
392
|
+
try {
|
|
393
|
+
cleanupTempDir(built.tempDir);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
cleanupErrors.push(error instanceof Error ? error.message : String(error));
|
|
396
|
+
}
|
|
397
|
+
resolve({ ...result, exitStatus: result.exitStatus ?? { exitCode: result.exitCode, cancelled: abortRequested, timedOut: responseTimeoutHit, killed: hardKilled, cleanupErrors, finalDrainMs } });
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const abort = (): void => {
|
|
401
|
+
abortRequested = true;
|
|
402
|
+
killProcessTree(child.pid, child);
|
|
403
|
+
if (process.platform !== "win32") {
|
|
404
|
+
trySignalChild(child, "SIGTERM");
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
408
|
+
} catch {
|
|
409
|
+
// Ignore kill races.
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
414
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
415
|
+
restartNoResponseTimer();
|
|
416
|
+
lineObserver.observe(chunk.toString("utf-8"));
|
|
417
|
+
});
|
|
418
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
419
|
+
restartNoResponseTimer();
|
|
420
|
+
stderr = appendBoundedTail(stderr, chunk.toString("utf-8"));
|
|
421
|
+
});
|
|
422
|
+
child.on("error", (error) => {
|
|
423
|
+
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
424
|
+
});
|
|
425
|
+
child.on("exit", () => {
|
|
426
|
+
if (child.pid) {
|
|
427
|
+
activeChildProcesses.delete(child.pid);
|
|
428
|
+
clearHardKillTimer(child.pid);
|
|
429
|
+
}
|
|
430
|
+
childExited = true;
|
|
431
|
+
clearNoResponseTimer();
|
|
432
|
+
clearFinalDrainTimers();
|
|
433
|
+
if (!postExitGuardCleanup) {
|
|
434
|
+
postExitGuardCleanup = attachPostExitStdioGuard(child, {
|
|
435
|
+
idleMs: POST_EXIT_STDIO_GUARD_MS,
|
|
436
|
+
hardMs: HARD_KILL_MS,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
child.on("close", (exitCode) => {
|
|
441
|
+
if (child.pid) {
|
|
442
|
+
activeChildProcesses.delete(child.pid);
|
|
443
|
+
clearHardKillTimer(child.pid);
|
|
444
|
+
}
|
|
445
|
+
const timeoutError = responseTimeoutHit && !stderr.trim() ? { error: `Child Pi produced no new output for ${responseTimeoutMs}ms; process was terminated as unresponsive.` } : undefined;
|
|
446
|
+
const finalExitCode = forcedFinalDrain && !timeoutError ? 0 : exitCode;
|
|
447
|
+
// A final assistant event is the child Pi contract for "the worker produced its answer".
|
|
448
|
+
// Some Pi processes can linger during post-final cleanup/stdio shutdown; finalDrain terminates
|
|
449
|
+
// that lingering process so the parent can continue, but it must not turn a completed
|
|
450
|
+
// subagent answer into a failed task. Real pre-final response timeouts still report errors.
|
|
451
|
+
settle({ exitCode: finalExitCode, stdout, stderr, ...(timeoutError ? { error: timeoutError.error } : {}), exitStatus: { exitCode: finalExitCode, cancelled: abortRequested, timedOut: responseTimeoutHit, killed: hardKilled, cleanupErrors, finalDrainMs } });
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
} finally {
|
|
455
|
+
cleanupTempDir(built.tempDir);
|
|
456
|
+
}
|
|
457
|
+
}
|