pi-subagents 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +125 -19
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +30 -0
  9. package/src/agents/agent-management.ts +189 -8
  10. package/src/agents/agent-serializer.ts +35 -12
  11. package/src/agents/agents.ts +243 -24
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/proactive-skills.ts +191 -0
  14. package/src/agents/skills.ts +117 -20
  15. package/src/extension/doctor.ts +20 -0
  16. package/src/extension/fanout-child.ts +2 -1
  17. package/src/extension/index.ts +50 -5
  18. package/src/extension/schemas.ts +40 -79
  19. package/src/intercom/intercom-bridge.ts +2 -3
  20. package/src/runs/background/async-execution.ts +180 -67
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +53 -5
  23. package/src/runs/background/async-status.ts +4 -1
  24. package/src/runs/background/chain-append.ts +282 -0
  25. package/src/runs/background/chain-root-attachment.ts +161 -0
  26. package/src/runs/background/result-watcher.ts +11 -2
  27. package/src/runs/background/run-status.ts +1 -0
  28. package/src/runs/background/stale-run-reconciler.ts +9 -4
  29. package/src/runs/background/subagent-runner.ts +158 -11
  30. package/src/runs/foreground/chain-execution.ts +26 -2
  31. package/src/runs/foreground/execution.ts +114 -8
  32. package/src/runs/foreground/subagent-executor.ts +611 -87
  33. package/src/runs/shared/acceptance.ts +285 -34
  34. package/src/runs/shared/chain-outputs.ts +23 -8
  35. package/src/runs/shared/completion-guard.ts +1 -1
  36. package/src/runs/shared/dynamic-fanout.ts +5 -3
  37. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  38. package/src/runs/shared/parallel-utils.ts +13 -1
  39. package/src/runs/shared/pi-args.ts +12 -3
  40. package/src/runs/shared/single-output.ts +15 -1
  41. package/src/runs/shared/subagent-control.ts +8 -11
  42. package/src/shared/settings.ts +1 -0
  43. package/src/shared/types.ts +17 -2
  44. package/src/shared/utils.ts +19 -1
  45. package/src/slash/prompt-template-bridge.ts +26 -3
  46. package/src/slash/slash-bridge.ts +3 -1
  47. package/src/slash/slash-commands.ts +34 -4
  48. package/src/tui/render.ts +265 -13
@@ -21,7 +21,7 @@ import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-
21
21
  const WATCHER_RESTART_DELAY_MS = 3000;
22
22
  const POLL_INTERVAL_MS = 3000;
23
23
 
24
- type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch">;
24
+ type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "watch">;
25
25
 
