pi-subagents 0.81.0 → 0.82.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 (141) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/bundle/agents.js +3 -3
  3. package/bundle/child-tool-plan.js +2 -2
  4. package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
  5. package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
  6. package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
  7. package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
  8. package/bundle/{chunk-CEKS3TFO.js → chunk-7IVSUMQQ.js} +6 -6
  9. package/bundle/{chunk-KSWQVGDW.js → chunk-7PCKZHR5.js} +225 -18
  10. package/bundle/{chunk-563BHAZ5.js → chunk-7YMXOYEU.js} +33 -25
  11. package/bundle/{chunk-XMFLCKMQ.js → chunk-B5EKXDHE.js} +336 -130
  12. package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
  13. package/bundle/{chunk-L5ZHE4L4.js → chunk-GC4DSPMH.js} +4 -4
  14. package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
  15. package/bundle/{chunk-2ZSNH26H.js → chunk-M2CTFYT3.js} +7 -7
  16. package/bundle/{chunk-TUWLXTAM.js → chunk-MHZ4G23A.js} +681 -320
  17. package/bundle/{chunk-WSBNENFS.js → chunk-NGGAFDL6.js} +14 -4
  18. package/bundle/{chunk-VJHOELQP.js → chunk-NRQ75MGH.js} +160 -78
  19. package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
  20. package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
  21. package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
  22. package/bundle/{chunk-V6MR5TOQ.js → chunk-SIJ4S3EN.js} +25 -19
  23. package/bundle/{chunk-NNLASTQE.js → chunk-VH5OAWVG.js} +19 -5
  24. package/bundle/{chunk-BPS2AGBZ.js → chunk-W2SZ22EM.js} +93 -52
  25. package/bundle/{chunk-V3BIUPLU.js → chunk-WVD4UEL4.js} +256 -52
  26. package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
  27. package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
  28. package/bundle/{chunk-26C3WLYQ.js → chunk-ZLINIYTP.js} +497 -272
  29. package/bundle/{chunk-2XYWN7QJ.js → chunk-ZQGSDHL6.js} +2 -2
  30. package/bundle/control-channel.js +2 -2
  31. package/bundle/index.js +1 -1
  32. package/bundle/intercom-bridge.js +2 -2
  33. package/bundle/preflight.js +8 -8
  34. package/bundle/project-panes.js +3 -3
  35. package/docs/agents.md +31 -0
  36. package/docs/configuration.md +9 -1
  37. package/docs/tool-reference.md +3 -1
  38. package/docs/workflows.md +16 -0
  39. package/package.json +2 -2
  40. package/skills/pi-subagents/references/execution-controls.md +5 -4
  41. package/src/agents/agent-management.js +3 -3
  42. package/src/agents/agent-memory.js +3 -1
  43. package/src/agents/agents.js +6 -6
  44. package/src/agents/frontmatter.d.ts +2 -0
  45. package/src/agents/frontmatter.js +15 -1
  46. package/src/agents/skills.js +2 -1
  47. package/src/extension/config.js +3 -0
  48. package/src/extension/herdr-pi-bridge.js +17 -10
  49. package/src/extension/index.js +82 -2
  50. package/src/extension/schemas.js +3 -3
  51. package/src/intercom/native-supervisor-channel.js +20 -0
  52. package/src/missions/lifecycle.js +7 -1
  53. package/src/runs/background/active-async-capacity.d.ts +1 -0
  54. package/src/runs/background/active-async-capacity.js +9 -3
  55. package/src/runs/background/async-execution.js +20 -10
  56. package/src/runs/background/async-job-tracker.d.ts +6 -1
  57. package/src/runs/background/async-job-tracker.js +9 -4
  58. package/src/runs/background/async-status.d.ts +2 -0
  59. package/src/runs/background/async-status.js +21 -2
  60. package/src/runs/background/await-async-run.js +3 -7
  61. package/src/runs/background/boot-id.d.ts +12 -0
  62. package/src/runs/background/boot-id.js +47 -0
  63. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  64. package/src/runs/background/interrupted-elsewhere.js +68 -0
  65. package/src/runs/background/notify.d.ts +2 -0
  66. package/src/runs/background/notify.js +13 -0
  67. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  68. package/src/runs/background/result-delivery-ownership.js +19 -1
  69. package/src/runs/background/result-watcher.d.ts +3 -1
  70. package/src/runs/background/result-watcher.js +24 -8
  71. package/src/runs/background/run-child-session.d.ts +1 -6
  72. package/src/runs/background/run-child-session.js +1 -1
  73. package/src/runs/background/run-status.js +2 -1
  74. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  75. package/src/runs/background/stale-run-reconciler.js +52 -13
  76. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  77. package/src/runs/background/subagent-runner.d.ts +4 -0
  78. package/src/runs/background/subagent-runner.js +13 -5
  79. package/src/runs/background/subagent-wait.d.ts +1 -1
  80. package/src/runs/background/subagent-wait.js +6 -1
  81. package/src/runs/background/terminal-run-index.d.ts +1 -0
  82. package/src/runs/background/terminal-run-index.js +4 -2
  83. package/src/runs/foreground/async-stop-action.js +3 -1
  84. package/src/runs/foreground/execution.js +6 -3
  85. package/src/runs/foreground/foreground-history.d.ts +16 -2
  86. package/src/runs/foreground/foreground-history.js +133 -31
  87. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  88. package/src/runs/foreground/subagent-executor.js +277 -74
  89. package/src/runs/shared/child-launch.d.ts +2 -2
  90. package/src/runs/shared/child-session.d.ts +3 -2
  91. package/src/runs/shared/child-session.js +51 -8
  92. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  93. package/src/runs/shared/child-tool-plan.js +21 -6
  94. package/src/runs/shared/herdr-connection.d.ts +17 -8
  95. package/src/runs/shared/herdr-connection.js +19 -7
  96. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  97. package/src/runs/shared/herdr-external-adapters.js +1 -1
  98. package/src/runs/shared/herdr-machine.d.ts +12 -2
  99. package/src/runs/shared/herdr-machine.js +55 -9
  100. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  101. package/src/runs/shared/herdr-pane-host.js +155 -0
  102. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  103. package/src/runs/shared/herdr-placed-run.js +48 -395
  104. package/src/runs/shared/local-machine-access.d.ts +5 -0
  105. package/src/runs/shared/local-machine-access.js +28 -0
  106. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  107. package/src/runs/shared/orca-progress-tabs.js +1 -24
  108. package/src/runs/shared/parallel-utils.d.ts +1 -1
  109. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  110. package/src/runs/shared/placed-run-owner.js +93 -0
  111. package/src/runs/shared/placement-host.d.ts +99 -0
  112. package/src/runs/shared/placement-host.js +157 -0
  113. package/src/runs/shared/rex-machine.d.ts +25 -0
  114. package/src/runs/shared/rex-machine.js +83 -0
  115. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  116. package/src/runs/shared/rex-pane-host.js +327 -0
  117. package/src/shared/completion-owner.d.ts +5 -0
  118. package/src/shared/completion-owner.js +5 -0
  119. package/src/shared/session-identity.d.ts +2 -0
  120. package/src/shared/session-identity.js +26 -0
  121. package/src/shared/types.d.ts +45 -6
  122. package/src/tui/fleet.d.ts +1 -0
  123. package/src/tui/fleet.js +24 -15
  124. package/src/tui/render.d.ts +6 -0
  125. package/src/tui/render.js +3 -3
  126. package/src/tui/subagent-messages.js +1 -0
  127. package/src/workflows/scripted-workflow.d.ts +1 -1
  128. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  129. package/src/workflows/workflow-auto-resume.js +102 -0
  130. package/src/workflows/workflow-launch-record.d.ts +27 -0
  131. package/src/workflows/workflow-launch-record.js +48 -0
  132. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  133. package/src/workflows/workflow-resume-claim.js +210 -0
  134. package/src/workflows/workflow-resume.d.ts +56 -0
  135. package/src/workflows/workflow-resume.js +106 -0
  136. package/src/workflows/workflow-reuse.d.ts +15 -9
  137. package/src/workflows/workflow-reuse.js +77 -14
  138. package/src/workflows/workflow-revival.d.ts +2 -0
  139. package/src/workflows/workflow-revival.js +2 -1
  140. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  141. package/src/workflows/workflow-stop-cause.js +8 -0
