pi-subagents 0.17.4 → 0.17.5

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.17.5] - 2026-04-23
6
+
7
+ ### Added
8
+ - Added subagent control activity state for foreground and async runs, including `starting`/`active`/`quiet`/`stalled`/`paused` tracking, compact stalled/recovered/paused control events, and an in-tool `action: "interrupt"` soft interrupt that pauses the current child turn without adding another top-level tool.
9
+
10
+ ### Changed
11
+ - Updated bundled agents to use `openai-codex/gpt-5.5` defaults, with `scout` on `openai-codex/gpt-5.5-mini` and `oracle-executor` on `openai-codex/gpt-5.5:xhigh`.
12
+
13
+ ### Fixed
14
+ - Async/background status token reporting now falls back to in-memory model-attempt usage when detached runs do not produce session `.jsonl` files, which also preserves token totals across model fallback retries.
15
+ - Non-Windows subagent launches now use plain `pi` again instead of reusing the current CLI script path, avoiding runs that get confused by installed `dist/cli.js` entrypoints.
16
+
5
17
  ## [0.17.4] - 2026-04-22
6
18
 
7
19
  ### Added
@@ -2,7 +2,7 @@
2
2
  name: context-builder
3
3
  description: Analyzes requirements and codebase, generates context and meta-prompt
4
4
  tools: read, grep, find, ls, bash, write, web_search
5
- model: openai-codex/gpt-5.4
5
+ model: openai-codex/gpt-5.5
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
@@ -2,7 +2,7 @@
2
2
  name: oracle-executor
3
3
  description: High-context implementation agent that executes only after main-agent approval
4
4
  tools: read, grep, find, ls, bash, edit, write, intercom
5
- model: openai-codex/gpt-5.3-codex
5
+ model: openai-codex/gpt-5.5:xhigh
6
6
  thinking: high
7
7
  systemPromptMode: replace
8
8
  inheritProjectContext: true
package/agents/oracle.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: oracle
3
3
  description: High-context decision-consistency oracle that protects inherited state and prevents drift
4
4
  tools: read, grep, find, ls, bash, intercom
5
- model: openai-codex/gpt-5.4
5
+ model: openai-codex/gpt-5.5
6
6
  thinking: high
7
7
  systemPromptMode: replace
8
8
  inheritProjectContext: true
package/agents/planner.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: planner
3
3
  description: Creates implementation plans from context and requirements
4
4
  tools: read, grep, find, ls, write
5
- model: openai-codex/gpt-5.4
5
+ model: openai-codex/gpt-5.5
6
6
  thinking: high
7
7
  systemPromptMode: replace
8
8
  inheritProjectContext: true
@@ -2,7 +2,7 @@
2
2
  name: researcher
3
3
  description: Autonomous web researcher — searches, evaluates, and synthesizes a focused research brief
4
4
  tools: read, write, web_search, fetch_content, get_search_content
5
- model: openai-codex/gpt-5.4
5
+ model: openai-codex/gpt-5.5
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
@@ -2,7 +2,7 @@
2
2
  name: reviewer
3
3
  description: Code review specialist that validates implementation and fixes issues
4
4
  tools: read, grep, find, ls, bash, edit, write
5
- model: openai-codex/gpt-5.3-codex
5
+ model: openai-codex/gpt-5.5
6
6
  thinking: high
7
7
  systemPromptMode: replace
8
8
  inheritProjectContext: true
package/agents/scout.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff
4
4
  tools: read, grep, find, ls, bash, write
5
- model: openai-codex/gpt-5.4-mini
5
+ model: openai-codex/gpt-5.5-mini
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
package/agents/worker.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: worker
3
3
  description: General-purpose subagent with full capabilities
4
- model: openai-codex/gpt-5.4
4
+ model: openai-codex/gpt-5.5
5
5
  systemPromptMode: replace
6
6
  inheritProjectContext: true
7
7
  inheritSkills: false