26
26
  type ResultWatcherTimers = {
27
27
  setTimeout: typeof setTimeout;
@@ -91,6 +91,14 @@ function shouldFallBackToPolling(error: unknown): boolean {
91
91
  return code === "EMFILE" || code === "ENOSPC";
92
92
  }
93
93
 
94
+ function resolveNativeWatchDir(fsApi: ResultWatcherFs, resultsDir: string): string {
95
+ try {
96
+ return fsApi.realpathSync.native(resultsDir);
97
+ } catch {
98
+ return resultsDir;
99
+ }
100
+ }
101
+
94
102
  export function createResultWatcher(
95
103
  pi: { events: IntercomEventBus },
96
104
  state: SubagentState,
@@ -264,7 +272,8 @@ export function createResultWatcher(
264
272
  state.watcherRestartTimer = null;
265
273
  }
266
274
  try {
267
- state.watcher = fsApi.watch(resultsDir, (ev, file) => {
275
+ const watchDir = resolveNativeWatchDir(fsApi, resultsDir);
276
+ state.watcher = fsApi.watch(watchDir, (ev, file) => {
268
277
  if (ev !== "rename" || !file) return;
269
278
  const fileName = file.toString();
270
279
  if (!fileName.endsWith(".json")) return;
@@ -205,6 +205,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
205
205
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
206
206
  `Mode: ${status.mode}`,
207
207
  `Progress: ${progressLabel}`,
208
+ status.pendingAppends ? `Pending appends: ${status.pendingAppends}` : undefined,
208
209
  `Started: ${started}`,
209
210
  `Updated: ${updated}`,
210
211
  `Dir: ${asyncDir}`,
@@ -48,9 +48,14 @@ function isNotFoundError(error: unknown): boolean {
48
48
  && (error as NodeJS.ErrnoException).code === "ENOENT";
49
49
  }
50
50
 
51
- function appendJsonl(filePath: string, payload: object): void {
52
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
- fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
51
+ function appendJsonlBestEffort(filePath: string, payload: object): void {
52
+ try {
53
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
54
+ fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
55
+ } catch {
56
+ // Repair status/result writes are the important path. A broken or full
57
+ // diagnostic event log must not make stale-run reconciliation fail.
58
+ }
54
59
  }
55
60
 
56
61
  function readStatusFile(asyncDir: string): AsyncStatus | null {
@@ -223,7 +228,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
223
228
  const repair = buildFailedRepair(status, asyncDir, now, reason);
224
229
  writeAtomicJson(resultPath, repair.result);
225
230
  writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
226
- appendJsonl(path.join(asyncDir, "events.jsonl"), {
231
+ appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
227
232
  type: "subagent.run.repaired_stale",
228
233
  ts: now,
229
234
  runId: repair.status.runId,
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import type { Message } from "@earendil-works/pi-ai";
6
6
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
- import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
7
+ import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
8
8
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
9
9
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
10
10
  import {
@@ -78,6 +78,8 @@ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
78
78
  import { writeInitialProgressFile } from "../../shared/settings.ts";
79
79
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
80
80
  import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
81
+ import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
82
+ import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
81
83
 
82
84
  interface SubagentRunConfig {
83
85
  id: string;
@@ -129,6 +131,79 @@ interface StepResult {
129
131
  }
130
132
 
131
133
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
134
+ const DEFAULT_MAX_ASYNC_EVENTS_BYTES = 50 * 1024 * 1024;
135
+ const ASYNC_EVENTS_MAX_BYTES_ENV = "PI_SUBAGENT_ASYNC_EVENTS_MAX_BYTES";
136
+ const TRUNCATED_EVENT_TYPE = "subagent.events.truncated";
137
+ const TRUNCATION_MARKER_RESERVE_BYTES = 512;
138
+
139
+ interface AsyncEventLogState {
140
+ bytes: number;
141
+ diagnosticsTruncated: boolean;
142
+ }
143
+
144
+ const asyncEventLogStates = new Map<string, AsyncEventLogState>();
145
+
146
+ function maxAsyncEventsBytes(): number {
147
+ const raw = process.env[ASYNC_EVENTS_MAX_BYTES_ENV];
148
+ if (!raw) return DEFAULT_MAX_ASYNC_EVENTS_BYTES;
149
+ const parsed = Number(raw);
150
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_MAX_ASYNC_EVENTS_BYTES;
151
+ return Math.floor(parsed);
152
+ }
153
+
154
+ function eventLogState(filePath: string): AsyncEventLogState {
155
+ let state = asyncEventLogStates.get(filePath);
156
+ if (state) return state;
157
+ let bytes = 0;
158
+ try {
159
+ bytes = fs.statSync(filePath).size;
160
+ } catch (error) {
161
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
162
+ // Diagnostic event accounting is best-effort; writes below are also safe.
163
+ }
164
+ }
165
+ state = { bytes, diagnosticsTruncated: false };
166
+ asyncEventLogStates.set(filePath, state);
167
+ return state;
168
+ }
169
+
170
+ function appendJsonl(filePath: string, line: string): void {
171
+ try {
172
+ appendRawJsonl(filePath, line);
173
+ const state = asyncEventLogStates.get(filePath);
174
+ if (state) state.bytes += Buffer.byteLength(`${line}\n`, "utf-8");
175
+ } catch {
176
+ // Async event logging is diagnostic and must not fail the run.
177
+ }
178
+ }
179
+
180
+ function appendDiagnosticJsonl(filePath: string, line: string, droppedEventType?: string): void {
181
+ if (!line.trim()) return;
182
+ const state = eventLogState(filePath);
183
+ if (state.diagnosticsTruncated) return;
184
+ const maxBytes = maxAsyncEventsBytes();
185
+ const chunkBytes = Buffer.byteLength(`${line}\n`, "utf-8");
186
+ const diagnosticBudget = Math.max(0, maxBytes - TRUNCATION_MARKER_RESERVE_BYTES);
187
+ if (state.bytes + chunkBytes <= diagnosticBudget) {
188
+ appendJsonl(filePath, line);
189
+ return;
190
+ }
191
+
192
+ const marker = JSON.stringify({
193
+ type: TRUNCATED_EVENT_TYPE,
194
+ ts: Date.now(),
195
+ maxBytes,
196
+ droppedEventType,
197
+ });
198
+ if (state.bytes + Buffer.byteLength(`${marker}\n`, "utf-8") <= maxBytes) {
199
+ appendJsonl(filePath, marker);
200
+ }
201
+ state.diagnosticsTruncated = true;
202
+ }
203
+
204
+ function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
205
+ return event.type !== "message_update";
206
+ }
132
207
 
133
208
  function findLatestSessionFile(sessionDir: string): string | null {
134
209
  try {
@@ -272,14 +347,15 @@ function runPiStreaming(
272
347
 
273
348
  const appendChildEvent = (event: Record<string, unknown>) => {
274
349
  if (!childEventContext) return;
275
- appendJsonl(childEventContext.eventsPath, JSON.stringify({
350
+ if (!shouldPersistChildEvent(event)) return;
351
+ appendDiagnosticJsonl(childEventContext.eventsPath, JSON.stringify({
276
352
  ...event,
277
353
  subagentSource: "child",
278
354
  subagentRunId: childEventContext.runId,
279
355
  subagentStepIndex: childEventContext.stepIndex,
280
356
  subagentAgent: childEventContext.agent,
281
357
  observedAt: Date.now(),
282
- }));
358
+ }), typeof event.type === "string" ? event.type : undefined);
283
359
  };
284
360
 
285
361
  const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
@@ -601,6 +677,30 @@ async function runSingleStep(
601
677
  structuredOutputSchemaPath?: string;
602
678
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
603
679
  }> {
680
+ if (step.importAsyncRoot) {
681
+ const imported = await waitForImportedAsyncRoot(step.importAsyncRoot);
682
+ try {
683
+ fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
684
+ } catch {
685
+ // Output files are observability only for imported roots.
686
+ }
687
+ return {
688
+ agent: imported.agent,
689
+ output: imported.output,
690
+ exitCode: imported.exitCode,
691
+ error: imported.error,
692
+ sessionFile: imported.sessionFile,
693
+ intercomTarget: imported.intercomTarget,
694
+ model: imported.model,
695
+ attemptedModels: imported.attemptedModels,
696
+ modelAttempts: imported.modelAttempts,
697
+ structuredOutput: imported.structuredOutput,
698
+ structuredOutputPath: imported.structuredOutputPath,
699
+ structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
700
+ acceptance: imported.acceptance,
701
+ };
702
+ }
703
+
604
704
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
605
705
  ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
606
706
  : undefined);
@@ -650,6 +750,7 @@ async function runSingleStep(
650
750
  }
651
751
  }
652
752
  const { args, env, tempDir } = buildPiArgs({
753
+ parentSessionId: step.parentSessionId,
653
754
  baseArgs: ["--mode", "json", "-p"],
654
755
  task,
655
756
  sessionEnabled,
@@ -658,8 +759,10 @@ async function runSingleStep(
658
759
  model: candidate,
659
760
  inheritProjectContext: step.inheritProjectContext,
660
761
  inheritSkills: step.inheritSkills,
762
+ requireReadTool: Boolean(step.skills?.length),
661
763
  tools: step.tools,
662
764
  extensions: step.extensions,
765
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
663
766
  systemPrompt: step.systemPrompt,
664
767
  systemPromptMode: step.systemPromptMode,
665
768
  mcpDirectTools: step.mcpDirectTools,
@@ -1112,6 +1215,45 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1112
1215
  writeAtomicJson(statusPath, statusPayload);
1113
1216
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1114
1217
  };
1218
+ const consumePendingAppendRequests = (): void => {
1219
+ if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1220
+ const requests = consumeChainAppendRequests(asyncDir);
1221
+ if (requests.length === 0) {
1222
+ const pendingAppends = countPendingChainAppendRequests(asyncDir);
1223
+ if ((statusPayload.pendingAppends ?? 0) !== pendingAppends) {
1224
+ statusPayload.pendingAppends = pendingAppends;
1225
+ statusPayload.lastUpdate = Date.now();
1226
+ writeStatusPayload();
1227
+ }
1228
+ return;
1229
+ }
1230
+ const appendedSteps = requests.flatMap((request) => request.steps);
1231
+ steps.push(...appendedSteps);
1232
+ const now = Date.now();
1233
+ const pendingAppends = countPendingChainAppendRequests(asyncDir);
1234
+ const added = appendRunnerStepsToStatus({
1235
+ status: statusPayload,
1236
+ steps: appendedSteps,
1237
+ now,
1238
+ pendingAppends,
1239
+ });
1240
+ mutatingFailureStates.push(...Array.from({ length: added.addedFlatSteps }, () => createMutatingFailureState()));
1241
+ pendingToolResults.push(...Array.from({ length: added.addedFlatSteps }, () => undefined));
1242
+ if (config.childIntercomTargets) {
1243
+ config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
1244
+ }
1245
+ writeStatusPayload();
1246
+ for (const request of requests) {
1247
+ appendJsonl(eventsPath, JSON.stringify({
1248
+ type: "subagent.chain.append.accepted",
1249
+ ts: now,
1250
+ runId: id,
1251
+ requestId: request.id,
1252
+ stepCount: request.steps.length,
1253
+ pendingAppends,
1254
+ }));
1255
+ }
1256
+ };
1115
1257
  const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
1116
1258
  const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1117
1259
  if (!groupNode) return;
@@ -1403,10 +1545,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1403
1545
  );
1404
1546
 
1405
1547
  let flatIndex = 0;
1548
+ let stepCursor = 0;
1406
1549
 
1407
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
1550
+ while (true) {
1408
1551
  if (interrupted) break;
1409
- const step = steps[stepIndex];
1552
+ consumePendingAppendRequests();
1553
+ if (stepCursor >= steps.length) break;
1554
+ const stepIndex = stepCursor++;
1555
+ const step = steps[stepIndex]!;
1410
1556
 
1411
1557
  if (isDynamicRunnerGroup(step)) {
1412
1558
  const groupStartFlatIndex = flatIndex;
@@ -1835,7 +1981,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1835
1981
  outputs,
1836
1982
  sessionDir: taskSessionDir,
1837
1983
  artifactsDir, artifactConfig, id,
1838
- flatIndex: fi, flatStepCount: flatSteps.length,
1984
+ flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
1839
1985
  outputFile: path.join(asyncDir, `output-${fi}.log`),
1840
1986
  piPackageRoot: config.piPackageRoot,
1841
1987
  piArgv1: config.piArgv1,
@@ -2000,7 +2146,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2000
2146
  outputs,
2001
2147
  sessionDir: config.sessionDir,
2002
2148
  artifactsDir, artifactConfig, id,
2003
- flatIndex, flatStepCount: flatSteps.length,
2149
+ flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
2004
2150
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2005
2151
  piPackageRoot: config.piPackageRoot,
2006
2152
  piArgv1: config.piArgv1,
@@ -2131,11 +2277,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2131
2277
  }
2132
2278
 
2133
2279
  const resultMode = config.resultMode ?? statusPayload.mode;
2134
- const agentName = flatSteps.length === 1
2135
- ? flatSteps[0].agent
2280
+ const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2281
+ const agentName = finalFlatAgents.length === 1
2282
+ ? finalFlatAgents[0]!
2136
2283
  : resultMode === "parallel"
2137
- ? `parallel:${flatSteps.map((s) => s.agent).join("+")}`
2138
- : `chain:${flatSteps.map((s) => s.agent).join("->")}`;
2284
+ ? `parallel:${finalFlatAgents.join("+")}`
2285
+ : `chain:${finalFlatAgents.join("->")}`;
2139
2286
  let sessionFile: string | undefined;
2140
2287
  let shareUrl: string | undefined;
2141
2288
  let gistUrl: string | undefined;
@@ -102,6 +102,7 @@ interface ParallelChainRunInput {
102
102
  globalTaskIndex: number;
103
103
  sessionDirForIndex: (idx?: number) => string | undefined;
104
104
  sessionFileForIndex?: (idx?: number) => string | undefined;
105
+ sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
105
106
  shareEnabled: boolean;
106
107
  artifactConfig: ArtifactConfig;
107
108
  artifactsDir: string;
@@ -136,6 +137,8 @@ interface ParallelChainRunInput {
136
137
  worktreeSetup?: WorktreeSetup;
137
138
  maxSubagentDepth: number;
138
139
  nestedRoute?: NestedRouteInfo;
140
+ timeoutMs?: number;
141
+ deadlineAt?: number;
139
142
  }
140
143
 
141
144
  function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
@@ -266,6 +269,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
266
269
  ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
267
270
  : undefined;
268
271
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
272
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
269
273
  cwd: taskCwd,
270
274
  signal: input.signal,
271
275
  interruptSignal: interruptController.signal,
@@ -274,7 +278,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
274
278
  runId: input.runId,
275
279
  index: input.globalTaskIndex + taskIndex,
276
280
  sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
277
- sessionFile: input.sessionFileForIndex?.(input.globalTaskIndex + taskIndex),
281
+ sessionFile: input.sessionFileForTask?.(task.agent, input.globalTaskIndex + taskIndex)
282
+ ?? input.sessionFileForIndex?.(input.globalTaskIndex + taskIndex),
278
283
  share: input.shareEnabled,
279
284
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
280
285
  artifactConfig: input.artifactConfig,
@@ -293,6 +298,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
293
298
  structuredOutput: structuredRuntime,
294
299
  acceptance: task.acceptance,
295
300
  acceptanceContext: { mode: "chain" },
301
+ timeoutMs: input.timeoutMs,
302
+ deadlineAt: input.deadlineAt,
296
303
  onUpdate: input.onUpdate
297
304
  ? (progressUpdate) => {
298
305
  const stepResults = progressUpdate.details?.results || [];
@@ -365,6 +372,7 @@ interface ChainExecutionParams {
365
372
  shareEnabled: boolean;
366
373
  sessionDirForIndex: (idx?: number) => string | undefined;
367
374
  sessionFileForIndex?: (idx?: number) => string | undefined;
375
+ sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
368
376
  artifactsDir: string;
369
377
  artifactConfig: ArtifactConfig;
370
378
  includeProgress?: boolean;
@@ -395,6 +403,8 @@ interface ChainExecutionParams {
395
403
  nestedRoute?: NestedRouteInfo;
396
404
  worktreeSetupHook?: string;
397
405
  worktreeSetupHookTimeoutMs?: number;
406
+ timeoutMs?: number;
407
+ deadlineAt?: number;
398
408
  }
399
409
 
400
410
  interface ChainExecutionResult {
@@ -422,6 +432,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
422
432
  shareEnabled,
423
433
  sessionDirForIndex,
424
434
  sessionFileForIndex,
435
+ sessionFileForTask,
425
436
  artifactsDir,
426
437
  artifactConfig,
427
438
  includeProgress,
@@ -584,6 +595,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
584
595
  tuiBehaviorOverrides = result.behaviorOverrides;
585
596
  }
586
597
 
598
+ const deadlineAt = params.deadlineAt ?? (params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined);
587
599
  let prev = "";
588
600
  let globalTaskIndex = 0;
589
601
  let progressCreated = false;
@@ -649,6 +661,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
649
661
  globalTaskIndex,
650
662
  sessionDirForIndex,
651
663
  sessionFileForIndex,
664
+ sessionFileForTask,
652
665
  shareEnabled,
653
666
  artifactConfig,
654
667
  artifactsDir,
@@ -670,6 +683,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
670
683
  nestedRoute: params.nestedRoute,
671
684
  worktreeSetup,
672
685
  maxSubagentDepth: params.maxSubagentDepth,
686
+ timeoutMs: params.timeoutMs,
687
+ deadlineAt,
673
688
  });
674
689
  globalTaskIndex += step.parallel.length;
675
690
 
@@ -739,6 +754,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
739
754
  output: getSingleResultOutput(result),
740
755
  exitCode: result.exitCode,
741
756
  error: result.error,
757
+ timedOut: result.timedOut,
742
758
  outputTargetPath,
743
759
  outputTargetExists: outputTargetPath ? fs.existsSync(outputTargetPath) : undefined,
744
760
  };
@@ -855,6 +871,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
855
871
  globalTaskIndex,
856
872
  sessionDirForIndex,
857
873
  sessionFileForIndex,
874
+ sessionFileForTask,
858
875
  shareEnabled,
859
876
  artifactConfig,
860
877
  artifactsDir,
@@ -875,6 +892,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
875
892
  foregroundControl,
876
893
  nestedRoute: params.nestedRoute,
877
894
  maxSubagentDepth: params.maxSubagentDepth,
895
+ timeoutMs: params.timeoutMs,
896
+ deadlineAt,
878
897
  });
879
898
  globalTaskIndex += dynamicParallelStep.parallel.length;
880
899
 
@@ -970,6 +989,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
970
989
  output: getSingleResultOutput(result),
971
990
  exitCode: result.exitCode,
972
991
  error: result.error,
992
+ timedOut: result.timedOut,
973
993
  }));
974
994
  prev = aggregateParallelOutputs(taskResults, (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`);
975
995
  } else {
@@ -1054,6 +1074,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1054
1074
  ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1055
1075
  : undefined;
1056
1076
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1077
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1057
1078
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1058
1079
  signal,
1059
1080
  interruptSignal: interruptController.signal,
@@ -1062,7 +1083,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1062
1083
  runId,
1063
1084
  index: globalTaskIndex,
1064
1085
  sessionDir: sessionDirForIndex(globalTaskIndex),
1065
- sessionFile: sessionFileForIndex?.(globalTaskIndex),
1086
+ sessionFile: sessionFileForTask?.(seqStep.agent, globalTaskIndex)
1087
+ ?? sessionFileForIndex?.(globalTaskIndex),
1066
1088
  share: shareEnabled,
1067
1089
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1068
1090
  artifactConfig,
@@ -1081,6 +1103,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1081
1103
  structuredOutput: structuredRuntime,
1082
1104
  acceptance: seqStep.acceptance,
1083
1105
  acceptanceContext: { mode: "chain" },
1106
+ timeoutMs: params.timeoutMs,
1107
+ deadlineAt,
1084
1108
  onUpdate: onUpdate
1085
1109
  ? (p) => {
1086
1110
  const stepResults = p.details?.results || [];