@@ -0,0 +1,327 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as path from "node:path";
3
+ import { Type } from "typebox";
4
+ import { Compile } from "typebox/compile";
5
+ import { remoteShellCommand, sshMachineAccess } from "./herdr-connection.js";
6
+ import { localMachineAccess } from "./local-machine-access.js";
7
+ import { getPiSpawnCommand } from "./pi-spawn.js";
8
+ import { createPlacedRuntimeDir, PLACED_MANIFEST_READ_SCRIPT, PlacedRunOwner, removePlacedRuntimeDir } from "./placed-run-owner.js";
9
+ import { withHerdrPaneAllocationLock } from "./placement-host.js";
10
+ import { isRexLocalMachine, REX_BUNDLE_BIN, REX_CALL_TIMEOUT_MS, REX_EXIT_GONE, REX_EXIT_UNREACHABLE, REX_MAX_OUTPUT, resolveRexBinary, rexCommandEnv, rexGlobalArgs } from "./rex-machine.js";
11
+ const REX_CLOSE_POLLS = 20, REX_CLOSE_POLL_MS = 50;
12
+ /** On a Rex SSH machine, placed tabs open in the one session with this label. */
13
+ export const REX_SSH_SESSION_LABEL = "subagents";
14
+ const sessionInspection = Compile(Type.Object({ session_id: Type.String() }));
15
+ const sessionList = Compile(Type.Object({ sessions: Type.Array(Type.Object({ session_id: Type.String(), label: Type.Optional(Type.String()) })) }));
16
+ const createdWindow = Compile(Type.Object({ window_id: Type.String(), block_ids: Type.Tuple([Type.String()]) }));
17
+ const sessionWindows = Compile(Type.Object({ windows: Type.Array(Type.Object({ window_id: Type.String(), label: Type.Optional(Type.String()), layers: Type.Array(Type.Object({ blocks: Type.Array(Type.Object({ block_id: Type.String() })) })) })) }));
18
+ const blockProcess = Compile(Type.Object({ foreground: Type.Object({ pid: Type.Integer() }) }));
19
+ const bridgeManifestPid = Compile(Type.Object({ pid: Type.Integer() }));
20
+ /** Rex runtime directories go under /tmp: the bridge socket path inside a macOS TMPDIR exceeds the unix socket path limit. */
21
+ function rexAccess() { return localMachineAccess({ env: { ...rexCommandEnv(), TMPDIR: "/tmp" } }); }
22
+ /** The same /tmp rule on the remote, whatever TMPDIR its SSH session has. */
23
+ function rexSshAccess(machine, access = sshMachineAccess(machine)) { return { ...access, shellCommand: (script, args) => remoteShellCommand(`TMPDIR=/tmp; export TMPDIR; ${script}`, args) }; }
24
+ function asRexMachine(machine) { if (machine.provider !== "rex")
25
+ throw new Error("The Rex pane host only places runs on Rex machines."); return machine; }
26
+ /** Runs one rex command on the pane machine with --autostart=false, the recorded server, and no inherited Rex selectors; exit 4 always throws. */
27
+ async function runRex(endpoint, args, what) {
28
+ const result = await endpoint.access.runAsync(endpoint.access.shellCommand('exec /usr/bin/env -u REX_SESSION -u REX_BLOCK -u REX_SERVER "$@"', [endpoint.bin, ...rexGlobalArgs(endpoint.server), ...args]), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: REX_MAX_OUTPUT });
29
+ if (result.error)
30
+ throw new Error(`Rex ${what} failed: ${result.error.message}`, { cause: result.error });
31
+ if (result.status === REX_EXIT_UNREACHABLE)
32
+ throw new Error(`The Rex server is unreachable (rex ${what} exited 4); the placed pane's state is unknown.`);
33
+ if (result.status === null)
34
+ throw new Error(`Rex ${what} did not finish; the placed pane's state is unknown.`);
35
+ return { ...result, status: result.status };
36
+ }
37
+ function requireRexSuccess(result, what) {
38
+ if (result.status === REX_EXIT_GONE)
39
+ throw new Error(`Rex ${what} failed because its target no longer exists (exit 3): ${result.stderr.trim().slice(0, 512)}`);
40
+ if (result.status !== 0)
41
+ throw new Error(`Rex ${what} exited with code ${result.status}: ${result.stderr.trim().slice(0, 512)}`);
42
+ }
43
+ /** Parses rex's JSON output against its schema; undefined when the shape does not match. */
44
+ function parseRexJson(text, validator, what) {
45
+ let value;
46
+ try {
47
+ value = JSON.parse(text);
48
+ }
49
+ catch {
50
+ throw new Error(`Rex ${what} returned malformed JSON.`);
51
+ }
52
+ return validator.Check(value) ? value : undefined;
53
+ }
54
+ /** One remote lookup, matching resolveRexBinary: `$REX_BIN_DIR/rex`, then `rex` on PATH, then the Rex server bundle under the remote home. */
55
+ async function resolveRemoteRexBinary(access, machine) {
56
+ const script = `for c in "\${REX_BIN_DIR:+$REX_BIN_DIR/rex}" "$(command -v rex 2>/dev/null)" "$HOME/${REX_BUNDLE_BIN}"; do if [ -n "$c" ] && [ -f "$c" ] && [ -x "$c" ]; then printf '%s\\n' "$c"; exit 0; fi; done; exit 1`;
57
+ const found = await access.runAsync(access.shellCommand(script), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
58
+ if (found.error)
59
+ throw new Error(`Could not look for the Rex CLI on ${machine.id}: ${found.error.message}`, { cause: found.error });
60
+ const bin = found.stdout.trim();
61
+ if (found.status !== 0 || !path.posix.isAbsolute(bin) || bin.includes("\n"))
62
+ throw new Error(`The Rex CLI was not found on ${machine.id}. Install Rex there or put rex on its PATH.`);
63
+ return bin;
64
+ }
65
+ /** The pane's cwd as the remote Pi will report it: `~` expanded and symlinks resolved. Rex does not expand `~` itself. */
66
+ async function resolveRemoteCwd(access, machine) {
67
+ const script = `case "$1" in "~") d=$HOME;; "~/"*) d=$HOME/\${1#"~/"};; *) d=$1;; esac; cd -P -- "$d" 2>/dev/null && pwd -P`;
68
+ const resolved = await access.runAsync(access.shellCommand(script, [machine.cwd]), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
69
+ if (resolved.error)
70
+ throw new Error(`Could not check ${machine.cwd} on ${machine.id}: ${resolved.error.message}`, { cause: resolved.error });
71
+ const cwd = resolved.stdout.trim();
72
+ if (resolved.status !== 0 || !path.posix.isAbsolute(cwd) || cwd.includes("\n"))
73
+ throw new Error(`Nothing at ${machine.cwd} on ${machine.id}. Clone the repo there first; pi-subagents never clones, pulls, or checks out on a machine.`);
74
+ return cwd;
75
+ }
76
+ /** The remote Pi as a Rex tab's login environment finds it. */
77
+ async function resolveRemotePi(access, machine) {
78
+ const found = await access.runAsync(access.shellCommand(`exec "\${SHELL:-/bin/sh}" -lc 'command -v pi'`), { timeout: REX_CALL_TIMEOUT_MS, maxBuffer: 64 * 1024 });
79
+ if (found.error)
80
+ throw new Error(`Could not look for Pi on ${machine.id}: ${found.error.message}`, { cause: found.error });
81
+ const pi = found.stdout.trim().split("\n").at(-1)?.trim() ?? "";
82
+ if (found.status !== 0 || !path.posix.isAbsolute(pi))
83
+ throw new Error(`Pi was not found on ${machine.id}: its login shell has no pi on PATH. Install Pi there.`);
84
+ return pi;
85
+ }
86
+ async function labeledSessions(endpoint) {
87
+ const listed = await runRex(endpoint, ["ls", "--json"], "ls");
88
+ requireRexSuccess(listed, "ls");
89
+ const parsed = parseRexJson(listed.stdout, sessionList, "ls");
90
+ if (!parsed)
91
+ throw new Error("Rex ls did not list the server's sessions.");
92
+ return parsed.sessions.filter((session) => session.label === REX_SSH_SESSION_LABEL).map((session) => session.session_id);
93
+ }
94
+ /**
95
+ * The session labeled `subagents` on the SSH machine, created when there is none. Rex allows duplicate labels, so two or
96
+ * more fail closed, also after creating one. Only launch calls this; reconnect and cleanup use the recorded session id.
97
+ */
98
+ async function subagentsSession(endpoint, machine) {
99
+ return withHerdrPaneAllocationLock(JSON.stringify(["rex-session", machine.target, REX_SSH_SESSION_LABEL]), async () => {
100
+ let sessions = await labeledSessions(endpoint);
101
+ if (sessions.length === 0) {
102
+ requireRexSuccess(await runRex(endpoint, ["new", REX_SSH_SESSION_LABEL, "--json"], "new"), "new");
103
+ sessions = await labeledSessions(endpoint);
104
+ }
105
+ const [session] = sessions;
106
+ if (sessions.length > 1)
107
+ throw new Error(`Rex on ${machine.id} has ${sessions.length} sessions labeled ${REX_SSH_SESSION_LABEL}; close or rename the extras so placed runs have one session to open in.`);
108
+ if (!session)
109
+ throw new Error(`Rex on ${machine.id} did not list the ${REX_SSH_SESSION_LABEL} session it just created.`);
110
+ return session;
111
+ });
112
+ }
113
+ /** The server must confirm the recorded session; a missing session fails closed. A Rex SSH machine's session is the one recorded at launch. */
114
+ export async function connectRex(machine, options = {}) {
115
+ const rex = asRexMachine(machine);
116
+ let endpoint;
117
+ if (isRexLocalMachine(rex)) {
118
+ endpoint = { access: options.access ?? rexAccess(), bin: options.bin ?? resolveRexBinary(), session: rex.session };
119
+ if (rex.server)
120
+ endpoint.server = rex.server;
121
+ }
122
+ else {
123
+ if (!options.session)
124
+ throw new Error(`Rex machine '${rex.id}' has no recorded session; a placed run never picks one by label after launch.`);
125
+ const access = options.access ?? rexSshAccess(rex);
126
+ endpoint = { access, bin: options.bin ?? await resolveRemoteRexBinary(access, rex), session: options.session };
127
+ }
128
+ const inspected = await runRex(endpoint, ["session", "inspect", endpoint.session, "--json"], "session inspect");
129
+ if (inspected.status === REX_EXIT_GONE)
130
+ throw new Error(`Rex session ${endpoint.session} no longer exists on the Rex server.`);
131
+ requireRexSuccess(inspected, "session inspect");
132
+ if (parseRexJson(inspected.stdout, sessionInspection, "session inspect")?.session_id !== endpoint.session)
133
+ throw new Error("Rex session identity changed: the server reported a different session id.");
134
+ return { ...endpoint, ...endpoint.access.openSockets() };
135
+ }
136
+ async function inspectBlock(connection, blockId) {
137
+ const inspected = await runRex(connection, ["block", "inspect", "-s", connection.session, blockId], "block inspect");
138
+ if (inspected.status === REX_EXIT_GONE)
139
+ return "gone";
140
+ requireRexSuccess(inspected, "block inspect");
141
+ return "present";
142
+ }
143
+ /** Ownership is the never-reused block id in the recorded session. The block counts as closed only once `block inspect` exits 3. */
144
+ export async function closeOwnedRexPane(connection, owned) {
145
+ if (owned.workspaceId !== connection.session)
146
+ throw new Error("Refusing Rex cleanup: the owned pane belongs to a different Rex session.");
147
+ if (await inspectBlock(connection, owned.paneId) === "gone")
148
+ return;
149
+ const closed = await runRex(connection, ["block", "close", "-s", connection.session, owned.paneId], "block close");
150
+ if (closed.status !== REX_EXIT_GONE)
151
+ requireRexSuccess(closed, "block close");
152
+ for (let poll = 0; poll < REX_CLOSE_POLLS; poll++) {
153
+ if (await inspectBlock(connection, owned.paneId) === "gone")
154
+ return;
155
+ await new Promise((resolve) => setTimeout(resolve, REX_CLOSE_POLL_MS));
156
+ }
157
+ throw new Error(`Rex block ${owned.paneId} still exists after close; refusing to report the placed pane closed.`);
158
+ }
159
+ function reconcileRexPlacedRun(identity, candidate, proveEvidence) {
160
+ if (!candidate.present)
161
+ return { ok: false, reason: "Owned Rex block is gone." };
162
+ if (candidate.foregroundPid === undefined || candidate.foregroundPid !== candidate.bridgePid)
163
+ return { ok: false, reason: "The owned Rex block's foreground process is not the placed Pi." };
164
+ const proof = proveEvidence();
165
+ if (!proof.ok)
166
+ return proof;
167
+ return { ok: true, state: "unknown", paneId: identity.paneId, cursor: proof.cursor };
168
+ }
169
+ async function readBridgePid(connection, runtimeDir) {
170
+ const manifest = await connection.access.runAsync(connection.access.shellCommand(PLACED_MANIFEST_READ_SCRIPT, [runtimeDir]), { timeout: 5_000, maxBuffer: 64 * 1024 });
171
+ if (manifest.status !== 0)
172
+ return undefined;
173
+ return parseRexJson(manifest.stdout, bridgeManifestPid, "bridge manifest")?.pid;
174
+ }
175
+ /**
176
+ * `window new` failed, timed out, or printed something unreadable, so the server may still have opened this run's window.
177
+ * Find it by the run's tab label in the recorded session and close its blocks with the same close-then-prove-gone rule.
178
+ */
179
+ async function closeUntrackedRexWindow(connection, label, failure) {
180
+ try {
181
+ const inspected = await runRex(connection, ["session", "inspect", connection.session, "--json"], "session inspect");
182
+ if (inspected.status === REX_EXIT_GONE)
183
+ throw failure;
184
+ requireRexSuccess(inspected, "session inspect");
185
+ const windows = parseRexJson(inspected.stdout, sessionWindows, "session inspect")?.windows.filter((window) => window.label === label);
186
+ if (!windows)
187
+ throw new Error("Rex session inspect did not list the session's windows.");
188
+ if (windows.length > 1)
189
+ throw new Error(`The Rex session has ${windows.length} windows labeled ${label}; refusing to guess which one this run opened.`);
190
+ for (const window of windows)
191
+ for (const layer of window.layers)
192
+ for (const block of layer.blocks)
193
+ await closeOwnedRexPane(connection, { workspaceId: connection.session, tabId: window.window_id, paneId: block.block_id });
194
+ }
195
+ catch (cleanupError) {
196
+ if (cleanupError === failure)
197
+ throw failure;
198
+ throw new AggregateError([failure, cleanupError], `Rex window new failed, and the window labeled ${label} that it may have opened could not be closed.`, { cause: failure });
199
+ }
200
+ throw failure;
201
+ }
202
+ /** Opens the run's background window; on any failure, closes the window the server may have opened before rethrowing. */
203
+ async function openRexWindow(connection, label, cwd, argv) {
204
+ try {
205
+ const created = await runRex(connection, ["window", "new", "-s", connection.session, label, "-c", cwd, "--focus=false", "--json", "--", ...argv], "window new");
206
+ requireRexSuccess(created, "window new");
207
+ const window = parseRexJson(created.stdout, createdWindow, "window new");
208
+ if (!window)
209
+ throw new Error(`Rex window new did not return exactly one block: ${created.stdout.trim().slice(0, 512)}`);
210
+ return window;
211
+ }
212
+ catch (error) {
213
+ return closeUntrackedRexWindow(connection, label, error instanceof Error ? error : new Error(String(error)));
214
+ }
215
+ }
216
+ /**
217
+ * Rex pane host: rex CLI calls on this machine, always with an explicit session. A local child opens as a background
218
+ * window in the parent's session; a Rex SSH child in the remote `subagents` session, with every rex call run over SSH.
219
+ * Pi starts through the login shell's environment (as Rex starts any window) with the bridge variables set by
220
+ * /usr/bin/env. There is no status subscription: a dead Pi closes the bridge socket (directly, or through the SSH
221
+ * forward), and the reconnect that follows checks the block with `rex block call process` and fails closed.
222
+ */
223
+ export const rexPaneHost = {
224
+ connect: connectRex,
225
+ sessionOf(connection) { return connection.session; },
226
+ async launch(connection, machine, runId, _runtimeDir, request, onPane) {
227
+ if (request.kind !== "pi")
228
+ throw new Error("Rex supports native Pi children only.");
229
+ const environment = Object.entries(request.environment ?? {});
230
+ if (environment.some(([name]) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)))
231
+ throw new Error("Rex launch environment names must be shell variable names.");
232
+ const rex = asRexMachine(machine);
233
+ let pi;
234
+ if (isRexLocalMachine(rex)) {
235
+ pi = getPiSpawnCommand(request.args);
236
+ if (!path.isAbsolute(pi.command))
237
+ throw new Error("Rex placement needs an absolute Pi executable; set PI_SUBAGENT_PI_BINARY to the Pi binary path.");
238
+ }
239
+ else
240
+ pi = { command: await resolveRemotePi(connection.access, rex), args: request.args };
241
+ const window = await openRexWindow(connection, `subagent-${runId.slice(-8)}`, rex.cwd, ["/usr/bin/env", ...environment.map(([name, value]) => `${name}=${value}`), pi.command, ...pi.args]);
242
+ const owned = { workspaceId: connection.session, tabId: window.window_id, paneId: window.block_ids[0] };
243
+ await onPane(owned);
244
+ return { owned, terminalId: owned.paneId };
245
+ },
246
+ async prompt(connection, identity, text) {
247
+ requireRexSuccess(await runRex(connection, ["send", "-s", connection.session, "-b", identity.paneId, "--", text], "send"), "send");
248
+ requireRexSuccess(await runRex(connection, ["send-key", "-s", connection.session, "-b", identity.paneId, "enter"], "send-key"), "send-key");
249
+ },
250
+ async subscribe() { return () => { }; },
251
+ paneEvent() { return undefined; },
252
+ async panes(connection, identity) {
253
+ const processInfo = await runRex(connection, ["block", "call", "-s", connection.session, "-b", identity.paneId, "process"], "block process");
254
+ if (processInfo.status === REX_EXIT_GONE)
255
+ return { present: false };
256
+ requireRexSuccess(processInfo, "block process");
257
+ const candidate = { present: true };
258
+ const foregroundPid = parseRexJson(processInfo.stdout, blockProcess, "block process")?.foreground.pid;
259
+ if (foregroundPid !== undefined)
260
+ candidate.foregroundPid = foregroundPid;
261
+ const bridgePid = await readBridgePid(connection, identity.runtimeDir);
262
+ if (bridgePid !== undefined)
263
+ candidate.bridgePid = bridgePid;
264
+ return candidate;
265
+ },
266
+ reconcile: reconcileRexPlacedRun,
267
+ closeOwnedPane: closeOwnedRexPane,
268
+ };
269
+ function runtimeDirPresent(access, runtimeDir) {
270
+ const result = access.run(access.shellCommand('test -e "$1" || test -L "$1"', [runtimeDir]), { timeout: 10_000, maxBuffer: 4096 });
271
+ if (result.error)
272
+ throw new Error(`Could not check the owned runtime directory: ${result.error.message}`, { cause: result.error });
273
+ return result.status === 0;
274
+ }
275
+ /** A closed local Pi removes its own runtime directory on shutdown, so already-removed counts as removed. */
276
+ function removeRexRuntimeDir(access, runtimeDir) {
277
+ if (!runtimeDirPresent(access, runtimeDir))
278
+ return;
279
+ try {
280
+ removePlacedRuntimeDir(access, runtimeDir);
281
+ }
282
+ catch (error) {
283
+ if (runtimeDirPresent(access, runtimeDir))
284
+ throw error;
285
+ }
286
+ }
287
+ /** The placed-run owner bound to Rex and the pane machine's access. */
288
+ export class RexPlacedRunOwner extends PlacedRunOwner {
289
+ constructor(machine, runId, runtimeDir, connection, access, host) { super(machine, runId, runtimeDir, connection, host, access, () => removeRexRuntimeDir(access, runtimeDir)); }
290
+ /** A Rex SSH owner's machine carries the resolved remote cwd, and its host reconnects only to the session recorded here. */
291
+ static async create(machine, runId, options = {}) {
292
+ // A short hash names the directory, so /tmp/pi-subagents-herdr-<hash>-XXXXXXXX/bridge.sock (57 bytes) fits the 103-byte macOS socket path limit for any run id.
293
+ const hash = createHash("sha256").update(runId).digest("hex").slice(0, 12);
294
+ if (isRexLocalMachine(machine)) {
295
+ const access = rexAccess(), runtimeDir = await createPlacedRuntimeDir(access, hash);
296
+ try {
297
+ return new RexPlacedRunOwner(machine, runId, runtimeDir, await connectRex(machine, { access }), access, rexPaneHost);
298
+ }
299
+ catch (error) {
300
+ try {
301
+ removeRexRuntimeDir(access, runtimeDir);
302
+ }
303
+ catch (cleanupError) {
304
+ throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
305
+ }
306
+ throw error;
307
+ }
308
+ }
309
+ const access = rexSshAccess(machine, options.sshAccess), bin = await resolveRemoteRexBinary(access, machine);
310
+ const placed = { ...machine, cwd: await resolveRemoteCwd(access, machine) };
311
+ const session = await subagentsSession({ access, bin }, placed);
312
+ const host = { ...rexPaneHost, connect: (target) => connectRex(target, { access, bin, session }) };
313
+ const runtimeDir = await createPlacedRuntimeDir(access, hash);
314
+ try {
315
+ return new RexPlacedRunOwner(placed, runId, runtimeDir, await host.connect(placed), access, host);
316
+ }
317
+ catch (error) {
318
+ try {
319
+ removeRexRuntimeDir(access, runtimeDir);
320
+ }
321
+ catch (cleanupError) {
322
+ throw new AggregateError([error, cleanupError], "Rex owner creation failed and runtime cleanup also failed.", { cause: error });
323
+ }
324
+ throw error;
325
+ }
326
+ }
327
+ }
@@ -1,2 +1,7 @@
1
+ /**
2
+ * The completion owner of a result whose own owner process is dead: whichever session owns the run
3
+ * delivers it. Delivery consumes the result.
4
+ */
5
+ export declare const OWNING_SESSION_COMPLETION_OWNER = "owning-session";
1
6
  /** Stable for one parent Pi process across extension reloads. */
2
7
  export declare function currentCompletionOwnerId(): string;
@@ -1,5 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  const COMPLETION_OWNER_KEY = Symbol.for("pi-subagents.completion-owner-id");
3
+ /**
4
+ * The completion owner of a result whose own owner process is dead: whichever session owns the run
5
+ * delivers it. Delivery consumes the result.
6
+ */
7
+ export const OWNING_SESSION_COMPLETION_OWNER = "owning-session";
3
8
  /** Stable for one parent Pi process across extension reloads. */
4
9
  export function currentCompletionOwnerId() {
5
10
  const runtime = globalThis;
@@ -3,4 +3,6 @@ interface SessionIdentityManager {
3
3
  getSessionId(): string | null | undefined;
4
4
  }
5
5
  export declare function resolveCurrentSessionId(sessionManager: SessionIdentityManager): string;
6
+ /** The id in a Pi session file's header, which `pi --session <id>` resolves; only ids that need no shell quoting. */
7
+ export declare function resolvePiSessionId(sessionFile: string | undefined): string | undefined;
6
8
  export {};
@@ -1,6 +1,32 @@
1
+ import * as fs from "node:fs";
1
2
  export function resolveCurrentSessionId(sessionManager) {
2
3
  const sessionId = sessionManager.getSessionFile() ?? sessionManager.getSessionId();
3
4
  if (!sessionId)
4
5
  throw new Error("Current session identity is unavailable.");
5
6
  return sessionId;
6
7
  }
8
+ /** The id in a Pi session file's header, which `pi --session <id>` resolves; only ids that need no shell quoting. */
9
+ export function resolvePiSessionId(sessionFile) {
10
+ if (!sessionFile)
11
+ return undefined;
12
+ try {
13
+ const fd = fs.openSync(sessionFile, "r");
14
+ try {
15
+ const buffer = Buffer.alloc(16 * 1024);
16
+ const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
17
+ const line = buffer.toString("utf-8", 0, bytes).split("\n", 1)[0];
18
+ if (!line)
19
+ return undefined;
20
+ const header = JSON.parse(line);
21
+ if (header.type === "session" && typeof header.id === "string" && /^[A-Za-z0-9-]{8,}$/.test(header.id))
22
+ return header.id;
23
+ return undefined;
24
+ }
25
+ finally {
26
+ fs.closeSync(fd);
27
+ }
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
@@ -16,6 +16,8 @@ import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
16
16
  import type { WorkflowChildPermitContext } from "./workflow-child-permit.ts";
17
17
  import type { WatchdogWarningDetails } from "../watchdog/types.ts";
18
18
  import type { RequiredChildExtensionSnapshot } from "./required-child-extensions.ts";
19
+ import type { RecordedPid } from "../runs/background/stale-run-reconciler.ts";
20
+ import type { WorkflowStopCause } from "../workflows/workflow-stop-cause.ts";
19
21
  export interface MaxOutputConfig {
20
22
  bytes?: number;
21
23
  lines?: number;
@@ -1166,7 +1168,7 @@ export interface SingleResult {
1166
1168
  model?: string;
1167
1169
  /** Authoritative before/after Git evidence captured by a pane-native remote machine. */
1168
1170
  nativeMachine?: {
1169
- provider: "herdr";
1171
+ provider: PlacementMachineReference["provider"];
1170
1172
  machineId: string;
1171
1173
  initialGit?: HerdrRemoteGitStatus;
1172
1174
  finalGit?: HerdrRemoteGitStatus;
@@ -1431,10 +1433,17 @@ export interface Details {
1431
1433
  argsDigest?: string;
1432
1434
  /** SHA-256 of the workflow script source (async workflows). */
1433
1435
  scriptDigest?: string;
1434
- /** Structured stop cause; set only when the owning extension runtime was replaced. */
1435
- stopCause?: "runtime-replaced";
1436
- /** Runtime-replaced workflow run whose finished children this run reused. */
1436
+ /** Structured stop cause: `runtime-replaced` when the owning extension runtime was replaced; `owner-exited` when stale-run reconciliation found the owning Pi process dead. */
1437
+ stopCause?: WorkflowStopCause;
1438
+ /** Interrupted workflow run whose finished children this run reused. */
1437
1439
  reusedFrom?: string;
1440
+ /** Owner-exited runs: the last time the dead Pi process updated the run (epoch ms), not the repair time. */
1441
+ ownerExitedAt?: number;
1442
+ /** Set on a run that resumes an interrupted one; `attempt` counts resumes that followed Pi exits. */
1443
+ resumeOrigin?: {
1444
+ sourceRunId: string;
1445
+ attempt: number;
1446
+ };
1438
1447
  resource?: WorkflowResourceProvenance;
1439
1448
  preflightWarnings?: string[];
1440
1449
  trace: Array<{
@@ -1450,7 +1459,7 @@ export interface Details {
1450
1459
  generatedLaneKey?: string;
1451
1460
  warning?: string;
1452
1461
  error?: string;
1453
- /** Came from a previous runtime-replaced run of the same script (and args, unless named by options.reuseFrom); this run launched nothing. */
1462
+ /** Came from a previous runtime-replaced or owner-exited run of the same script (and args, unless named by options.reuseFrom); this run launched nothing. */
1454
1463
  reused?: boolean;
1455
1464
  }>;
1456
1465
  emits: unknown[];
@@ -1673,6 +1682,24 @@ export interface HerdrMachineReference {
1673
1682
  session?: string;
1674
1683
  cwd: string;
1675
1684
  }
1685
+ /** `machine: "local"`: a Rex pane in the parent's own Rex session on this machine. `session` is the Rex session id and `server` the Rex server address, both recorded in the parent at resolution time; `cwd` is the local launch directory. */
1686
+ export interface RexLocalMachineReference {
1687
+ provider: "rex";
1688
+ id: "local";
1689
+ session: string;
1690
+ server?: string;
1691
+ cwd: string;
1692
+ }
1693
+ /** A Rex machine reached over SSH, named in user settings (`subagents.machines.<id>.rex.ssh`). The pane opens in that machine's Rex session labeled `subagents`, found or created at launch and recorded with the run; `cwd` is the directory on that machine. */
1694
+ export interface RexSshMachineReference {
1695
+ provider: "rex";
1696
+ id: string;
1697
+ target: string;
1698
+ cwd: string;
1699
+ }
1700
+ export type RexMachineReference = RexLocalMachineReference | RexSshMachineReference;
1701
+ /** Where a placed native Pi child runs. */
1702
+ export type PlacementMachineReference = HerdrMachineReference | RexMachineReference;
1676
1703
  export interface ExternalCliMachineStatus extends HerdrMachineReference {
1677
1704
  remoteGit?: HerdrRemoteGitStatus;
1678
1705
  }
@@ -1824,6 +1851,8 @@ export interface AsyncStatus {
1824
1851
  runId: string;
1825
1852
  /** Parent Pi process/window that owns local completion delivery. */
1826
1853
  completionOwnerId?: string;
1854
+ /** The Pi process behind `completionOwnerId`; once it is dead, the session that owns the run delivers it. */
1855
+ completionOwnerProcess?: RecordedPid;
1827
1856
  /** Host tool-call id retained when it differs from the internal run id. */
1828
1857
  toolCallId?: string;
1829
1858
  sessionId?: string;
@@ -1860,6 +1889,8 @@ export interface AsyncStatus {
1860
1889
  pid?: number;
1861
1890
  /** Linux PID namespace identity used to scope liveness probes. */
1862
1891
  pidNamespaceScope?: string;
1892
+ /** Boot id of the machine when `pid` was recorded; a different current boot id proves the pid dead. */
1893
+ bootId?: string;
1863
1894
  cwd?: string;
1864
1895
  /** User-configured launcher that wrapped the background runner (argv only, never environment). */
1865
1896
  launcher?: RunnerLauncher;
@@ -2135,6 +2166,10 @@ export interface ForegroundResumeRun {
2135
2166
  sessionId?: string;
2136
2167
  updatedAt: number;
2137
2168
  children: ForegroundResumeChild[];
2169
+ /** Pi process running this run; only on records whose child is `running`. */
2170
+ owner?: RecordedPid;
2171
+ /** Workflow that launched this foreground child. */
2172
+ parentWorkflowRunId?: string;
2138
2173
  }
2139
2174
  export interface ForegroundChildControl {
2140
2175
  index: number;
@@ -2318,6 +2353,8 @@ export interface SubagentState {
2318
2353
  workflowControllers?: Map<string, AbortController>;
2319
2354
  /** Live in-process workflow child stoppers keyed by parent workflow run id. */
2320
2355
  workflowChildStops?: Map<string, (childId: string, message?: string) => boolean>;
2356
+ /** Background children awaited by a blocking workflow call: their parent cannot answer supervisor asks. */
2357
+ blockingWorkflowChildren?: Set<string>;
2321
2358
  }
2322
2359
  export interface HerdrProjectPaneSnapshot {
2323
2360
  projectRoot: string;
@@ -2422,7 +2459,7 @@ export interface RunSyncOptions {
2422
2459
  context?: "fresh" | "fork";
2423
2460
  cwd?: string;
2424
2461
  /** Resolved pane-native saved-machine placement. */
2425
- machine?: HerdrMachineReference;
2462
+ machine?: PlacementMachineReference;
2426
2463
  /** Explicit read override resolved by the remote ambient agent profile. */
2427
2464
  remoteReads?: string[] | false;
2428
2465
  /** Original cwd input retained for launch diagnostics. */
@@ -2583,6 +2620,8 @@ export interface ActiveAsyncCapacityConfig {
2583
2620
  }
2584
2621
  export interface ExtensionConfig {
2585
2622
  asyncByDefault?: boolean;
2623
+ /** Relaunch this session's interrupted async workflows when the session is reopened or reloaded (default true). */
2624
+ autoResume?: boolean;
2586
2625
  /** Set the context for launches that omit an explicit context. */
2587
2626
  defaultSubagentContext?: "fresh" | "fork";
2588
2627
  /** Configure how every resolved fork session is prepared before child spawn. */
@@ -46,6 +46,7 @@ export type FleetItem = ({
46
46
  updatedAt: number;
47
47
  run: AsyncRunSummary;
48
48
  step?: AsyncStep;
49
+ task?: string;
49
50
  } | {
50
51
  key: string;
51
52
  kind: "external";
package/src/tui/fleet.js CHANGED
@@ -16,7 +16,9 @@ import { steerAsyncRun } from "../runs/foreground/async-steering-action.js";
16
16
  import { stopAsyncRun } from "../runs/foreground/async-stop-action.js";
17
17
  import { resolveWorkflowForegroundSteeringTarget, steerWorkflowForegroundTarget } from "../runs/foreground/workflow-foreground-steering.js";
18
18
  import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.js";
19
+ import { foregroundChildStateLabel } from "../runs/foreground/foreground-history.js";
19
20
  import { FLEET_STATUS_WIDGET_KEY, reportFleetWarning } from "./fleet-status.js";
21
+ import { stepTaskSummary } from "./render.js";
20
22
  import { readFleetTranscript, renderFleetTranscript } from "./fleet-transcript.js";
21
23
  import { runningTone } from "./running-tone.js";
22
24
  import { childThinkingLevel } from "../shared/model-info.js";
@@ -110,18 +112,22 @@ function asyncItems(run, description) {
110
112
  if (run.steps.length === 0 || run.mode === "workflow") {
111
113
  return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run, ...(description ? { description } : {}) }];
112
114
  }
113
- return run.steps.map((step) => ({
114
- key: `async:${run.id}:${step.index}`,
115
- kind: "async",
116
- runId: run.id,
117
- index: step.index,
118
- agent: step.label ? `${step.label} (${step.agent})` : step.agent,
119
- state: step.status,
120
- updatedAt: step.lastActivityAt ?? updatedAt,
121
- run,
122
- step,
123
- ...(description ? { description } : {}),
124
- }));
115
+ return run.steps.map((step) => {
116
+ const task = stepTaskSummary(step, step.agent);
117
+ return {
118
+ key: `async:${run.id}:${step.index}`,
119
+ kind: "async",
120
+ runId: run.id,
121
+ index: step.index,
122
+ agent: step.label ? `${step.label} (${step.agent})` : step.agent,
123
+ state: step.status,
124
+ updatedAt: step.lastActivityAt ?? updatedAt,
125
+ run,
126
+ step,
127
+ ...(task ? { task } : {}),
128
+ ...(description ? { description } : {}),
129
+ };
130
+ });
125
131
  }
126
132
  function orderFleetAsyncRuns(runs, terminalLimit, trackedJobs) {
127
133
  const updatedAt = (run) => run.lastUpdate ?? run.endedAt ?? run.startedAt;
@@ -445,7 +451,7 @@ function foregroundRecentDetail(item, state) {
445
451
  const lines = [
446
452
  `Run: ${item.runId}`,
447
453
  "Source: foreground",
448
- `State: ${child.status}`,
454
+ `State: ${foregroundChildStateLabel(run, child)}`,
449
455
  `Mode: ${run.mode}`,
450
456
  `Child: ${child.index} (${child.agent})${contextModeLabel(child.context) ? ` ${contextModeLabel(child.context)}` : ""}`,
451
457
  modelThinking ? `Model: ${modelThinking}` : undefined,
@@ -1226,8 +1232,11 @@ export class SubagentFleetComponent {
1226
1232
  const item = this.snapshot.items[index];
1227
1233
  const marker = index === this.selected ? this.theme.fg("accent", "›") : " ";
1228
1234
  const context = item.kind === "async" ? contextModeBadge(this.theme, item.step?.context ?? item.run.context) : item.kind === "foreground-recent" ? contextModeBadge(this.theme, item.child.context) : "";
1229
- const agent = index === this.selected ? this.theme.bold(item.agent) : item.agent;
1230
- const identity = item.runId.slice(0, 8);
1235
+ // A child's task tells rows of one run apart; the run id stays in the detail header.
1236
+ const task = item.kind === "async" ? item.task : undefined;
1237
+ const name = task && item.kind === "async" && item.step ? item.step.agent : item.agent;
1238
+ const agent = index === this.selected ? this.theme.bold(name) : name;
1239
+ const identity = task ?? item.runId.slice(0, 8);
1231
1240
  const left = `${marker} ${statusGlyph(item, this.theme)} ${agent}${context} ${this.theme.fg("dim", `· ${identity}`)}`;
1232
1241
  return rightAligned(left, this.theme.fg("dim", item.state), layout.roster.width);
1233
1242
  });
@@ -39,6 +39,12 @@ export interface AsyncLaneProjection {
39
39
  /** Project already-loaded async status facts into one bounded, render-only lane row. */
40
40
  export declare function projectAsyncLane(job: AsyncJobState, ...args: [selectedStep?: AsyncJobStep]): AsyncLaneProjection | undefined;
41
41
  export declare function widgetRenderKey(job: AsyncJobState, expanded?: boolean): string;
42
+ /** A child's task in one line: its label and task, else its session name without the repeated agent name. */
43
+ export declare function stepTaskSummary(step: {
44
+ description?: string;
45
+ label?: string;
46
+ sessionName?: string;
47
+ }, agentName: string | undefined): string | undefined;
42
48
  /** Structural identity of the workflow rows that Fleet can cover. */
43
49
  export declare function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string;
44
50
  /** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
package/src/tui/render.js CHANGED
@@ -1104,14 +1104,14 @@ function shouldCollapseSingleChildDetails(job, step) {
1104
1104
  return isCompletedWidgetStepStatus(step.status);
1105
1105
  return false;
1106
1106
  }
1107
- function singleChildTask(job, step) {
1107
+ /** A child's task in one line: its label and task, else its session name without the repeated agent name. */
1108
+ export function stepTaskSummary(step, agentName) {
1108
1109
  const task = compactTaskText(step.description, step.label);
1109
1110
  if (task)
1110
1111
  return task;
1111
1112
  const sessionName = step.sessionName?.trim();
1112
1113
  if (!sessionName)
1113
1114
  return undefined;
1114
- const agentName = singleChildAgentName(job, step);
1115
1115
  const sessionTask = stripRepeatedAgentPrefix(sessionName, agentName);
1116
1116
  return sessionTask === agentName ? undefined : compactTaskText(sessionTask);
1117
1117
  }
@@ -2223,7 +2223,7 @@ function foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index, tota
2223
2223
  const lane = projectAsyncLane(job, step);
2224
2224
  if (lane)
2225
2225
  lines.push(...formatLaneProjectionLines(lane, theme, detailIndent));
2226
- const task = collapseDetails ? singleChildTask(job, step) : compactTaskText(step.description, step.label);
2226
+ const task = collapseDetails ? stepTaskSummary(step, displayName) : compactTaskText(step.description, step.label);
2227
2227
  if (task)
2228
2228
  lines.push(`${detailIndent}${theme.fg("dim", `task: ${task}`)}`);
2229
2229
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
@@ -183,6 +183,7 @@ const MESSAGE_VIEWS = new Map([
183
183
  ["subagent-wait-subscription", readsContent("result", (message) => waitSubscriptionHeadline(message.details))],
184
184
  ["subagent-incremental-child-notify", readsContent("result", workflowChildHeadline)],
185
185
  ["subagent-workflow-result-write-failed", readsContent("result", () => ["workflow result ", { status: "save failed" }])],
186
+ ["subagent-auto-resume", readsContent("result", () => ["workflow auto-resume ", { status: "failed" }])],
186
187
  [SUBAGENT_CONTROL_MESSAGE_TYPE, readsContent("attention", (message) => controlNoticeHeadline(message.details))],
187
188
  [SUBAGENT_STEERING_MESSAGE_TYPE, readsContent("attention", (message) => steeringNoticeHeadline(message.details))],
188
189
  [SUBAGENT_WATCHDOG_WARNING_TYPE, (message) => watchdogWarningView(message.details, contentText(message.content))],