@selesai/code 0.8.4 → 0.8.6

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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
@@ -0,0 +1,104 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import type { ProcessTreeTerminalV1 } from "../../shared/types.ts";
3
+
4
+ const DEFAULT_TERM_GRACE_MS = 3000;
5
+ const DEFAULT_KILL_VERIFY_MS = 1000;
6
+ const VERIFY_INTERVAL_MS = 25;
7
+
8
+ type SignalResult = "sent" | "absent" | { diagnostic: string };
9
+
10
+ function diagnostic(error: unknown): string {
11
+ return error instanceof Error ? error.message : String(error);
12
+ }
13
+
14
+ function signalProcess(id: number, signal: NodeJS.Signals): SignalResult {
15
+ try {
16
+ process.kill(id, signal);
17
+ return "sent";
18
+ } catch (error) {
19
+ if ((error as NodeJS.ErrnoException).code === "ESRCH") return "absent";
20
+ return { diagnostic: diagnostic(error) };
21
+ }
22
+ }
23
+
24
+ function activeProcessGroupMembers(processGroupId: number): number[] | { diagnostic: string } {
25
+ const result = spawnSync("ps", ["-axo", "pid=,pgid=,stat="], { encoding: "utf-8" });
26
+ if (result.error || result.status !== 0) {
27
+ return { diagnostic: result.error ? diagnostic(result.error) : (result.stderr.trim() || `ps exited with ${result.status}`) };
28
+ }
29
+ const members: number[] = [];
30
+ for (const line of result.stdout.split("\n")) {
31
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)/.exec(line);
32
+ if (!match || Number(match[2]) !== processGroupId || match[3]!.startsWith("Z")) continue;
33
+ members.push(Number(match[1]));
34
+ }
35
+ return members;
36
+ }
37
+
38
+ async function waitUntilGroupTerminal(
39
+ processGroupId: number,
40
+ timeoutMs: number,
41
+ ): Promise<false | { state: "enumeration-failed" | "still-active"; diagnostic: string }> {
42
+ const deadline = Date.now() + timeoutMs;
43
+ while (true) {
44
+ const members = activeProcessGroupMembers(processGroupId);
45
+ if (Array.isArray(members) && members.length === 0) return false;
46
+ const remaining = deadline - Date.now();
47
+ if (remaining <= 0) {
48
+ if (!Array.isArray(members)) return { state: "enumeration-failed", diagnostic: members.diagnostic };
49
+ return { state: "still-active", diagnostic: `Process group ${processGroupId} still has active members: ${members.join(", ")}.` };
50
+ }
51
+ await new Promise<void>((resolve) => setTimeout(resolve, Math.min(VERIFY_INTERVAL_MS, remaining)));
52
+ }
53
+ }
54
+
55
+ function observed(processGroupId: number): ProcessTreeTerminalV1 {
56
+ return { state: "observed", mechanism: "posix-process-group", processGroupId, verifiedAt: Date.now() };
57
+ }
58
+
59
+ /** Owns one writer process group and arbitrates its cleanup exactly once. */
60
+ export interface OwnedProcessTreeController {
61
+ terminate(): Promise<ProcessTreeTerminalV1>;
62
+ finishAfterWriterClose(): Promise<ProcessTreeTerminalV1>;
63
+ }
64
+
65
+ export function createOwnedProcessTreeController(
66
+ pid: number,
67
+ options: { termGraceMs?: number; killVerifyMs?: number } = {},
68
+ ): OwnedProcessTreeController {
69
+ let termination: Promise<ProcessTreeTerminalV1> | undefined;
70
+ const posixGroupOwned = process.platform !== "win32";
71
+ const target = posixGroupOwned ? -pid : pid;
72
+
73
+ const terminate = (): Promise<ProcessTreeTerminalV1> => {
74
+ if (termination) return termination;
75
+ termination = (async () => {
76
+ if (!posixGroupOwned) {
77
+ signalProcess(target, "SIGTERM");
78
+ return { state: "unknown", reason: "unsupported-platform" };
79
+ }
80
+ const term = signalProcess(target, "SIGTERM");
81
+ if (term !== "sent" && term !== "absent") {
82
+ return { state: "unknown", reason: "signal-failed", diagnostic: term.diagnostic };
83
+ }
84
+ const termExit = await waitUntilGroupTerminal(pid, options.termGraceMs ?? DEFAULT_TERM_GRACE_MS);
85
+ if (termExit === false) return observed(pid);
86
+
87
+ const kill = signalProcess(target, "SIGKILL");
88
+ if (kill !== "sent" && kill !== "absent") {
89
+ const members = activeProcessGroupMembers(pid);
90
+ if (!Array.isArray(members) || members.length > 0) {
91
+ return { state: "unknown", reason: "signal-failed", diagnostic: kill.diagnostic };
92
+ }
93
+ }
94
+ const killExit = await waitUntilGroupTerminal(pid, options.killVerifyMs ?? DEFAULT_KILL_VERIFY_MS);
95
+ if (killExit !== false) {
96
+ return { state: "unknown", reason: "verification-failed", diagnostic: killExit.diagnostic };
97
+ }
98
+ return observed(pid);
99
+ })();
100
+ return termination;
101
+ };
102
+
103
+ return { terminate, finishAfterWriterClose: terminate };
104
+ }
@@ -10,6 +10,7 @@ import {
10
10
  type ProcessTerminalV1,
11
11
  } from "../../shared/types.ts";
