pi-crew 0.11.6 → 0.11.7
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 +95 -0
- package/dist/index.mjs +486 -481
- package/package.json +2 -2
- package/src/config/config-validation.ts +28 -0
- package/src/config/env-vars.ts +23 -0
- package/src/config/types.ts +47 -0
- package/src/prompt/prompt-runtime.ts +33 -4
- package/src/runtime/child-pi/child-pi-spawn.ts +7 -0
- package/src/runtime/child-pi/child-pi.ts +93 -4
- package/src/runtime/child-pi/session-recovery.ts +334 -0
- package/src/runtime/model/model-scope.ts +141 -21
- package/src/runtime/model/pi-args.ts +104 -3
- package/src/runtime/peer-dep.ts +24 -0
- package/src/runtime/rpc/README.md +100 -0
- package/src/runtime/rpc/frame-client.ts +328 -0
- package/src/runtime/rpc/rpc-worker.ts +151 -0
- package/src/runtime/rpc/ui-request-policy.ts +112 -0
- package/src/runtime/run-worker.ts +14 -0
- package/src/runtime/surface/surface-spawn.ts +8 -1
- package/src/runtime/task-output-context.ts +191 -32
- package/src/runtime/task-packet.ts +3 -0
- package/src/runtime/task-runner/child-executor.ts +40 -2
- package/src/runtime/task-runner/post-execution.ts +5 -3
- package/src/runtime/task-runner/pre-execution.ts +24 -1
- package/src/runtime/task-runner/prompt-builder.ts +10 -2
- package/src/schema/config-schema.ts +25 -0
- package/src/schema/team-tool-schema.ts +10 -0
- package/src/ui/pi-ui-compat.ts +18 -9
- package/src/ui/widget/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=22.
|
|
11
|
+
"node": ">=22.19.0"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
@@ -77,6 +77,11 @@ const LIMIT_CEILINGS = {
|
|
|
77
77
|
// (10_000 turns), which capped the effective timeout at 10s and silently disabled
|
|
78
78
|
// any larger value (e.g. 300_000 = 5min) via parsePositiveInteger returning undefined.
|
|
79
79
|
runtimeTaskTimeoutMs: 24 * 60 * 60 * 1000,
|
|
80
|
+
// Handoff budget (est tokens, chars/4) for the dynamic.dependencyContext
|
|
81
|
+
// prompt layer. Above the ceiling the budget is treated as the OFF-switch
|
|
82
|
+
// (untrimmed render), mirroring the render-site semantics in
|
|
83
|
+
// task-output-context.ts.
|
|
84
|
+
runtimeHandoffBudgetTokens: 1_000_000,
|
|
80
85
|
nestingMaxSlots: 64,
|
|
81
86
|
nestingMaxDepth: 10,
|
|
82
87
|
} as const;
|
|
@@ -325,6 +330,21 @@ function parseModelFallbackConfig(value: unknown): CrewRuntimeConfig["modelFallb
|
|
|
325
330
|
return Object.values(modelFallback).some((entry) => entry !== undefined) ? modelFallback : undefined;
|
|
326
331
|
}
|
|
327
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Handoff budget for the dynamic.dependencyContext prompt layer (est tokens,
|
|
335
|
+
* chars/4). Deliberately NOT parsePositiveInteger: out-of-range values are
|
|
336
|
+
* dropped silently (undefined → render site falls back to default 1800)
|
|
337
|
+
* rather than rejecting the whole runtime block. The ≤0 / >1_000_000
|
|
338
|
+
* OFF-switch is expressible only via env PI_CREW_HANDOFF_BUDGET_TOKENS
|
|
339
|
+
* (env bypasses this parse and resolves first); the config schema documents
|
|
340
|
+
* 1..1_000_000 as the valid range.
|
|
341
|
+
*/
|
|
342
|
+
function parseHandoffBudgetTokens(value: unknown): number | undefined {
|
|
343
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return undefined;
|
|
344
|
+
if (value < 1 || value > LIMIT_CEILINGS.runtimeHandoffBudgetTokens) return undefined;
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
|
|
328
348
|
function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
329
349
|
const obj = asRecord(value);
|
|
330
350
|
if (!obj) return { inheritContext: true } as CrewRuntimeConfig;
|
|
@@ -362,10 +382,18 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
362
382
|
return Object.values(parsed).some((v) => v !== undefined) ? parsed : undefined;
|
|
363
383
|
})(),
|
|
364
384
|
excludeContextBash: parseWithSchema(Type.Boolean(), obj.excludeContextBash),
|
|
385
|
+
handoffBudgetTokens: parseHandoffBudgetTokens(obj.handoffBudgetTokens),
|
|
365
386
|
agentExtensions: parseStringList(obj.agentExtensions),
|
|
366
387
|
isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
|
|
367
388
|
surface: parseSurfacePolicy(obj.surface),
|
|
368
389
|
modelFallback: parseModelFallbackConfig(obj.modelFallback),
|
|
390
|
+
// F19-1 discipline: every key declared in types.ts + schema MUST be
|
|
391
|
+
// emitted here or the config key is dead (runtime.sessionRecovery —
|
|
392
|
+
// W2/P1-1; runtime.workerTransport — W7/P2-3, experimental;
|
|
393
|
+
// runtime.hermeticWorkers — R3-19/D5 reversal, hermetic spawns).
|
|
394
|
+
sessionRecovery: parseWithSchema(Type.Boolean(), obj.sessionRecovery),
|
|
395
|
+
hermeticWorkers: parseWithSchema(Type.Boolean(), obj.hermeticWorkers),
|
|
396
|
+
workerTransport: parseWithSchema(Type.Union([Type.Literal("stdio"), Type.Literal("rpc")]), obj.workerTransport),
|
|
369
397
|
};
|
|
370
398
|
return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
|
|
371
399
|
}
|
package/src/config/env-vars.ts
CHANGED
|
@@ -317,6 +317,11 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
317
317
|
name: "PI_CREW_DISABLE_RESULT_READ_CACHE",
|
|
318
318
|
doc: "'1' bypasses the per-run result-artifact read cache (R10-1 bypass control; task-output-context.ts createResultArtifactReadCache)",
|
|
319
319
|
},
|
|
320
|
+
PI_CREW_HANDOFF_BUDGET_TOKENS: {
|
|
321
|
+
name: "PI_CREW_HANDOFF_BUDGET_TOKENS",
|
|
322
|
+
parser: "int",
|
|
323
|
+
doc: "est-token (chars/4) budget for the dynamic.dependencyContext prompt layer; beats runtime.handoffBudgetTokens, default 1800; resolved ≤0 or >1_000_000 disables the trim (task-output-context.ts renderDependencyOutputContext)",
|
|
324
|
+
},
|
|
320
325
|
PI_CREW_MOCK_LIVE_SESSION: {
|
|
321
326
|
name: "PI_CREW_MOCK_LIVE_SESSION",
|
|
322
327
|
doc: "'success' mocks a successful live-session (live-session-runtime.ts:558, runtime-resolver.ts:38/116)",
|
|
@@ -440,6 +445,24 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
440
445
|
name: "PI_CREW_AGENT_EVENTS_PATH",
|
|
441
446
|
doc: "per-agent events log (<stateRoot>/agents/<taskId>/events.jsonl) written by the surface worker recorder (surface-worker.ts; derived by prepareSurfaceSpawn)",
|
|
442
447
|
},
|
|
448
|
+
PI_CREW_SESSION_RECOVERY: {
|
|
449
|
+
name: "PI_CREW_SESSION_RECOVERY",
|
|
450
|
+
parser: "boolean",
|
|
451
|
+
doc: "W2 session-file recovery override for killed workers (exitCode null / killed): '0' disables, '1' forces on — beats ChildPiRunInput.sessionRecovery / runtime.sessionRecovery; default ON (session-recovery.ts:resolveSessionRecoveryEnabled)",
|
|
452
|
+
},
|
|
453
|
+
PI_CREW_HERMETIC_WORKERS: {
|
|
454
|
+
name: "PI_CREW_HERMETIC_WORKERS",
|
|
455
|
+
parser: "boolean",
|
|
456
|
+
doc: "R3-19/D5 hermetic worker spawn override: '1' forces --no-extensions on worker argv, '0' restores the old ambient-stack parity (discovered/configured/built-in extensions load); beats BuildPiWorkerArgsInput.hermeticWorkers / runtime.hermeticWorkers; default ON (pi-args.ts:resolveHermeticWorkers). Explicit -e prompt-runtime + agent extensions + --skill flags are unaffected (cli.md:186-191)",
|
|
457
|
+
},
|
|
458
|
+
PI_CREW_WORKER_TRANSPORT: {
|
|
459
|
+
name: "PI_CREW_WORKER_TRANSPORT",
|
|
460
|
+
doc: "W7 EXPERIMENTAL worker transport gate: 'rpc' selects the prototype RPC frame-client (src/runtime/rpc/) — the run-worker seam currently returns a structured not-implemented result instead of spawning; anything else/unset → default stdio child-pi path (rpc-worker.ts resolveWorkerTransport)",
|
|
461
|
+
},
|
|
462
|
+
PI_CREW_RPC_DIALOG_ANSWER: {
|
|
463
|
+
name: "PI_CREW_RPC_DIALOG_ANSWER",
|
|
464
|
+
doc: "W7 EXPERIMENTAL dialog auto-answer policy for RPC-mode extension_ui_request dialogs (select/confirm/input/editor): 'cancel' (default) answers {cancelled:true} — universal safe answer; 'block' leaves the server promise pending (debug). Auto-confirm deliberately NOT implemented (security); invalid → 'cancel' (rpc-worker.ts resolveDialogAnswerPolicy)",
|
|
465
|
+
},
|
|
443
466
|
PI_CREW_AUTO_EXIT: {
|
|
444
467
|
name: "PI_CREW_AUTO_EXIT",
|
|
445
468
|
doc: "'1' → the worker shuts its session down after the final settled turn — spec §5.2 D7 (written by prepareSurfaceSpawn, read by surface-worker.ts)",
|
package/src/config/types.ts
CHANGED
|
@@ -65,6 +65,30 @@ export interface CrewRuntimeConfig {
|
|
|
65
65
|
*/
|
|
66
66
|
taskTimeoutMs?: number;
|
|
67
67
|
inheritContext?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* W2 (session-file recovery): when a worker dies without a final assistant
|
|
70
|
+
* event (exitCode null / killed), tail-replay its session JSONL (written by
|
|
71
|
+
* the worker under `<artifactsRoot>/sessions/<taskId>/`) and attach the last
|
|
72
|
+
* COMPLETE assistant record to the task result (`recoveredFromSession`).
|
|
73
|
+
* Default: enabled. The env var PI_CREW_SESSION_RECOVERY overrides this
|
|
74
|
+
* flag in either direction (see session-recovery.ts:resolveSessionRecoveryEnabled).
|
|
75
|
+
* Manifest polling is NOT replaced — this only augments the settle-time result.
|
|
76
|
+
*/
|
|
77
|
+
sessionRecovery?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* R3-19/D5 reversal (2026-10-04): hermetic worker spawns — pass
|
|
80
|
+
* `--no-extensions` on worker argv. Explicit `-e prompt-runtime` and agent
|
|
81
|
+
* `extensions:` still load (cli.md:186-187: "Disables discovered, configured,
|
|
82
|
+
* and built-in extensions. Explicit -e paths still load"), and `--skill`
|
|
83
|
+
* flags are independent — what gets cut is the ~1.37s/spawn ambient package
|
|
84
|
+
* stack (pi-crew host ext + configured packages; probe C §R3b.6: 431ms vs
|
|
85
|
+
* 1806ms to first stdout record) plus the ambient tool surface (team/
|
|
86
|
+
* crew_agent/Agent host tools + 8 direct-exposure MCP tools).
|
|
87
|
+
* Default: true. Env PI_CREW_HERMETIC_WORKERS overrides in either
|
|
88
|
+
* direction (pi-args.ts resolveHermeticWorkers). Surface TUI spawns strip
|
|
89
|
+
* the flag — panes keep the full interactive session.
|
|
90
|
+
*/
|
|
91
|
+
hermeticWorkers?: boolean;
|
|
68
92
|
promptMode?: "replace" | "append";
|
|
69
93
|
groupJoin?: "off" | "group" | "smart";
|
|
70
94
|
groupJoinAckTimeoutMs?: number;
|
|
@@ -85,6 +109,29 @@ export interface CrewRuntimeConfig {
|
|
|
85
109
|
};
|
|
86
110
|
/** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
|
|
87
111
|
excludeContextBash?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Est-token budget for the dynamic.dependencyContext prompt layer
|
|
114
|
+
* (handoff budget). When the rendered dependency output exceeds this many
|
|
115
|
+
* estimated tokens (chars/4 — the same heuristic the prompt breakdown
|
|
116
|
+
* uses), dependencies are trimmed IN DECLARATION ORDER: earlier deps keep
|
|
117
|
+
* their full output, later ones are downgraded to taskId/role/status + a
|
|
118
|
+
* ≤240-char summary head + an artifact pointer
|
|
119
|
+
* (`artifacts/<runId>/results/<taskId>.txt`) the worker can `read` back.
|
|
120
|
+
* Long sharedRead bodies are dropped the same way. Valid range 1..1_000_000;
|
|
121
|
+
* a RESOLVED value ≤0 or >1_000_000 disables the trim entirely
|
|
122
|
+
* (off-switch). Env override PI_CREW_HANDOFF_BUDGET_TOKENS beats this
|
|
123
|
+
* config. Default: 1800.
|
|
124
|
+
*/
|
|
125
|
+
handoffBudgetTokens?: number;
|
|
126
|
+
/**
|
|
127
|
+
* W7 (P2-3) EXPERIMENTAL: transport used to spawn child-pi workers.
|
|
128
|
+
* "stdio" (default) — the production print-mode path, unchanged.
|
|
129
|
+
* "rpc" — prototype RPC frame-client (src/runtime/rpc/); the run-worker
|
|
130
|
+
* seam currently returns a structured not-implemented result instead of
|
|
131
|
+
* spawning. Live gate: env PI_CREW_WORKER_TRANSPORT (config→seam plumbing
|
|
132
|
+
* is the integration-phase follow-up).
|
|
133
|
+
*/
|
|
134
|
+
workerTransport?: "stdio" | "rpc";
|
|
88
135
|
/**
|
|
89
136
|
* Mux-surface policy (mux-surface spec v0.7 §8.1): WHERE worker processes
|
|
90
137
|
* live — a pane in tmux/herdr or headless child processes. Surface only
|
|
@@ -16,6 +16,7 @@ import { pollWorkerInbox } from "./inbox-poll.ts";
|
|
|
16
16
|
import { createMessageTool, type MessageToolParams as MessageToolInputs, shouldRegisterMessageTool } from "./message-tool.ts";
|
|
17
17
|
import { registerScratchpadLifecycle } from "./scratchpad-lifecycle.ts";
|
|
18
18
|
import { createWorkerActivityTracker, registerSurfaceWorkerLifecycle, trackToolActivity } from "./surface-worker.ts";
|
|
19
|
+
import { createWorkerEventsChannel } from "./worker-events-channel.ts";
|
|
19
20
|
|
|
20
21
|
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
21
22
|
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|
|
@@ -653,10 +654,11 @@ async function resolvePark(
|
|
|
653
654
|
}
|
|
654
655
|
|
|
655
656
|
/** ADR item 10: every ask-timeout outcome appends `ask.timedout` to the run's
|
|
656
|
-
* events.jsonl.
|
|
657
|
-
*
|
|
658
|
-
* the
|
|
659
|
-
*
|
|
657
|
+
* events.jsonl. WP-9 made PI_CREW_EVENTS_PATH unconditional (this comment
|
|
658
|
+
* used to say it was scratchpad-gated — stale); this path predates that and
|
|
659
|
+
* keeps deriving the file from the state store invariant eventsPath ===
|
|
660
|
+
* <stateRoot>/events.jsonl — the same invariant child-pi-spawn relies on to
|
|
661
|
+
* derive PI_CREW_STATE_ROOT. Fire-and-forget: never blocks the tool result. */
|
|
660
662
|
function emitAskTimedOutEvent(stateRoot: string, runId: string, taskId: string, questionId: string): void {
|
|
661
663
|
appendEventFireAndForget(path.join(stateRoot, "events.jsonl"), {
|
|
662
664
|
type: "ask.timedout",
|
|
@@ -1030,6 +1032,33 @@ export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
|
|
|
1030
1032
|
void brokerHandle.close().catch(() => undefined);
|
|
1031
1033
|
});
|
|
1032
1034
|
|
|
1035
|
+
// ── W5 (P2-2): observe worker auto-compaction ──────────────────────────
|
|
1036
|
+
// Auto-compaction inside a worker is otherwise silent from the
|
|
1037
|
+
// orchestrator's view (perf reports only see token jumps). The SDK fires
|
|
1038
|
+
// `session_before_compact` right before the host pi compacts the session
|
|
1039
|
+
// context; this listener OBSERVES ONLY — it returns undefined so the host
|
|
1040
|
+
// proceeds with its own preparation untouched (host analog:
|
|
1041
|
+
// compaction-guard.ts `pi.on("session_before_compact", async () => { return; })`).
|
|
1042
|
+
// NAME DISCIPLINE: this is the SDK's SESSION compaction (host context
|
|
1043
|
+
// summarization) — NOT pi-crew's internal `prepareCompaction` event-log
|
|
1044
|
+
// rotation (event-log-rotation.ts); same word, different mechanism.
|
|
1045
|
+
// Channel discipline (WP-9): createWorkerEventsChannel is registered
|
|
1046
|
+
// unconditionally and self-no-ops without PI_CREW_EVENTS_PATH/
|
|
1047
|
+
// PI_CREW_BROKER_RUN_ID, so a main user session pays nothing. Payload is
|
|
1048
|
+
// scalar-only: branchEntries (large) and signal (AbortSignal) are never
|
|
1049
|
+
// serialized; preparation fields are read defensively (SDK shape evolves).
|
|
1050
|
+
const compactEventsChannel = createWorkerEventsChannel();
|
|
1051
|
+
pi.on("session_before_compact", (event) => {
|
|
1052
|
+
const tokensBefore = (event.preparation as { tokensBefore?: unknown } | undefined)?.tokensBefore;
|
|
1053
|
+
compactEventsChannel.emit("worker.session_before_compact", {
|
|
1054
|
+
reason: event.reason,
|
|
1055
|
+
willRetry: event.willRetry,
|
|
1056
|
+
branchEntryCount: event.branchEntries?.length ?? 0,
|
|
1057
|
+
hasCustomInstructions: typeof event.customInstructions === "string",
|
|
1058
|
+
...(typeof tokensBefore === "number" ? { tokensBefore } : {}),
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1033
1062
|
// ── Task 5 (§15.2): worker inbox pickup ────────────────────────────────
|
|
1034
1063
|
// A broker-eligible worker ALSO picks up sibling DMs / group broadcasts
|
|
1035
1064
|
// (`kind:"message"` entries addressed to this task in the durable
|
|
@@ -270,10 +270,17 @@ export function prepareSpawnContext(
|
|
|
270
270
|
agent: input.agent,
|
|
271
271
|
model: input.model,
|
|
272
272
|
sessionEnabled: true,
|
|
273
|
+
// W2 (P1-1): deterministic session identity so crash-path tail-recovery
|
|
274
|
+
// can find the worker's session JSONL (see session-recovery.ts). Falls
|
|
275
|
+
// back to child-pi.ts deriveSessionPaths when unset (idempotent both ways).
|
|
276
|
+
sessionId: input.sessionId,
|
|
277
|
+
sessionDir: input.sessionDir,
|
|
273
278
|
maxDepth: input.maxDepth,
|
|
274
279
|
skillPaths: input.skillPaths,
|
|
275
280
|
role: input.role,
|
|
276
281
|
thinkingOverride: input.thinkingOverride,
|
|
282
|
+
systemPromptAppend: input.systemPromptAppend,
|
|
283
|
+
hermeticWorkers: input.hermeticWorkers,
|
|
277
284
|
// ADR-5 §3: depthOverride is pre-encoded into depthEnv by runChildPi
|
|
278
285
|
// (parent's record depth as base-env PI_CREW_DEPTH); forward it so the
|
|
279
286
|
// child env gets parentDepth+1 = the true grandchild depth.
|
|
@@ -27,6 +27,16 @@ import { ChildPiLineObserver } from "./child-pi-streams.ts";
|
|
|
27
27
|
// Phase 2.3: the six timer constructs moved to child-pi-timers.ts (pure motion).
|
|
28
28
|
import { createChildPiTimers } from "./child-pi-timers.ts";
|
|
29
29
|
import { runMockChildPi } from "./mock-fixtures.ts";
|
|
30
|
+
// W2 (session-file recovery): deterministic worker session flags + crash-path
|
|
31
|
+
// tail-replay of the session JSONL (see session-recovery.ts header).
|
|
32
|
+
import {
|
|
33
|
+
appendWorkerSessionArgs,
|
|
34
|
+
deriveSessionPaths,
|
|
35
|
+
recoverLastAssistantFromSession,
|
|
36
|
+
resolveSessionRecoveryEnabled,
|
|
37
|
+
type SessionRecoveryInfo,
|
|
38
|
+
shouldAttemptSessionRecovery,
|
|
39
|
+
} from "./session-recovery.ts";
|
|
30
40
|
|
|
31
41
|
// ── Re-exports from child-pi-kill.ts (H-7 decomposition step 2) ──
|
|
32
42
|
// killProcessTree is internal (not previously exported) — keep that invariant.
|
|
@@ -185,8 +195,18 @@ export interface ChildPiRunInput {
|
|
|
185
195
|
inheritContext?: boolean;
|
|
186
196
|
/** Pass to pi to mark certain commands as context-excluded. Default: false */
|
|
187
197
|
excludeContextBash?: boolean;
|
|
188
|
-
/** pi session ID for session naming (aligns with pi-crew run ID)
|
|
198
|
+
/** pi session ID for session naming (aligns with pi-crew run ID). W2: now
|
|
199
|
+
* WIRED — flows to `--session-id` (deterministic worker session files for
|
|
200
|
+
* crash-path tail-recovery; see session-recovery.ts). */
|
|
189
201
|
sessionId?: string;
|
|
202
|
+
/** W2 (session-file recovery): explicit session directory override. When
|
|
203
|
+
* absent, derived per-worker as `<artifactsRoot>/sessions/<taskId>/` so
|
|
204
|
+
* session JSONL is cleaned up together with run artifacts. */
|
|
205
|
+
sessionDir?: string;
|
|
206
|
+
/** W2 (session-file recovery): explicit on/off (runtime.sessionRecovery
|
|
207
|
+
* config). Env PI_CREW_SESSION_RECOVERY overrides in either direction;
|
|
208
|
+
* default ON. Only consulted on crash-ish settles (exitCode null / killed). */
|
|
209
|
+
sessionRecovery?: boolean;
|
|
190
210
|
/** Path to steering JSONL file for real-time steer injection. */
|
|
191
211
|
steeringFile?: string;
|
|
192
212
|
/** Run ID for cleanup tracking */
|
|
@@ -197,6 +217,15 @@ export interface ChildPiRunInput {
|
|
|
197
217
|
role?: string;
|
|
198
218
|
/** Team-role thinking override (takes precedence over agent.thinking). */
|
|
199
219
|
thinkingOverride?: string;
|
|
220
|
+
/** R3-1: run-static worker header for the --append-system-prompt channel
|
|
221
|
+
* (renderTaskPrompt().stablePrefix). Threaded by child-executor so the
|
|
222
|
+
* Protocol/mailbox/workspace/runtime-context block survives compaction
|
|
223
|
+
* instead of living in the summarizable user-message span. */
|
|
224
|
+
systemPromptAppend?: string;
|
|
225
|
+
/** R3-19/D5 (hermetic worker spawns): thread runtime.hermeticWorkers to
|
|
226
|
+
* buildPiWorkerArgs (--no-extensions). Env PI_CREW_HERMETIC_WORKERS
|
|
227
|
+
* overrides either way; default TRUE. */
|
|
228
|
+
hermeticWorkers?: boolean;
|
|
200
229
|
/** Root directory for artifacts (used to validate transcriptPath). */
|
|
201
230
|
artifactsRoot?: string;
|
|
202
231
|
/** I5: run events JSONL path — threaded to the worker so its scratchpad
|
|
@@ -291,6 +320,13 @@ export interface ChildPiRunResult {
|
|
|
291
320
|
* text) still produce a non-empty result. Consumers should prefer rawFinalText
|
|
292
321
|
* first — this is a last-resort fallback. */
|
|
293
322
|
intermediateFindings?: string;
|
|
323
|
+
/** W2 (session-file recovery): present when the worker died WITHOUT a
|
|
324
|
+
* final assistant event (exitCode null / killed) and a COMPLETE assistant
|
|
325
|
+
* record was tail-recovered from the worker's session JSONL (deterministic
|
|
326
|
+
* `--session-id`/`--session-dir` under the run artifacts root). Consumers
|
|
327
|
+
* treat it as a crash-path supplement to rawFinalText — never a replacement
|
|
328
|
+
* for clean-path output, and manifest polling is untouched. */
|
|
329
|
+
recoveredFromSession?: SessionRecoveryInfo;
|
|
294
330
|
/**
|
|
295
331
|
* MuxSurface A1: present ONLY when the worker booted inside a multiplexer
|
|
296
332
|
* pane instead of a stdio pipe (spec §13.1). Downstream (T9 EventLogTailSource,
|
|
@@ -371,6 +407,17 @@ function safeLoadSurfaceConfig(cwd: string): PiTeamsConfig {
|
|
|
371
407
|
}
|
|
372
408
|
}
|
|
373
409
|
|
|
410
|
+
/**
|
|
411
|
+
* D1/D5: flags that apply to HEADLESS worker spawns only. The surface TUI
|
|
412
|
+
* branch strips them before booting a pane — a pane hosts a full interactive
|
|
413
|
+
* pi session where the interactive trust prompt must stay available and the
|
|
414
|
+
* ambient extension stack is part of the human-visible session (task mandate:
|
|
415
|
+
* worker spawn CHỈ, KHÔNG đụng surface TUI spawn).
|
|
416
|
+
*/
|
|
417
|
+
function stripHeadlessOnlyFlags(args: string[]): string[] {
|
|
418
|
+
return args.filter((arg) => arg !== "--no-approve" && arg !== "--no-extensions");
|
|
419
|
+
}
|
|
420
|
+
|
|
374
421
|
/**
|
|
375
422
|
* MuxSurface A1 spawn branch (spec §13.1). Returns a ChildPiRunResult when the
|
|
376
423
|
* worker was booted INSIDE a multiplexer pane (no stdio process is spawned and
|
|
@@ -413,7 +460,7 @@ async function trySurfaceBranch(
|
|
|
413
460
|
livePaneCount: degradeController ? degradeController.livePaneCount() : (surfaceOpts?.livePaneCount ?? 0),
|
|
414
461
|
taskId,
|
|
415
462
|
cwd: input.cwd,
|
|
416
|
-
piArgs: builtArgs,
|
|
463
|
+
piArgs: stripHeadlessOnlyFlags(builtArgs),
|
|
417
464
|
stateRoot: input.eventsPath ? path.dirname(input.eventsPath) : "",
|
|
418
465
|
baseDir: surfaceOpts?.baseDir,
|
|
419
466
|
deps: preResolved ? { provider: preResolved } : {},
|
|
@@ -684,6 +731,24 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
684
731
|
const spawnPrep = prepareSpawnContext(brokerSpawn ? { ...input, brokerSpawn } : input, effectiveTask, depthEnv);
|
|
685
732
|
if (spawnPrep.kind === "aborted") return spawnPrep.result;
|
|
686
733
|
const { spawnSpec, mergedEnv, tempDir, builtEnv, builtArgs } = spawnPrep.ctx;
|
|
734
|
+
// W2 (session-file recovery): resolve the worker's deterministic session
|
|
735
|
+
// identity (per-worker dir under the run artifacts root) and arm it on BOTH
|
|
736
|
+
// argv views — the headless spawnSpec.args ([script, ...builtArgs]) and the
|
|
737
|
+
// raw builtArgs (also consumed by the surface branch). prepareSpawnContext
|
|
738
|
+
// does not forward session identity to the arg builder (ownership boundary
|
|
739
|
+
// with child-pi-spawn.ts — see the lane report), so this is the in-zone
|
|
740
|
+
// activation point; appendWorkerSessionArgs is idempotent, making the move
|
|
741
|
+
// to builder-side forwarding later a zero-diff no-op.
|
|
742
|
+
const workerSession = deriveSessionPaths(input);
|
|
743
|
+
const sessionRecoveryEnabled = resolveSessionRecoveryEnabled(input.sessionRecovery);
|
|
744
|
+
if (workerSession) {
|
|
745
|
+
try {
|
|
746
|
+
fs.mkdirSync(workerSession.sessionDir, { recursive: true });
|
|
747
|
+
} catch {
|
|
748
|
+
// Unwritable target → spawn proceeds; recovery simply finds no files.
|
|
749
|
+
}
|
|
750
|
+
appendWorkerSessionArgs(spawnSpec.args, builtArgs, workerSession);
|
|
751
|
+
}
|
|
687
752
|
// MuxSurface A1 (spec §13.1): attempt booting the worker in a mux pane BEFORE
|
|
688
753
|
// spawning a stdio pipe. Returns a finished result in surface mode (pane exit
|
|
689
754
|
// awaited) or null → fall through to the classic headless spawn below.
|
|
@@ -869,6 +934,22 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
869
934
|
clearPostExitGuard();
|
|
870
935
|
};
|
|
871
936
|
|
|
937
|
+
// W2 (session-file recovery): crash-path augment — when the worker died
|
|
938
|
+
// WITHOUT a final assistant event (exitCode null / killed), tail-replay
|
|
939
|
+
// its session JSONL and surface the last COMPLETE assistant record on the
|
|
940
|
+
// resolved result. Best-effort and deadline-bounded (session-recovery.ts)
|
|
941
|
+
// so settle can never hang on pathological IO. This AUGMENTS the result —
|
|
942
|
+
// manifest polling (heartbeat-watcher / detached-run-results) is untouched.
|
|
943
|
+
const recoverForSettle = async (settleResult: ChildPiRunResult): Promise<SessionRecoveryInfo | undefined> => {
|
|
944
|
+
if (!workerSession || !sessionRecoveryEnabled) return undefined;
|
|
945
|
+
if (!shouldAttemptSessionRecovery(settleResult, hardKilled)) return undefined;
|
|
946
|
+
try {
|
|
947
|
+
return (await recoverLastAssistantFromSession(workerSession.sessionDir, workerSession.sessionId)) ?? undefined;
|
|
948
|
+
} catch {
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
872
953
|
const settle = (result: ChildPiRunResult): Promise<void> => {
|
|
873
954
|
if (settled) return Promise.resolve();
|
|
874
955
|
settled = true;
|
|
@@ -885,7 +966,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
885
966
|
// durable transcript on disk.
|
|
886
967
|
return lineObserver
|
|
887
968
|
.flush()
|
|
888
|
-
.then(() => {
|
|
969
|
+
.then(async () => {
|
|
889
970
|
input.signal?.removeEventListener("abort", abort);
|
|
890
971
|
input.signal?.removeEventListener("abort", onParentAbort);
|
|
891
972
|
try {
|
|
@@ -893,12 +974,16 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
893
974
|
} catch (error) {
|
|
894
975
|
cleanupErrors.push(error instanceof Error ? error.message : String(error));
|
|
895
976
|
}
|
|
977
|
+
// W2: crash-path session tail-recovery (await BEFORE resolve so the
|
|
978
|
+
// recovered record rides the same settled result object).
|
|
979
|
+
const recoveredFromSession = await recoverForSettle(result);
|
|
896
980
|
// Catch all errors from settle to prevent unhandled rejection from propagating
|
|
897
981
|
try {
|
|
898
982
|
resolve({
|
|
899
983
|
...result,
|
|
900
984
|
rawFinalText: lineObserver.getRawFinalText(),
|
|
901
985
|
intermediateFindings: lineObserver.getIntermediateFindings(),
|
|
986
|
+
...(recoveredFromSession ? { recoveredFromSession } : {}),
|
|
902
987
|
exitStatus: result.exitStatus ?? {
|
|
903
988
|
exitCode: result.exitCode,
|
|
904
989
|
cancelled: abortRequested,
|
|
@@ -926,7 +1011,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
926
1011
|
);
|
|
927
1012
|
}
|
|
928
1013
|
})
|
|
929
|
-
.catch((flushError) => {
|
|
1014
|
+
.catch(async (flushError) => {
|
|
930
1015
|
// Drain failed — log and still resolve so runChildPi doesn't hang.
|
|
931
1016
|
logInternalError(
|
|
932
1017
|
"child-pi.settle-flush-failed",
|
|
@@ -940,11 +1025,15 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
940
1025
|
} catch (error) {
|
|
941
1026
|
cleanupErrors.push(error instanceof Error ? error.message : String(error));
|
|
942
1027
|
}
|
|
1028
|
+
// W2: crash-path session tail-recovery — same augment on the
|
|
1029
|
+
// drain-failed path (a torn flush is itself crash-adjacent).
|
|
1030
|
+
const recoveredFromSession = await recoverForSettle(result);
|
|
943
1031
|
try {
|
|
944
1032
|
resolve({
|
|
945
1033
|
...result,
|
|
946
1034
|
rawFinalText: lineObserver.getRawFinalText(),
|
|
947
1035
|
intermediateFindings: lineObserver.getIntermediateFindings(),
|
|
1036
|
+
...(recoveredFromSession ? { recoveredFromSession } : {}),
|
|
948
1037
|
exitStatus: result.exitStatus ?? {
|
|
949
1038
|
exitCode: result.exitCode,
|
|
950
1039
|
cancelled: abortRequested,
|