@@ -22,6 +22,7 @@ import {
22
22
  type ArtifactConfig,
23
23
  type Details,
24
24
  type MaxOutputConfig,
25
+ type ResolvedControlConfig,
25
26
  ASYNC_DIR,
26
27
  RESULTS_DIR,
27
28
  TEMP_ROOT_DIR,
@@ -75,6 +76,7 @@ export interface AsyncChainParams {
75
76
  maxSubagentDepth: number;
76
77
  worktreeSetupHook?: string;
77
78
  worktreeSetupHookTimeoutMs?: number;
79
+ controlConfig?: ResolvedControlConfig;
78
80
  }
79
81
 
80
82
  export interface AsyncSingleParams {
@@ -96,6 +98,7 @@ export interface AsyncSingleParams {
96
98
  maxSubagentDepth: number;
97
99
  worktreeSetupHook?: string;
98
100
  worktreeSetupHookTimeoutMs?: number;
101
+ controlConfig?: ResolvedControlConfig;
99
102
  }
100
103
 
101
104
  export interface AsyncExecutionResult {
@@ -178,6 +181,7 @@ export function executeAsyncChain(
178
181
  maxSubagentDepth,
179
182
  worktreeSetupHook,
180
183
  worktreeSetupHookTimeoutMs,
184
+ controlConfig,
181
185
  } = params;
182
186
  const chainSkills = params.chainSkills ?? [];
183
187
  const availableModels = params.availableModels;
@@ -297,6 +301,7 @@ export function executeAsyncChain(
297
301
  piArgv1: process.argv[1],
298
302
  worktreeSetupHook,
299
303
  worktreeSetupHookTimeoutMs,
304
+ controlConfig,
300
305
  },
301
306
  id,
302
307
  runnerCwd,
@@ -364,6 +369,7 @@ export function executeAsyncSingle(
364
369
  maxSubagentDepth,
365
370
  worktreeSetupHook,
366
371
  worktreeSetupHookTimeoutMs,
372
+ controlConfig,
367
373
  } = params;
368
374
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
369
375
  const skillNames = params.skills ?? agentConfig.skills ?? [];
@@ -430,6 +436,7 @@ export function executeAsyncSingle(
430
436
  piArgv1: process.argv[1],
431
437
  worktreeSetupHook,
432
438
  worktreeSetupHookTimeoutMs,
439
+ controlConfig,
433
440
  },
434
441
  id,
435
442
  runnerCwd,
@@ -56,6 +56,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
56
56
  if (status) {
57
57
  const previousStatus = job.status;
58
58
  job.status = status.state;
59
+ job.activityState = status.activityState;
59
60
  job.mode = status.mode;
60
61
  job.currentStep = status.currentStep ?? job.currentStep;
61
62
  job.stepsTotal = status.steps?.length ?? job.stepsTotal;
@@ -68,7 +69,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
68
69
  job.outputFile = status.outputFile ?? job.outputFile;
69
70
  job.totalTokens = status.totalTokens ?? job.totalTokens;
70
71
  job.sessionFile = status.sessionFile ?? job.sessionFile;
71
- if ((job.status === "complete" || job.status === "failed") && previousStatus !== job.status) {
72
+ if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && previousStatus !== job.status) {
72
73
  scheduleCleanup(job.asyncId);
73
74
  }
74
75
  continue;
@@ -102,6 +103,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
102
103
  asyncId: info.id,
103
104
  asyncDir,
104
105
  status: "queued",
106
+ activityState: "starting",
105
107
  mode: info.chain ? "chain" : "single",
106
108
  agents,
107
109
  stepsTotal: agents?.length,
@@ -136,6 +138,8 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
136
138
  }
137
139
  state.cleanupTimers.clear();
138
140
  state.asyncJobs.clear();
141
+ state.foregroundControls?.clear();
142
+ state.lastForegroundControlId = null;
139
143
  state.resultFileCoalescer.clear();
140
144
  if (ctx?.hasUI) {
141
145
  state.lastUiContext = ctx;
package/async-status.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatTokens, shortenPath } from "./formatters.ts";
4
- import { type AsyncStatus, type TokenUsage } from "./types.ts";
4
+ import { type ActivityState, type AsyncStatus, type TokenUsage } from "./types.ts";
5
+ import { DEFAULT_CONTROL_CONFIG, deriveActivityState } from "./subagent-control.ts";
5
6
  import { readStatus } from "./utils.ts";
6
7
 
7
8
  export interface AsyncRunStepSummary {
8
9
  index: number;
9
10
  agent: string;
10
11
  status: string;
12
+ activityState?: ActivityState;
11
13
  durationMs?: number;
12
14
  tokens?: TokenUsage;
13
15
  skills?: string[];
@@ -19,7 +21,8 @@ export interface AsyncRunStepSummary {
19
21
  export interface AsyncRunSummary {
20
22
  id: string;
21
23
  asyncDir: string;
22
- state: "queued" | "running" | "complete" | "failed";
24
+ state: "queued" | "running" | "complete" | "failed" | "paused";
25
+ activityState?: ActivityState;
23
26
  mode: "single" | "chain";
24
27
  cwd?: string;
25
28
  startedAt: number;
@@ -66,28 +69,57 @@ function isAsyncRunDir(root: string, entry: string): boolean {
66
69
  }
67
70
  }
68
71
 
72
+ function outputFileMtime(outputFile: string | undefined): number | undefined {
73
+ if (!outputFile) return undefined;
74
+ try {
75
+ return fs.statSync(outputFile).mtimeMs;
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
80
+
81
+ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): ActivityState | undefined {
82
+ if (status.state === "paused") return "paused";
83
+ if (status.state !== "running") return status.activityState;
84
+ const outputPath = status.outputFile ? (path.isAbsolute(status.outputFile) ? status.outputFile : path.join(asyncDir, status.outputFile)) : undefined;
85
+ const lastActivityAt = outputFileMtime(outputPath) ?? status.lastUpdate;
86
+ return deriveActivityState({
87
+ config: DEFAULT_CONTROL_CONFIG,
88
+ startedAt: status.startedAt,
89
+ lastActivityAt,
90
+ hasSeenActivity: Boolean(lastActivityAt),
91
+ paused: false,
92
+ });
93
+ }
94
+
69
95
  function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
96
+ const activityState = deriveAsyncActivityState(asyncDir, status);
70
97
  return {
71
98
  id: status.runId || path.basename(asyncDir),
72
99
  asyncDir,
73
100
  state: status.state,
101
+ activityState,
74
102
  mode: status.mode,
75
103
  cwd: status.cwd,
76
104
  startedAt: status.startedAt,
77
105
  lastUpdate: status.lastUpdate,
78
106
  endedAt: status.endedAt,
79
107
  currentStep: status.currentStep,
80
- steps: (status.steps ?? []).map((step, index) => ({
81
- index,
82
- agent: step.agent,
83
- status: step.status,
84
- ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
85
- ...(step.tokens ? { tokens: step.tokens } : {}),
86
- ...(step.skills ? { skills: step.skills } : {}),
87
- ...(step.model ? { model: step.model } : {}),
88
- ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
89
- ...(step.error ? { error: step.error } : {}),
90
- })),
108
+ steps: (status.steps ?? []).map((step, index) => {
109
+ const stepActivityState = step.activityState ?? (step.status === "running" ? activityState : undefined);
110
+ return {
111
+ index,
112
+ agent: step.agent,
113
+ status: step.status,
114
+ ...(stepActivityState ? { activityState: stepActivityState } : {}),
115
+ ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
116
+ ...(step.tokens ? { tokens: step.tokens } : {}),
117
+ ...(step.skills ? { skills: step.skills } : {}),
118
+ ...(step.model ? { model: step.model } : {}),
119
+ ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
120
+ ...(step.error ? { error: step.error } : {}),
121
+ };
122
+ }),
91
123
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
92
124
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
93
125
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
@@ -100,8 +132,9 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
100
132
  switch (state) {
101
133
  case "running": return 0;
102
134
  case "queued": return 1;
103
- case "failed": return 2;
104
- case "complete": return 3;
135
+ case "failed": return 2;
136
+ case "paused": return 2;
137
+ case "complete": return 3;
105
138
  }
106
139
  };
107
140
  return [...runs].sort((a, b) => {
@@ -142,7 +175,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
142
175
  export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5): AsyncRunOverlayData {
143
176
  const all = listAsyncRuns(asyncDirRoot);
144
177
  const recent = all
145
- .filter((run) => run.state === "complete" || run.state === "failed")
178
+ .filter((run) => run.state === "complete" || run.state === "failed" || run.state === "paused")
146
179
  .sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
147
180
  .slice(0, recentLimit);
148
181
  return {
@@ -152,7 +185,8 @@ export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5):
152
185
  }
153
186
 
154
187
  function formatStepLine(step: AsyncRunStepSummary): string {
155
- const parts = [`${step.index + 1}. ${step.agent}`, step.status];
188
+ const state = step.activityState ? `${step.status}/${step.activityState}` : step.status;
189
+ const parts = [`${step.index + 1}. ${step.agent}`, state];
156
190
  if (step.model) parts.push(step.model);
157
191
  if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
158
192
  if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
@@ -163,7 +197,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
163
197
  const stepCount = run.steps.length || 1;
164
198
  const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
165
199
  const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
166
- return `${run.id} | ${run.state} | ${run.mode} | ${stepLabel} | ${cwd}`;
200
+ const state = run.activityState ? `${run.state}/${run.activityState}` : run.state;
201
+ return `${run.id} | ${state} | ${run.mode} | ${stepLabel} | ${cwd}`;
167
202
  }
168
203
 
169
204
  export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
@@ -40,10 +40,12 @@ import {
40
40
  type WorktreeSetup,
41
41
  } from "./worktree.ts";
42
42
  import {
43
+ type ActivityState,
43
44
  type AgentProgress,
44
45
  type ArtifactConfig,
45
46
  type ArtifactPaths,
46
47
  type Details,
48
+ type ResolvedControlConfig,
47
49
  type SingleResult,
48
50
  MAX_CONCURRENCY,
49
51
  resolveChildMaxSubagentDepth,
@@ -82,6 +84,14 @@ interface ParallelChainRunInput {
82
84
  artifactsDir: string;
83
85
  signal?: AbortSignal;
84
86
  onUpdate?: (r: AgentToolResult<Details>) => void;
87
+ controlConfig: ResolvedControlConfig;
88
+ foregroundControl?: {
89
+ updatedAt: number;
90
+ currentAgent?: string;
91
+ currentIndex?: number;
92
+ currentActivityState?: ActivityState;
93
+ interrupt?: () => boolean;
94
+ };
85
95
  results: SingleResult[];
86
96
  allProgress: AgentProgress[];
87
97
  chainAgents: string[];
@@ -186,10 +196,25 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
186
196
  const outputPath = typeof behavior.output === "string"
187
197
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
188
198
  : undefined;
199
+ const interruptController = new AbortController();
200
+ if (input.foregroundControl) {
201
+ input.foregroundControl.currentAgent = task.agent;
202
+ input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
203
+ input.foregroundControl.currentActivityState = "starting";
204
+ input.foregroundControl.updatedAt = Date.now();
205
+ input.foregroundControl.interrupt = () => {
206
+ if (interruptController.signal.aborted) return false;
207
+ interruptController.abort();
208
+ input.foregroundControl!.currentActivityState = "paused";
209
+ input.foregroundControl!.updatedAt = Date.now();
210
+ return true;
211
+ };
212
+ }
189
213
 
190
214
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
191
215
  cwd: taskCwd,
192
216
  signal: input.signal,
217
+ interruptSignal: interruptController.signal,
193
218
  runId: input.runId,
194
219
  index: input.globalTaskIndex + taskIndex,
195
220
  sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
@@ -199,28 +224,41 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
199
224
  artifactConfig: input.artifactConfig,
200
225
  outputPath,
201
226
  maxSubagentDepth,
227
+ controlConfig: input.controlConfig,
202
228
  modelOverride: effectiveModel,
203
229
  availableModels: input.availableModels,
204
230
  preferredModelProvider: input.ctx.model?.provider,
205
231
  skills: behavior.skills === false ? [] : behavior.skills,
206
232
  onUpdate: input.onUpdate
207
233
  ? (progressUpdate) => {
208
- const stepResults = progressUpdate.details?.results || [];
209
- const stepProgress = progressUpdate.details?.progress || [];
210
- input.onUpdate?.({
211
- ...progressUpdate,
212
- details: {
213
- mode: "chain",
214
- results: input.results.concat(stepResults),
215
- progress: input.allProgress.concat(stepProgress),
216
- chainAgents: input.chainAgents,
217
- totalSteps: input.totalSteps,
218
- currentStepIndex: input.stepIndex,
219
- },
220
- });
234
+ const stepResults = progressUpdate.details?.results || [];
235
+ const stepProgress = progressUpdate.details?.progress || [];
236
+ if (input.foregroundControl && stepProgress.length > 0) {
237
+ const current = stepProgress[0];
238
+ input.foregroundControl.currentAgent = task.agent;
239
+ input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
240
+ input.foregroundControl.currentActivityState = current?.activityState;
241
+ input.foregroundControl.updatedAt = Date.now();
221
242
  }
243
+ input.onUpdate?.({
244
+ ...progressUpdate,
245
+ details: {
246
+ mode: "chain",
247
+ results: input.results.concat(stepResults),
248
+ progress: input.allProgress.concat(stepProgress),
249
+ controlEvents: progressUpdate.details?.controlEvents,
250
+ chainAgents: input.chainAgents,
251
+ totalSteps: input.totalSteps,
252
+ currentStepIndex: input.stepIndex,
253
+ },
254
+ });
255
+ }
222
256
  : undefined,
223
257
  });
258
+ if (input.foregroundControl?.currentIndex === input.globalTaskIndex + taskIndex) {
259
+ input.foregroundControl.interrupt = undefined;
260
+ input.foregroundControl.updatedAt = Date.now();
261
+ }
224
262
 
225
263
  if (result.exitCode !== 0 && failFast) {
226
264
  aborted = true;
@@ -249,6 +287,14 @@ export interface ChainExecutionParams {
249
287
  includeProgress?: boolean;
250
288
  clarify?: boolean;
251
289
  onUpdate?: (r: AgentToolResult<Details>) => void;
290
+ controlConfig: ResolvedControlConfig;
291
+ foregroundControl?: {
292
+ updatedAt: number;
293
+ currentAgent?: string;
294
+ currentIndex?: number;
295
+ currentActivityState?: ActivityState;
296
+ interrupt?: () => boolean;
297
+ };
252
298
  chainSkills?: string[];
253
299
  chainDir?: string;
254
300
  maxSubagentDepth: number;
@@ -286,6 +332,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
286
332
  includeProgress,
287
333
  clarify,
288
334
  onUpdate,
335
+ controlConfig,
336
+ foregroundControl,
289
337
  chainSkills: chainSkillsParam,
290
338
  chainDir: chainDirBase,
291
339
  } = params;
@@ -484,6 +532,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
484
532
  allProgress,
485
533
  chainAgents,
486
534
  totalSteps,
535
+ controlConfig,
536
+ foregroundControl,
487
537
  worktreeSetup,
488
538
  maxSubagentDepth: params.maxSubagentDepth,
489
539
  });
@@ -495,6 +545,23 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
495
545
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
496
546
  }
497
547
 
548
+ const interrupted = parallelResults.find((result) => result.interrupted);
549
+ if (interrupted) {
550
+ return {
551
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
552
+ details: buildChainExecutionDetails({
553
+ results,
554
+ includeProgress,
555
+ allProgress,
556
+ allArtifactPaths,
557
+ artifactsDir,
558
+ chainAgents,
559
+ totalSteps,
560
+ currentStepIndex: stepIndex,
561
+ }),
562
+ };
563
+ }
564
+
498
565
  const failures = parallelResults
499
566
  .map((result, originalIndex) => ({ ...result, originalIndex }))
500
567
  .filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
@@ -603,10 +670,25 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
603
670
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
604
671
  : undefined;
605
672
  const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
673
+ const interruptController = new AbortController();
674
+ if (foregroundControl) {
675
+ foregroundControl.currentAgent = seqStep.agent;
676
+ foregroundControl.currentIndex = globalTaskIndex;
677
+ foregroundControl.currentActivityState = "starting";
678
+ foregroundControl.updatedAt = Date.now();
679
+ foregroundControl.interrupt = () => {
680
+ if (interruptController.signal.aborted) return false;
681
+ interruptController.abort();
682
+ foregroundControl.currentActivityState = "paused";
683
+ foregroundControl.updatedAt = Date.now();
684
+ return true;
685
+ };
686
+ }
606
687
 
607
688
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
608
689
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
609
690
  signal,
691
+ interruptSignal: interruptController.signal,
610
692
  runId,
611
693
  index: globalTaskIndex,
612
694
  sessionDir: sessionDirForIndex(globalTaskIndex),
@@ -616,28 +698,41 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
616
698
  artifactConfig,
617
699
  outputPath,
618
700
  maxSubagentDepth,
701
+ controlConfig,
619
702
  modelOverride: effectiveModel,
620
703
  availableModels,
621
704
  preferredModelProvider: ctx.model?.provider,
622
705
  skills: behavior.skills === false ? [] : behavior.skills,
623
706
  onUpdate: onUpdate
624
707
  ? (p) => {
625
- const stepResults = p.details?.results || [];
626
- const stepProgress = p.details?.progress || [];
627
- onUpdate({
628
- ...p,
629
- details: {
630
- mode: "chain",
631
- results: results.concat(stepResults),
632
- progress: allProgress.concat(stepProgress),
633
- chainAgents,
634
- totalSteps,
635
- currentStepIndex: stepIndex,
636
- },
637
- });
708
+ const stepResults = p.details?.results || [];
709
+ const stepProgress = p.details?.progress || [];
710
+ if (foregroundControl && stepProgress.length > 0) {
711
+ const current = stepProgress[0];
712
+ foregroundControl.currentAgent = seqStep.agent;
713
+ foregroundControl.currentIndex = globalTaskIndex;
714
+ foregroundControl.currentActivityState = current?.activityState;
715
+ foregroundControl.updatedAt = Date.now();
638
716
  }
717
+ onUpdate({
718
+ ...p,
719
+ details: {
720
+ mode: "chain",
721
+ results: results.concat(stepResults),
722
+ progress: allProgress.concat(stepProgress),
723
+ controlEvents: p.details?.controlEvents,
724
+ chainAgents,
725
+ totalSteps,
726
+ currentStepIndex: stepIndex,
727
+ },
728
+ });
729
+ }
639
730
  : undefined,
640
731
  });
732
+ if (foregroundControl?.currentIndex === globalTaskIndex) {
733
+ foregroundControl.interrupt = undefined;
734
+ foregroundControl.updatedAt = Date.now();
735
+ }
641
736
  recordRun(seqStep.agent, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
642
737
 
643
738
  globalTaskIndex++;
@@ -663,6 +758,22 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
663
758
  }
664
759
  }
665
760
 
761
+ if (r.interrupted) {
762
+ return {
763
+ content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${r.agent}). Waiting for explicit next action.` }],
764
+ details: buildChainExecutionDetails({
765
+ results,
766
+ includeProgress,
767
+ allProgress,
768
+ allArtifactPaths,
769
+ artifactsDir,
770
+ chainAgents,
771
+ totalSteps,
772
+ currentStepIndex: stepIndex,
773
+ }),
774
+ };
775
+ }
776
+
666
777
  if (r.exitCode !== 0) {
667
778
  const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
668
779
  index: stepIndex,