12
12
  import { canonicalSessionId, inspectSessionLease } from "../shared/session-lease.ts";
13
+ import { releaseActiveRunIndex } from "./active-run-index.ts";
13
14
 
14
15
  export interface ProcessTerminalCandidate {
15
16
  version: 1;
@@ -40,9 +41,19 @@ function validProcessInstance(value: unknown, kind?: "runner" | "pi-writer"): va
40
41
  if (typeof value.closeObservedAt !== "number" || !Number.isFinite(value.closeObservedAt)) return false;
41
42
  if (typeof value.exitCode !== "number" && value.exitCode !== null) return false;
42
43
  if (typeof value.signal !== "string" && value.signal !== null) return false;
43
- return value.kind === "runner"
44
- ? value.attempt === undefined
45
- : typeof value.attempt === "number" && Number.isInteger(value.attempt) && value.attempt >= 0;
44
+ if (value.kind === "runner") return value.attempt === undefined;
45
+ if (typeof value.attempt !== "number" || !Number.isInteger(value.attempt) || value.attempt < 0 || !isRecord(value.processTree)) return false;
46
+ if (value.processTree.state === "observed") {
47
+ return value.processTree.mechanism === "posix-process-group"
48
+ && typeof value.processTree.processGroupId === "number"
49
+ && Number.isInteger(value.processTree.processGroupId)
50
+ && value.processTree.processGroupId > 0
51
+ && typeof value.processTree.verifiedAt === "number"
52
+ && Number.isFinite(value.processTree.verifiedAt);
53
+ }
54
+ return value.processTree.state === "unknown"
55
+ && ["unsupported-platform", "signal-failed", "verification-failed"].includes(String(value.processTree.reason))
56
+ && (value.processTree.diagnostic === undefined || typeof value.processTree.diagnostic === "string");
46
57
  }
47
58
 
48
59
  function validInstance(value: unknown): value is ProcessInstanceExitV1 {
@@ -249,6 +260,8 @@ export function finalizeProcessTerminal(
249
260
  proof = unknownProof(runId, runnerClose.processInstanceId, "canonical-session-release-unverified");
250
261
  } else if (inconsistentWriters || (allWriters.length === 0 && expectedEntries.length === 0)) {
251
262
  proof = unknownProof(runId, runnerClose.processInstanceId, "writer-close-unverified");
263
+ } else if (allWriters.some((writer) => writer.kind === "pi-writer" && writer.processTree.state !== "observed")) {
264
+ proof = unknownProof(runId, runnerClose.processInstanceId, "process-tree-unverified");
252
265
  } else {
253
266
  const runner: ProcessInstanceExitV1 = { kind: "runner", ...runnerClose };
254
267
  const canonicalSession = session && sessionProjection(candidate, session);
@@ -271,6 +284,7 @@ export function finalizeProcessTerminal(
271
284
  try {
272
285
  writeAtomicJson(processTerminalPath(asyncDir), proof);
273
286
  durable = true;
287
+ if (proof.state === "observed") releaseActiveRunIndex(asyncDir);
274
288
  overlayStatus(asyncDir, proof, candidateForOverlay);
275
289
  fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({ type: "subagent.run.process_terminal", lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, ts: Date.now(), runId, processTerminal: proof })}\n`, "utf-8");
276
290
  } catch {
@@ -168,7 +168,24 @@ export function createResultWatcher(
168
168
  const ownsSession = (sessionId: string, epoch: number) => {
169
169
  if (!deliveryActive || epoch !== deliveryEpoch) return false;
170
170
  if (!activeSessionId && state.currentSessionId) activeSessionId = state.currentSessionId;
171
- return activeSessionId === sessionId && state.currentSessionId === sessionId;
171
+ if (activeSessionId !== state.currentSessionId) return false;
172
+ if (sessionId === state.currentSessionId) return true;
173
+ return (state.sessionLineage ?? []).includes(sessionId);
174
+ };
175
+
176
+ const lineageOwnsResult = (identity: ResultFileIdentity, file: string): boolean => {
177
+ const lineage = state.sessionLineage;
178
+ if (!identity.sessionId || !lineage || !lineage.includes(identity.sessionId)) return false;
179
+ // Replay guard: only accept lineage results written around or after the
180
+ // adoption window. The previous session's watcher deleted every result it
181
+ // delivered, so older files are stale leftovers, not missed deliveries.
182
+ const windowStart = state.adoptedResultWindowStart;
183
+ if (windowStart === undefined) return false;
184
+ try {
185
+ return fsApi.statSync(path.join(resultsDir, file)).mtimeMs >= windowStart;
186
+ } catch {
187
+ return false;
188
+ }
172
189
  };
173
190
 
174
191
  const scheduleResult = (file: string, triggerTurn: boolean, delayMs = 0) => {
@@ -210,6 +227,7 @@ export function createResultWatcher(
210
227
  // files keep their existing diagnostics and compatibility behavior.
211
228
  if (!identity.sessionId) return true;
212
229
  if (identity.sessionId === state.currentSessionId) return true;
230
+ if (lineageOwnsResult(identity, file)) return true;
213
231
  if (identity.asyncDir && fsApi.existsSync(path.join(identity.asyncDir, MISSION_BINDING_FILE))) return true;
214
232
  if (identity.runId && (observed ?? observedRunIds()).has(identity.runId)) return true;
215
233
  return Boolean(deps.observeCompletion && !deps.observedCompletionRunIds);
@@ -32,8 +32,8 @@ function boundedTaskSummary(value: string | undefined): string {
32
32
  : normalized;
33
33
  }
34
34
 
35
- export function listRetainedChildren(asyncDirRoot: string, sessionId: string): RetainedChild[] {
36
- return listAsyncRuns(asyncDirRoot, { sessionId, states: ["complete"], reconcile: false })
35
+ export function listRetainedChildren(asyncDirRoot: string, sessionIds: string[]): RetainedChild[] {
36
+ return listAsyncRuns(asyncDirRoot, { sessionIds, states: ["complete"], reconcile: false })
37
37
  .flatMap((run) => {
38
38
  if (!run.parentWorkflowRunId || run.steps.length !== 1) return [];
39
39
  const step = run.steps[0]!;
@@ -94,9 +94,10 @@ function toolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resu
94
94
 
95
95
  function foregroundIds(state: SubagentState | undefined): string[] {
96
96
  if (!state) return [];
97
+ const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : state.currentSessionId ? [state.currentSessionId] : []);
97
98
  const remembered = state.currentSessionId
98
99
  ? [...(state.foregroundRuns?.values() ?? [])]
99
- .filter((run) => run.sessionId === state.currentSessionId)
100
+ .filter((run) => run.sessionId !== undefined && acceptedSessions.has(run.sessionId))
100
101
  .map((run) => run.runId)
101
102
  : [];
102
103
  return [...new Set([...state.foregroundControls.keys(), ...remembered])];
@@ -106,7 +107,9 @@ function hasExactForegroundId(state: SubagentState | undefined, id: string): boo
106
107
  if (!state) return false;
107
108
  if (state.foregroundControls.has(id)) return true;
108
109
  const remembered = state.foregroundRuns?.get(id);
109
- return Boolean(remembered && state.currentSessionId && remembered.sessionId === state.currentSessionId);
110
+ if (!remembered || !state.currentSessionId || remembered.sessionId === undefined) return false;
111
+ const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId]);
112
+ return acceptedSessions.has(remembered.sessionId);
110
113
  }
111
114
 
112
115
  function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolutionScope | undefined {
@@ -19,6 +19,7 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
19
19
  import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
20
20
  import { readMissionBinding } from "../../missions/lifecycle.ts";
21
21
  import { formatWorkflowJsonPreview } from "../../workflows/scripted-workflow.ts";
22
+ import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
22
23
 
23
24
  interface RunStatusParams {
24
25
  action?: string;
@@ -216,6 +217,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
216
217
  const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
217
218
  const resultsDir = deps.resultsDir ?? DIRS.results;
218
219
  const currentSessionId = deps.state?.currentSessionId ?? undefined;
220
+ const acceptedSessions = new Set<string>(deps.state?.sessionLineage?.length ? deps.state.sessionLineage : currentSessionId ? [currentSessionId] : []);
221
+ const ownsSession = (sessionId: string | undefined) => sessionId !== undefined && acceptedSessions.has(sessionId);
219
222
  if (params.view && params.view !== "fleet" && params.view !== "transcript") {
220
223
  return {
221
224
  content: [{ type: "text", text: `Unknown status view: ${params.view}. Valid: fleet, transcript.` }],
@@ -235,7 +238,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
235
238
  };
236
239
  }
237
240
  try {
238
- const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: currentSessionId, resultsDir, kill: deps.kill, now: deps.now });
241
+ const runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], ...(acceptedSessions.size ? { sessionIds: [...acceptedSessions] } : {}), resultsDir, kill: deps.kill, now: deps.now });
239
242
  if (params.view === "transcript") {
240
243
  if (runs.length === 1) return inspectSubagentStatus({ ...params, id: runs[0]!.id }, deps);
241
244
  return {
@@ -331,9 +334,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
331
334
  const status = reconciliation.status;
332
335
  if (!status && diskStatus?.displayDismissedAt !== undefined) {
333
336
  if (params.view === "transcript") {
334
- if (currentSessionId && diskStatus.sessionId !== currentSessionId) {
337
+ if (currentSessionId && !ownsSession(diskStatus.sessionId)) {
335
338
  return {
336
- content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session." }],
339
+ content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session or its carried-over predecessors." }],
337
340
  isError: true,
338
341
  details: { mode: "single", results: [] },
339
342
  };
@@ -350,9 +353,9 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
350
353
  const eventsPath = path.join(asyncDir, "events.jsonl");
351
354
  if (status) {
352
355
  if (params.view === "transcript") {
353
- if (currentSessionId && status.sessionId !== currentSessionId) {
356
+ if (currentSessionId && !ownsSession(status.sessionId)) {
354
357
  return {
355
- content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session." }],
358
+ content: [{ type: "text", text: "Transcript view is only available for async runs owned by the current session or its carried-over predecessors." }],
356
359
  isError: true,
357
360
  details: { mode: "single", results: [] },
358
361
  };
@@ -389,6 +392,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
389
392
  const steeringText = formatSteeringSummary(status);
390
393
  const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
391
394
  ?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
395
+ const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
396
+ const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
392
397
  let missionId: string | undefined;
393
398
  try {
394
399
  missionId = readMissionBinding(asyncDir)?.missionId;
@@ -410,6 +415,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
410
415
  statusActivityText ? `Activity: ${statusActivityText}` : undefined,
411
416
  steeringText ? `Steering: ${steeringText}` : undefined,
412
417
  `Mode: ${status.mode}`,
418
+ runFanoutBudget ? formatRunFanoutBudget(runFanoutBudget) : undefined,
413
419
  status.parentWorkflowRunId ? `Workflow parent: ${status.parentWorkflowRunId}${status.workflowKey ? ` (${status.workflowKey})` : ""}` : undefined,
414
420
  status.mode === "workflow" && workflowReturnPreview !== undefined ? `Return: ${workflowReturnPreview}` : undefined,
415
421
  status.mode === "workflow" && workflowEmitPreview !== undefined ? `Latest emit: ${workflowEmitPreview}` : undefined,
@@ -466,7 +472,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
466
472
  if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
467
473
  if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
468
474
 
469
- return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
475
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
470
476
  }
471
477
  }
472
478
 
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
4
- import { updateActiveRunIndex } from "./active-run-index.ts";
4
+ import { releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
5
5
  import { readStatus } from "../../shared/utils.ts";
6
6
  import { DIRS, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode } from "../../shared/types.ts";
7
7
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -261,7 +261,7 @@ function writeFailedRepair(asyncDir: string, status: AsyncStatus, resultPath: st
261
261
  const repair = buildFailedRepair(status, asyncDir, now, reason);
262
262
  writeAtomicJson(resultPath, repair.result);
263
263
  writeAtomicJson(path.join(asyncDir, "status.json"), repair.status);
264
- updateActiveRunIndex(asyncDir, repair.status.state);
264
+ if (repair.status.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
265
265
  appendJsonlBestEffort(path.join(asyncDir, "events.jsonl"), {
266
266
  type: "subagent.run.repaired_stale",
267
267
  ts: now,
@@ -351,7 +351,7 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
351
351
  : undefined;
352
352
  if (terminalStatus) {
353
353
  writeAtomicJson(path.join(asyncDir, "status.json"), terminalStatus);
354
- updateActiveRunIndex(asyncDir, terminalStatus.state);
354
+ if (terminalStatus.processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
355
355
  return { status: terminalStatus, repaired: true, resultPath, message: "Existing async result file was used to repair stale running status." };
356
356
  }
357
357
  if (effectiveStatus.displayDismissedAt === undefined) return { status: effectiveStatus, repaired: false, resultPath };