pi-subagents 0.80.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 (184) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-43SCHIMD.js +57 -0
  7. package/bundle/chunk-4QFZZV3O.js +57 -0
  8. package/bundle/chunk-4SDU2BZX.js +1190 -0
  9. package/bundle/chunk-52UBALJL.js +753 -0
  10. package/bundle/chunk-5LFLVSTM.js +260 -0
  11. package/bundle/chunk-5TYMSZX3.js +235 -0
  12. package/bundle/chunk-62OCY2S4.js +14 -0
  13. package/bundle/chunk-7F6RKT6W.js +132 -0
  14. package/bundle/chunk-7IVSUMQQ.js +1337 -0
  15. package/bundle/chunk-7J4ZXMY7.js +18 -0
  16. package/bundle/chunk-7PCKZHR5.js +1234 -0
  17. package/bundle/chunk-7YMXOYEU.js +1373 -0
  18. package/bundle/chunk-B5EKXDHE.js +16870 -0
  19. package/bundle/chunk-CIZQXJ5Q.js +8619 -0
  20. package/bundle/chunk-CTPAE36Y.js +94 -0
  21. package/bundle/chunk-D4EHT5SJ.js +14 -0
  22. package/bundle/chunk-GC4DSPMH.js +946 -0
  23. package/bundle/chunk-GJECHSAR.js +179 -0
  24. package/bundle/chunk-M2CTFYT3.js +4164 -0
  25. package/bundle/chunk-MHZ4G23A.js +1296 -0
  26. package/bundle/chunk-N2BN7BF5.js +159 -0
  27. package/bundle/chunk-NF7IEKBH.js +490 -0
  28. package/bundle/chunk-NGGAFDL6.js +742 -0
  29. package/bundle/chunk-NIXWDZZV.js +9 -0
  30. package/bundle/chunk-NRQ75MGH.js +8327 -0
  31. package/bundle/chunk-OYF6AFDY.js +458 -0
  32. package/bundle/chunk-PLCCBLPY.js +526 -0
  33. package/bundle/chunk-RMQFMMAT.js +416 -0
  34. package/bundle/chunk-SIJ4S3EN.js +276 -0
  35. package/bundle/chunk-UKG4V45O.js +228 -0
  36. package/bundle/chunk-VH5OAWVG.js +4096 -0
  37. package/bundle/chunk-W2SZ22EM.js +5462 -0
  38. package/bundle/chunk-WVD4UEL4.js +9367 -0
  39. package/bundle/chunk-XSWOTIA3.js +175 -0
  40. package/bundle/chunk-YFCK7H2S.js +129 -0
  41. package/bundle/chunk-YKEWIB4M.js +380 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZLINIYTP.js +8613 -0
  45. package/bundle/chunk-ZQGSDHL6.js +128 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/agents.md +31 -0
  59. package/docs/configuration.md +9 -1
  60. package/docs/extension-api.md +27 -0
  61. package/docs/observability.md +6 -2
  62. package/docs/tool-reference.md +3 -1
  63. package/docs/workflows.md +16 -0
  64. package/package.json +19 -19
  65. package/skills/pi-subagents/references/execution-controls.md +5 -4
  66. package/src/agents/agent-management.js +3 -3
  67. package/src/agents/agent-memory.js +3 -1
  68. package/src/agents/agents.js +6 -6
  69. package/src/agents/frontmatter.d.ts +4 -0
  70. package/src/agents/frontmatter.js +24 -3
  71. package/src/agents/skills.js +2 -1
  72. package/src/api/delegation.d.ts +20 -0
  73. package/src/extension/config.js +3 -0
  74. package/src/extension/herdr-pi-bridge.js +17 -10
  75. package/src/extension/index.js +109 -9
  76. package/src/extension/schemas.js +3 -3
  77. package/src/intercom/native-supervisor-channel.js +20 -0
  78. package/src/missions/lifecycle.js +7 -1
  79. package/src/runs/background/active-async-capacity.d.ts +1 -0
  80. package/src/runs/background/active-async-capacity.js +9 -3
  81. package/src/runs/background/async-execution.js +20 -10
  82. package/src/runs/background/async-job-tracker.d.ts +6 -1
  83. package/src/runs/background/async-job-tracker.js +9 -4
  84. package/src/runs/background/async-status.d.ts +2 -0
  85. package/src/runs/background/async-status.js +21 -2
  86. package/src/runs/background/await-async-run.js +3 -7
  87. package/src/runs/background/boot-id.d.ts +12 -0
  88. package/src/runs/background/boot-id.js +47 -0
  89. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  90. package/src/runs/background/interrupted-elsewhere.js +68 -0
  91. package/src/runs/background/notify.d.ts +2 -0
  92. package/src/runs/background/notify.js +13 -0
  93. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  94. package/src/runs/background/result-delivery-ownership.js +19 -1
  95. package/src/runs/background/result-watcher.d.ts +3 -1
  96. package/src/runs/background/result-watcher.js +24 -8
  97. package/src/runs/background/run-child-session.d.ts +1 -6
  98. package/src/runs/background/run-child-session.js +19 -1
  99. package/src/runs/background/run-status.js +2 -1
  100. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  101. package/src/runs/background/stale-run-reconciler.js +54 -13
  102. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  103. package/src/runs/background/subagent-runner.d.ts +4 -0
  104. package/src/runs/background/subagent-runner.js +13 -5
  105. package/src/runs/background/subagent-wait.d.ts +1 -1
  106. package/src/runs/background/subagent-wait.js +6 -1
  107. package/src/runs/background/terminal-run-index.d.ts +1 -0
  108. package/src/runs/background/terminal-run-index.js +4 -2
  109. package/src/runs/foreground/async-stop-action.js +3 -1
  110. package/src/runs/foreground/execution.js +45 -5
  111. package/src/runs/foreground/foreground-history.d.ts +16 -2
  112. package/src/runs/foreground/foreground-history.js +133 -31
  113. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  114. package/src/runs/foreground/subagent-executor.js +277 -74
  115. package/src/runs/shared/child-launch.d.ts +2 -2
  116. package/src/runs/shared/child-session.d.ts +3 -2
  117. package/src/runs/shared/child-session.js +51 -8
  118. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  119. package/src/runs/shared/child-tool-plan.js +21 -6
  120. package/src/runs/shared/herdr-connection.d.ts +17 -8
  121. package/src/runs/shared/herdr-connection.js +19 -7
  122. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  123. package/src/runs/shared/herdr-external-adapters.js +1 -1
  124. package/src/runs/shared/herdr-machine.d.ts +12 -2
  125. package/src/runs/shared/herdr-machine.js +55 -9
  126. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  127. package/src/runs/shared/herdr-pane-host.js +155 -0
  128. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  129. package/src/runs/shared/herdr-placed-run.js +48 -395
  130. package/src/runs/shared/local-machine-access.d.ts +5 -0
  131. package/src/runs/shared/local-machine-access.js +28 -0
  132. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  133. package/src/runs/shared/orca-progress-tabs.js +1 -24
  134. package/src/runs/shared/parallel-utils.d.ts +1 -1
  135. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  136. package/src/runs/shared/placed-run-owner.js +93 -0
  137. package/src/runs/shared/placement-host.d.ts +99 -0
  138. package/src/runs/shared/placement-host.js +157 -0
  139. package/src/runs/shared/rex-machine.d.ts +25 -0
  140. package/src/runs/shared/rex-machine.js +83 -0
  141. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  142. package/src/runs/shared/rex-pane-host.js +327 -0
  143. package/src/runs/shared/structured-output.d.ts +1 -0
  144. package/src/runs/shared/structured-output.js +1 -0
  145. package/src/shared/accessible-dir.js +4 -1
  146. package/src/shared/completion-owner.d.ts +5 -0
  147. package/src/shared/completion-owner.js +5 -0
  148. package/src/shared/provider-diagnostics.d.ts +9 -0
  149. package/src/shared/provider-diagnostics.js +75 -0
  150. package/src/shared/session-identity.d.ts +2 -0
  151. package/src/shared/session-identity.js +26 -0
  152. package/src/shared/types.d.ts +48 -6
  153. package/src/slash/delegation-adapters.d.ts +3 -1
  154. package/src/slash/delegation-adapters.js +7 -0
  155. package/src/slash/prompt-template-bridge.js +3 -1
  156. package/src/slash/subagents-admin.d.ts +1 -0
  157. package/src/slash/subagents-admin.js +1 -1
  158. package/src/tui/fleet-layout.d.ts +20 -0
  159. package/src/tui/fleet-layout.js +17 -0
  160. package/src/tui/fleet-viewport.d.ts +12 -0
  161. package/src/tui/fleet-viewport.js +35 -0
  162. package/src/tui/fleet.d.ts +6 -6
  163. package/src/tui/fleet.js +93 -66
  164. package/src/tui/render.d.ts +6 -0
  165. package/src/tui/render.js +3 -3
  166. package/src/tui/subagent-messages.js +1 -0
  167. package/src/workflows/scripted-workflow.d.ts +1 -1
  168. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  169. package/src/workflows/structured-workflow-scripts.js +5 -4
  170. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  171. package/src/workflows/workflow-auto-resume.js +102 -0
  172. package/src/workflows/workflow-launch-record.d.ts +27 -0
  173. package/src/workflows/workflow-launch-record.js +48 -0
  174. package/src/workflows/workflow-resources.js +2 -2
  175. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  176. package/src/workflows/workflow-resume-claim.js +210 -0
  177. package/src/workflows/workflow-resume.d.ts +56 -0
  178. package/src/workflows/workflow-resume.js +106 -0
  179. package/src/workflows/workflow-reuse.d.ts +15 -9
  180. package/src/workflows/workflow-reuse.js +77 -14
  181. package/src/workflows/workflow-revival.d.ts +2 -0
  182. package/src/workflows/workflow-revival.js +2 -1
  183. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  184. package/src/workflows/workflow-stop-cause.js +8 -0
@@ -0,0 +1,1337 @@
1
+ import {
2
+ isTrustedRecordedSessionFile,
3
+ resolveSubagentRunId
4
+ } from "./chunk-7PCKZHR5.js";
5
+ import {
6
+ agentStreamOptions,
7
+ opencodeSessionHeaders,
8
+ ownsRun,
9
+ resolveAsyncResumeTarget
10
+ } from "./chunk-VH5OAWVG.js";
11
+ import {
12
+ currentCompletionOwnerId,
13
+ listMissions,
14
+ missionRecordPath,
15
+ readMissionBinding,
16
+ reconcileAsyncRun,
17
+ resolveAuthorityDecision,
18
+ resolveMissionStoreLocation
19
+ } from "./chunk-NRQ75MGH.js";
20
+ import {
21
+ deliverInterruptRequest,
22
+ queueRevivalBrief,
23
+ requestAsyncSteer
24
+ } from "./chunk-OYF6AFDY.js";
25
+ import {
26
+ formatShellCommand
27
+ } from "./chunk-7J4ZXMY7.js";
28
+ import {
29
+ readStatus,
30
+ redactSecretValues,
31
+ resolveNodeExecutable,
32
+ tryLease,
33
+ writeAtomicJson,
34
+ writePrivateAtomicJson
35
+ } from "./chunk-CIZQXJ5Q.js";
36
+ import {
37
+ DIRS
38
+ } from "./chunk-5LFLVSTM.js";
39
+ import {
40
+ previewDisplayText,
41
+ safeTerminalText
42
+ } from "./chunk-YFCK7H2S.js";
43
+
44
+ // src/runs/background/steering.ts
45
+ import * as fs from "node:fs";
46
+ import * as path from "node:path";
47
+ var MAX_STEERING_REQUESTS = 20;
48
+ var STEERING_MESSAGE_PREVIEW_LIMIT = 160;
49
+ function steeringMessagePreview(message) {
50
+ return previewDisplayText(redactSecretValues(message), STEERING_MESSAGE_PREVIEW_LIMIT);
51
+ }
52
+ function steeringReceipt(message, receipt) {
53
+ const preview = steeringMessagePreview(message);
54
+ const longestFence = Math.max(2, ...[...preview.matchAll(/`{3,}/g)].map((match) => match[0].length));
55
+ const fence = "`".repeat(longestFence + 1);
56
+ return `${receipt}
57
+
58
+ Message sent:
59
+ ${fence}text
60
+ ${preview}
61
+ ${fence}`;
62
+ }
63
+ function createSteeringStatus() {
64
+ return { requested: 0, scheduled: 0, pending: 0, delivered: 0, failed: 0, recovered: 0, recent: [] };
65
+ }
66
+ function steeringStatus(status) {
67
+ return status.steering ?? createSteeringStatus();
68
+ }
69
+ function recordSteeringRequest(status, input) {
70
+ const existing = status.recent.find((request2) => request2.id === input.id);
71
+ if (existing) return existing;
72
+ const request = {
73
+ id: input.id,
74
+ requestedAt: input.requestedAt,
75
+ ...input.source ? { source: input.source } : {},
76
+ messagePreview: steeringMessagePreview(input.message),
77
+ targets: input.targets.map((target) => ({ index: target.index, state: target.state, ...target.reason ? { reason: target.reason } : {} }))
78
+ };
79
+ status.requested++;
80
+ status.lastRequestedAt = input.requestedAt;
81
+ status.recent = [...status.recent, request].slice(-MAX_STEERING_REQUESTS);
82
+ for (const target of request.targets) incrementStateCount(status, target.state);
83
+ return request;
84
+ }
85
+ function incrementStateCount(status, state) {
86
+ if (state === "scheduled") status.scheduled++;
87
+ else if (state === "routed" || state === "queued") status.pending++;
88
+ else if (state === "delivered" || state === "late") status.delivered++;
89
+ else if (state === "failed") status.failed++;
90
+ else if (state === "recovered") status.recovered++;
91
+ }
92
+ function updateSteeringTarget(status, requestId, index, state, now, fields = {}) {
93
+ const request = status.recent.find((candidate) => candidate.id === requestId);
94
+ const target = request?.targets.find((candidate) => candidate.index === index);
95
+ if (!target) return void 0;
96
+ if (state === "late" && target.state === "recovered") {
97
+ if (target.lateDeliveredAt === void 0) {
98
+ target.lateDeliveredAt = now;
99
+ status.delivered++;
100
+ status.lastDeliveredAt = now;
101
+ }
102
+ if (fields.reason) target.reason = fields.reason;
103
+ return target;
104
+ }
105
+ if (target.state === state) {
106
+ if (state === "routed" && target.routedAt === void 0) target.routedAt = now;
107
+ if (state === "delivered" && target.deliveredAt === void 0) target.deliveredAt = now;
108
+ if (state === "late" && target.lateDeliveredAt === void 0) target.lateDeliveredAt = now;
109
+ if (fields.reason) target.reason = fields.reason;
110
+ if (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;
111
+ return target;
112
+ }
113
+ const wasPending = target.state === "routed" || target.state === "queued";
114
+ const nowPending = state === "routed" || state === "queued";
115
+ if (wasPending && !nowPending) status.pending = Math.max(0, status.pending - 1);
116
+ target.state = state;
117
+ if (state === "routed") target.routedAt = now;
118
+ if (state === "delivered") {
119
+ target.deliveredAt = now;
120
+ status.lastDeliveredAt = now;
121
+ }
122
+ if (state === "late") {
123
+ target.lateDeliveredAt = now;
124
+ status.lastDeliveredAt = now;
125
+ }
126
+ if (state === "failed") target.failedAt = now;
127
+ if (state === "recovered") target.recoveredAt = now;
128
+ if (fields.reason) target.reason = fields.reason;
129
+ if (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;
130
+ if (!wasPending || !nowPending) incrementStateCount(status, state);
131
+ return target;
132
+ }
133
+ function findSteeringRequest(status, requestId) {
134
+ return status.recent.find((request) => request.id === requestId);
135
+ }
136
+ function actionResultFromSteeringStatus(status, sourceRunId, requestId, replacementRunId) {
137
+ const request = findSteeringRequest(status, requestId);
138
+ if (!request) return void 0;
139
+ const targets = request.targets.map((target) => ({
140
+ index: target.index,
141
+ state: target.state,
142
+ ...target.deliveredAt !== void 0 ? { deliveredAt: target.deliveredAt } : {},
143
+ ...target.lateDeliveredAt !== void 0 ? { lateDeliveredAt: target.lateDeliveredAt } : {},
144
+ ...target.reason ? { reason: target.reason } : {},
145
+ ...target.replacementRunId ? { replacementRunId: target.replacementRunId } : {}
146
+ }));
147
+ const states = targets.map((target) => target.state);
148
+ let state = "pending";
149
+ if (states.length > 0 && states.every((candidate) => candidate === "delivered")) state = "delivered";
150
+ else if (states.length > 0 && states.every((candidate) => candidate === "scheduled")) state = "scheduled";
151
+ else if (states.length > 0 && states.every((candidate) => candidate === "recovered")) state = "recovered";
152
+ else if (states.length > 0 && states.every((candidate) => candidate === "failed" || candidate === "late")) state = "failed";
153
+ else if (states.some((candidate) => candidate === "failed" || candidate === "late") && states.some((candidate) => candidate !== "failed" && candidate !== "late")) state = "partial";
154
+ const effectiveReplacementRunId = replacementRunId ?? request.targets.find((target) => target.replacementRunId)?.replacementRunId;
155
+ const deliveryStatus = states.length > 0 && states.every((candidate) => candidate === "delivered" || candidate === "recovered") ? "delivered" : "queued";
156
+ return { requestId, state, deliveryStatus, sourceRunId, ...effectiveReplacementRunId ? { replacementRunId: effectiveReplacementRunId } : {}, targets };
157
+ }
158
+ function steeringActionIsTerminal(result2) {
159
+ return (result2?.targets.length ?? 0) > 0 && result2?.targets.every((target) => target.state === "queued" || target.state === "delivered") === true || result2?.state === "delivered" || result2?.state === "scheduled" || result2?.state === "partial" || result2?.state === "recovered" || result2?.state === "failed";
160
+ }
161
+ function claimSteeringRecovery(asyncDir, input) {
162
+ const recoveryDir = path.join(asyncDir, "control", "steer-recovery");
163
+ fs.mkdirSync(recoveryDir, { recursive: true, mode: 448 });
164
+ const claimPath = path.join(recoveryDir, "claim.json");
165
+ let fd;
166
+ try {
167
+ fd = fs.openSync(claimPath, "wx", 384);
168
+ } catch (error) {
169
+ if (error.code === "EEXIST") {
170
+ throw new Error("Another steering recovery is already committed for this source run.");
171
+ }
172
+ throw error;
173
+ }
174
+ let writeError;
175
+ try {
176
+ fs.writeFileSync(fd, JSON.stringify({ version: 1, ...input }, null, 2), "utf-8");
177
+ fs.fsyncSync(fd);
178
+ } catch (error) {
179
+ writeError = error;
180
+ } finally {
181
+ fs.closeSync(fd);
182
+ }
183
+ if (writeError !== void 0) {
184
+ fs.rmSync(claimPath, { force: true });
185
+ throw writeError;
186
+ }
187
+ const markerPath = path.join(recoveryDir, `${Buffer.from(input.requestId).toString("base64url")}.json`);
188
+ try {
189
+ writePrivateAtomicJson(markerPath, { version: 1, ...input });
190
+ } catch (error) {
191
+ fs.rmSync(claimPath, { force: true });
192
+ throw error;
193
+ }
194
+ return { claimPath, markerPath };
195
+ }
196
+ function readSteeringStatus(asyncDir) {
197
+ return readStatus(asyncDir)?.steering;
198
+ }
199
+ function remainingSteeringRecoveryLimits(descriptor, status, now = Date.now()) {
200
+ const limits = {};
201
+ if (descriptor.absoluteDeadlineAt !== void 0) {
202
+ const timeoutMs = descriptor.absoluteDeadlineAt - now;
203
+ if (timeoutMs <= 0) throw new Error("Source run has no remaining deadline budget; it remains paused.");
204
+ limits.timeoutMs = timeoutMs;
205
+ limits.absoluteDeadlineAt = descriptor.absoluteDeadlineAt;
206
+ }
207
+ if (descriptor.initialToolBudget) {
208
+ const consumed = status.toolBudget?.toolCount ?? status.toolCount ?? 0;
209
+ const hard = descriptor.initialToolBudget.hard - consumed;
210
+ if (hard <= 0) throw new Error("Source run has no remaining tool budget; it remains paused.");
211
+ const soft = descriptor.initialToolBudget.soft === void 0 ? void 0 : descriptor.initialToolBudget.soft - consumed;
212
+ limits.toolBudget = {
213
+ hard,
214
+ ...soft !== void 0 && soft > 0 && soft < hard ? { soft } : {},
215
+ block: descriptor.initialToolBudget.block
216
+ };
217
+ }
218
+ return limits;
219
+ }
220
+ async function waitForSteeringAction(input) {
221
+ const deadline = Date.now() + input.timeoutMs;
222
+ while (Date.now() <= deadline) {
223
+ if (input.signal?.aborted) return void 0;
224
+ const status2 = readSteeringStatus(input.asyncDir);
225
+ const result2 = status2 ? actionResultFromSteeringStatus(status2, input.sourceRunId, input.requestId) : void 0;
226
+ if (steeringActionIsTerminal(result2)) return result2;
227
+ await new Promise((resolve3) => setTimeout(resolve3, Math.min(50, Math.max(1, deadline - Date.now()))));
228
+ }
229
+ const status = readSteeringStatus(input.asyncDir);
230
+ return status ? actionResultFromSteeringStatus(status, input.sourceRunId, input.requestId) : void 0;
231
+ }
232
+
233
+ // src/runs/foreground/async-steering-action.ts
234
+ import { randomUUID } from "node:crypto";
235
+ import * as fs2 from "node:fs";
236
+ import * as path2 from "node:path";
237
+ function steerTargetsAccepted(result2) {
238
+ const targets = result2?.targets ?? [];
239
+ return targets.length > 0 && targets.every((target) => target.state === "queued" || target.state === "delivered");
240
+ }
241
+ function canQueueRetainedAsyncFollowUp(status, index) {
242
+ const steps = status.steps ?? [];
243
+ return status.state === "complete" && Boolean(status.parentWorkflowRunId) && steps.length === 1 && (steps[0]?.status === "complete" || steps[0]?.status === "completed") && Boolean(steps[0]?.sessionFile ?? status.sessionFile) && (index === void 0 || index === 0);
244
+ }
245
+ async function steerAsyncRun(input) {
246
+ if (!input.location.asyncDir) {
247
+ return {
248
+ content: [{ type: "text", text: `Async run '${input.runId}' has no live run directory to steer.` }],
249
+ isError: true,
250
+ details: { mode: "management", results: [] }
251
+ };
252
+ }
253
+ const asyncDir = input.location.asyncDir;
254
+ const snapshot = input.findPendingAsks ? readStatus(asyncDir) : null;
255
+ if (snapshot?.mode === "single" && (!input.state.currentSessionId || ownsRun(input.state.currentSessionId, snapshot.sessionId)) && (snapshot.state === "running" || snapshot.state === "queued") && snapshot.steps?.length === 1 && (input.index === void 0 || input.index === 0) && (snapshot.steps[0]?.status === "running" || snapshot.steps[0]?.status === "pending")) {
256
+ const asks = input.findPendingAsks?.({ runId: snapshot.runId, agent: snapshot.steps[0].agent, childIndex: 0 }) ?? [];
257
+ if (asks.length > 0) {
258
+ const replies = asks.map((replyTo) => `subagent_supervisor(${JSON.stringify({ action: "reply", replyTo, message: "<explicit answer>" })})`).join("\n");
259
+ return {
260
+ content: [{ type: "text", text: `Steering not delivered or queued: async run ${snapshot.runId} is blocked on ${asks.length === 1 ? "a pending supervisor ask" : "ambiguous pending supervisor asks"}. No reply or recovery was attempted. Reply explicitly${asks.length > 1 ? " to the intended request ID" : ""}:
261
+ ${replies}` }],
262
+ isError: true,
263
+ details: { mode: "management", results: [] }
264
+ };
265
+ }
266
+ }
267
+ const status = reconcileAsyncRun(asyncDir, { kill: input.kill }).status;
268
+ if (input.state.currentSessionId && !ownsRun(input.state.currentSessionId, status?.sessionId)) {
269
+ return {
270
+ content: [{ type: "text", text: `Async run '${input.runId}' was not found in the active session.` }],
271
+ isError: true,
272
+ details: { mode: "management", results: [] }
273
+ };
274
+ }
275
+ if (!status) {
276
+ return {
277
+ content: [{ type: "text", text: `Async run '${input.runId}' was not found.` }],
278
+ isError: true,
279
+ details: { mode: "management", results: [] }
280
+ };
281
+ }
282
+ const steps = status.steps ?? [];
283
+ if (status.state !== "running" && status.state !== "queued") {
284
+ const followUpEligible = canQueueRetainedAsyncFollowUp(status, input.index);
285
+ const retained = input.mode === "follow_up" && followUpEligible;
286
+ if (!retained) {
287
+ let resumeHint = `Continue it with action "resume", or read its saved result.`;
288
+ try {
289
+ resolveAsyncResumeTarget({ dir: asyncDir, index: input.index }, { kill: input.kill }, { requireSessionFile: false, sessionId: input.state.currentSessionId ?? void 0 });
290
+ } catch (error) {
291
+ resumeHint = `action "resume" is not available either: ${error instanceof Error ? error.message : String(error)}`;
292
+ }
293
+ const hint = followUpEligible ? `Queue this message for its next resume with mode "follow_up", or continue the run now with action "resume".` : input.mode === "follow_up" ? `mode "follow_up" requires a completed single-child workflow run with a persisted session. ${resumeHint}` : resumeHint;
294
+ return {
295
+ content: [{ type: "text", text: `Async run '${input.runId}' is ${status.state} and cannot be steered; steering requires state "running" or "queued". ${hint}` }],
296
+ isError: true,
297
+ details: { mode: "management", results: [] }
298
+ };
299
+ }
300
+ const request = { type: "steer", id: randomUUID(), ts: Date.now(), message: input.message.trim(), mode: "follow_up", targetIndex: 0, source: "steer-action" };
301
+ try {
302
+ queueRevivalBrief(asyncDir, request);
303
+ } catch (error) {
304
+ return { content: [{ type: "text", text: `Failed to queue retained follow-up for async run ${status.runId}: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "management", results: [] } };
305
+ }
306
+ status.steering ??= createSteeringStatus();
307
+ recordSteeringRequest(status.steering, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets: [{ index: 0, state: "scheduled" }] });
308
+ writeAtomicJson(path2.join(asyncDir, "status.json"), status);
309
+ const queued = { requestId: request.id, state: "scheduled", deliveryStatus: "queued", sourceRunId: status.runId, targets: [{ index: 0, state: "scheduled" }] };
310
+ return { content: [{ type: "text", text: steeringReceipt(request.message, `Follow-up queued for the next resume of retained async run ${status.runId} (request ${request.id}).`) }], details: { mode: "management", results: [], steering: queued } };
311
+ }
312
+ if (input.index !== void 0) {
313
+ if (input.index < 0 || input.index >= steps.length) {
314
+ return {
315
+ content: [{ type: "text", text: `Async run '${status.runId}' has ${steps.length} children. Index ${input.index} is out of range.` }],
316
+ isError: true,
317
+ details: { mode: "management", results: [] }
318
+ };
319
+ }
320
+ const targetStep = steps[input.index];
321
+ if (targetStep && targetStep.status !== "running" && targetStep.status !== "pending") {
322
+ return {
323
+ content: [{ type: "text", text: `Async run '${status.runId}' child ${input.index} is ${targetStep.status} and cannot be steered.` }],
324
+ isError: true,
325
+ details: { mode: "management", results: [] }
326
+ };
327
+ }
328
+ } else {
329
+ const running2 = steps.filter((step) => step.status === "running");
330
+ if (running2.length === 0 && steps.length > 1) {
331
+ return {
332
+ content: [{ type: "text", text: `Async run '${status.runId}' has no running child yet. Provide index to steer a queued child.` }],
333
+ isError: true,
334
+ details: { mode: "management", results: [] }
335
+ };
336
+ }
337
+ }
338
+ const runningIndexes = steps.map((step, index) => step.status === "running" ? index : void 0).filter((index) => index !== void 0);
339
+ const effectiveTargetIndex = input.index ?? (status.mode === "single" && runningIndexes.length === 0 && steps[0]?.status === "pending" ? 0 : void 0);
340
+ const targetIndexes = effectiveTargetIndex !== void 0 ? [effectiveTargetIndex] : runningIndexes;
341
+ if (targetIndexes.length === 0) {
342
+ return {
343
+ content: [{ type: "text", text: `Async run '${status.runId}' has no running child to steer.` }],
344
+ isError: true,
345
+ details: { mode: "management", results: [] }
346
+ };
347
+ }
348
+ const requestId = randomUUID();
349
+ let requestPath;
350
+ try {
351
+ requestPath = requestAsyncSteer(asyncDir, {
352
+ message: input.message,
353
+ mode: input.mode,
354
+ ...effectiveTargetIndex !== void 0 ? { targetIndex: effectiveTargetIndex } : { targetIndexes },
355
+ source: "steer-action",
356
+ id: requestId
357
+ });
358
+ } catch (error) {
359
+ return {
360
+ content: [{ type: "text", text: `Failed to queue steering for async run ${status.runId}: ${error instanceof Error ? error.message : String(error)}` }],
361
+ isError: true,
362
+ details: { mode: "management", results: [] }
363
+ };
364
+ }
365
+ input.onRequestQueued?.(requestPath);
366
+ const tracked = input.state.asyncJobs.get(status.runId);
367
+ if (tracked) tracked.updatedAt = Date.now();
368
+ const targets = targetIndexes.map((index) => ({ index, state: status.steps?.[index]?.status === "pending" ? "scheduled" : "pending" }));
369
+ if (targets.every((target) => target.state === "scheduled")) {
370
+ const scheduled = { requestId, state: "scheduled", deliveryStatus: "queued", sourceRunId: status.runId, targets };
371
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering scheduled for async run ${status.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: scheduled } };
372
+ }
373
+ const waited = await waitForSteeringAction({ asyncDir, sourceRunId: status.runId, requestId, timeoutMs: input.ackTimeoutMs ?? 3e3, signal: input.signal });
374
+ const result2 = waited ?? { requestId, state: "pending", deliveryStatus: "queued", sourceRunId: status.runId, targets };
375
+ if (input.signal?.aborted) {
376
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering pending for async run ${status.runId} (request ${requestId}); caller aborted before recovery.`) }], details: { mode: "management", results: [], steering: result2 } };
377
+ }
378
+ const finalStatus = readStatus(asyncDir);
379
+ const finalResult = finalStatus?.steering ? actionResultFromSteeringStatus(finalStatus.steering, status.runId, requestId) : void 0;
380
+ const acceptedReply = (accepted) => {
381
+ const acceptedState = accepted.state === "delivered" ? "delivered" : "queued";
382
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${acceptedState} for async run ${status.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: accepted } };
383
+ };
384
+ if (finalResult && steerTargetsAccepted(finalResult)) return acceptedReply(finalResult);
385
+ const running = (finalStatus?.steps ?? status.steps ?? []).filter((step) => step.status === "running");
386
+ const recoveryAllowed = (input.mode ?? "steer") === "steer" && status.mode === "single" && status.isNested !== true && running.length === 1 && Boolean(finalStatus?.steering) && (input.index === void 0 || input.index === 0);
387
+ if (recoveryAllowed && finalResult?.state !== "scheduled" && input.recover) {
388
+ const appendSteeringNotice = (state, message) => {
389
+ try {
390
+ fs2.appendFileSync(path2.join(asyncDir, "events.jsonl"), `${JSON.stringify({ type: "subagent.steering.notice", ts: Date.now(), runId: status.runId, requestId, state, message, ...status.sessionId ? { currentSessionId: status.sessionId } : {} })}
391
+ `);
392
+ } catch {
393
+ }
394
+ };
395
+ try {
396
+ const latest = readStatus(asyncDir);
397
+ const latestResult = latest?.steering ? actionResultFromSteeringStatus(latest.steering, status.runId, requestId) : void 0;
398
+ if (latestResult && steerTargetsAccepted(latestResult)) return acceptedReply(latestResult);
399
+ const committedAt = Date.now();
400
+ input.onBeforeRecoveryClaim?.(requestId, committedAt);
401
+ const { claimPath, markerPath } = claimSteeringRecovery(asyncDir, { requestId, sourceRunId: status.runId, committedAt });
402
+ input.onRecoveryCommitted?.(requestId, committedAt);
403
+ const preCommitStatus = readStatus(asyncDir);
404
+ const preCommitResult = preCommitStatus?.steering ? actionResultFromSteeringStatus(preCommitStatus.steering, status.runId, requestId) : void 0;
405
+ if (preCommitResult && preCommitResult.targets.length > 0 && preCommitResult.targets.every((target) => target.state === "queued" || target.deliveredAt !== void 0 && target.deliveredAt <= committedAt)) {
406
+ fs2.rmSync(markerPath, { force: true });
407
+ fs2.rmSync(claimPath, { force: true });
408
+ return acceptedReply(preCommitResult);
409
+ }
410
+ try {
411
+ deliverInterruptRequest({ asyncDir, source: "steering-recovery" });
412
+ } catch (error) {
413
+ fs2.rmSync(markerPath, { force: true });
414
+ fs2.rmSync(claimPath, { force: true });
415
+ throw new Error(`Failed to commit steering recovery interrupt: ${error instanceof Error ? error.message : String(error)}`);
416
+ }
417
+ const pauseBudgetMs = input.recoveryTimeoutMs ?? 15e3;
418
+ const pauseDeadline = Date.now() + pauseBudgetMs;
419
+ let paused = null;
420
+ let lastObservedState;
421
+ while (Date.now() <= pauseDeadline) {
422
+ if (input.signal?.aborted) break;
423
+ const candidate = readStatus(asyncDir);
424
+ lastObservedState = candidate?.state;
425
+ if (candidate?.state === "paused" && candidate.endedAt !== void 0) {
426
+ paused = candidate;
427
+ break;
428
+ }
429
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
430
+ }
431
+ if (!paused) {
432
+ fs2.rmSync(claimPath, { force: true });
433
+ throw new Error(`Source run did not confirm a paused state within ${pauseBudgetMs / 1e3} seconds after the recovery interrupt (last observed state: ${lastObservedState ?? "unknown"}). No replacement was launched and the steering message was not applied. Once it pauses, continue it with action "resume" and id "${status.runId}".`);
434
+ }
435
+ let recoveryTarget;
436
+ try {
437
+ recoveryTarget = resolveAsyncResumeTarget(
438
+ { id: status.runId },
439
+ { kill: input.kill },
440
+ { sessionId: input.state.currentSessionId ?? void 0 }
441
+ );
442
+ } catch (error) {
443
+ throw new Error(`Source run remains paused and cannot be revived safely: ${error instanceof Error ? error.message : String(error)}`);
444
+ }
445
+ if (recoveryTarget.kind !== "revive" || !recoveryTarget.sessionFile) throw new Error("Source run has no valid persisted child session; it remains paused and requires a new run.");
446
+ if (!recoveryTarget.recoveryDescriptor) throw new Error("Source run has no private recovery descriptor; it remains paused and is not safely resumable.");
447
+ const limits = remainingSteeringRecoveryLimits(recoveryTarget.recoveryDescriptor, paused);
448
+ const revived = await input.recover(limits);
449
+ if (revived.isError || !revived.details.asyncId) throw new Error(revived.content[0]?.type === "text" ? revived.content[0].text : "Replacement launch failed; source run remains paused.");
450
+ const targetIndex = input.index ?? paused.steering?.recent.find((request) => request.id === requestId)?.targets[0]?.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
451
+ if (paused.steering && targetIndex >= 0) {
452
+ updateSteeringTarget(paused.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
453
+ const stepSteering = paused.steps?.[targetIndex]?.steering;
454
+ if (stepSteering) updateSteeringTarget(stepSteering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
455
+ writeAtomicJson(path2.join(asyncDir, "status.json"), paused);
456
+ }
457
+ const recovered = paused.steering ? actionResultFromSteeringStatus(paused.steering, status.runId, requestId, revived.details.asyncId) : void 0;
458
+ appendSteeringNotice("recovered", `Steering recovered for run ${status.runId}; replacement ${revived.details.asyncId} launched.`);
459
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering recovered for async run ${status.runId}; replacement ${revived.details.asyncId} launched after the source paused.`) }], details: { mode: "management", results: [], steering: recovered ?? { requestId, state: "recovered", sourceRunId: status.runId, replacementRunId: revived.details.asyncId, deliveryStatus: "delivered", targets: [{ index: input.index ?? 0, state: "recovered" }] } } };
460
+ } catch (error) {
461
+ const reason = error instanceof Error ? error.message : String(error);
462
+ const failedStatus = readStatus(asyncDir);
463
+ const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
464
+ if (failedStatus && failedStatus.state !== "running" && failedStatus.state !== "queued" && failedStatus.steering && targetIndex >= 0) {
465
+ updateSteeringTarget(failedStatus.steering, requestId, targetIndex, "failed", Date.now(), { reason });
466
+ const stepSteering = failedStatus.steps?.[targetIndex]?.steering;
467
+ if (stepSteering) updateSteeringTarget(stepSteering, requestId, targetIndex, "failed", Date.now(), { reason });
468
+ failedStatus.activityState = "needs_attention";
469
+ writeAtomicJson(path2.join(asyncDir, "status.json"), failedStatus);
470
+ }
471
+ const failed = failedStatus?.steering ? actionResultFromSteeringStatus(failedStatus.steering, status.runId, requestId) : void 0;
472
+ appendSteeringNotice("failed", `Steering failed for run ${status.runId}: ${reason}`);
473
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering failed for async run ${status.runId} (request ${requestId}): ${reason}`) }], isError: true, details: { mode: "management", results: [], steering: failed ?? { requestId, state: "failed", sourceRunId: status.runId, deliveryStatus: "queued", targets: [{ index: input.index ?? 0, state: "failed", reason }] } } };
474
+ }
475
+ }
476
+ const stateText = result2.state === "failed" ? "failed" : result2.state === "partial" ? "partial" : result2.deliveryStatus === "queued" ? "queued" : result2.state === "delivered" ? "delivered" : result2.state === "scheduled" ? "scheduled" : result2.state === "recovered" ? "recovered" : "pending";
477
+ const isError = result2.state === "failed" || result2.state === "partial";
478
+ return {
479
+ content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${stateText} for async run ${status.runId} (request ${requestId}).`) }],
480
+ ...isError ? { isError: true } : {},
481
+ details: { mode: "management", results: [], steering: result2 }
482
+ };
483
+ }
484
+
485
+ // src/runs/foreground/workflow-foreground-steering.ts
486
+ import { randomUUID as randomUUID2 } from "node:crypto";
487
+ import * as path3 from "node:path";
488
+
489
+ // src/runs/shared/workflow-async-child-guidance.ts
490
+ function workflowAsyncChildSteeringGuidance(status, state) {
491
+ if (status.mode !== "workflow" || status.state !== "running" && status.state !== "queued" || !state?.currentSessionId || status.sessionId !== state.currentSessionId || !state.workflowControllers?.has(status.runId)) return [];
492
+ const ids = new Set((status.steps ?? []).flatMap((step) => step.async === true && step.status === "running" && step.runner?.type !== "external-cli" && step.runner?.type !== "external-job" && typeof step.runId === "string" && /^[A-Za-z0-9_-]+$/.test(step.runId) && step.runId !== status.runId ? [step.runId] : []));
493
+ if (ids.size === 0) return [];
494
+ return [
495
+ "Async children recorded as running; direct steering rechecks controls. Choose an exact child ID (queued does not mean consumed):",
496
+ ...[...ids].map((id) => ` subagent({ action: "steer", id: "${id}", message: "...", options: { mode: "follow_up" } })`)
497
+ ];
498
+ }
499
+
500
+ // src/runs/foreground/workflow-foreground-steering.ts
501
+ function activeWorkflowError(state, workflowRunId, asyncDirRoot) {
502
+ if (!state.currentSessionId) return "Workflow steering requires an active parent session.";
503
+ if (!state.workflowControllers?.has(workflowRunId)) return `Workflow '${workflowRunId}' has no live foreground child.`;
504
+ const status = readStatus(path3.join(asyncDirRoot, workflowRunId));
505
+ if (!status || status.mode !== "workflow" || status.state !== "running" && status.state !== "queued") {
506
+ return `Workflow '${workflowRunId}' has no live foreground child.`;
507
+ }
508
+ if (status.sessionId !== state.currentSessionId) return `Workflow '${workflowRunId}' was not found in the active session.`;
509
+ return void 0;
510
+ }
511
+ function controlIsLiveInWorkflow(control, workflowRunId, sessionId) {
512
+ return control.parentWorkflowRunId === workflowRunId && control.sessionId === sessionId && (control.activeChildren?.size ?? 0) > 0;
513
+ }
514
+ function resolveWorkflowForegroundSteeringTarget(input) {
515
+ const { state, childRunId, asyncDirRoot } = input;
516
+ if (childRunId) {
517
+ const control = state.foregroundControls.get(childRunId);
518
+ if (!control?.parentWorkflowRunId) return { ok: false, message: `Foreground run '${childRunId}' is not a live workflow-owned child.` };
519
+ const workflowRunId2 = control.parentWorkflowRunId;
520
+ const workflowError2 = activeWorkflowError(state, workflowRunId2, asyncDirRoot);
521
+ if (workflowError2) return { ok: false, message: workflowError2 };
522
+ if (!controlIsLiveInWorkflow(control, workflowRunId2, state.currentSessionId)) {
523
+ return { ok: false, message: `Foreground run '${childRunId}' is not a live workflow-owned child in the active session.` };
524
+ }
525
+ return { ok: true, target: { control, workflowRunId: workflowRunId2, sourceRunId: childRunId } };
526
+ }
527
+ const workflowRunId = input.workflowRunId;
528
+ if (!workflowRunId) return { ok: false, message: "Workflow steering requires a workflow or child run id." };
529
+ const workflowError = activeWorkflowError(state, workflowRunId, asyncDirRoot);
530
+ if (workflowError) return { ok: false, message: workflowError };
531
+ const controls = [...state.foregroundControls.values()].filter((control) => controlIsLiveInWorkflow(control, workflowRunId, state.currentSessionId));
532
+ if (controls.length === 0) return { ok: false, message: `Workflow '${workflowRunId}' has no live foreground child.` };
533
+ if (controls.length > 1) return { ok: false, message: `Workflow '${workflowRunId}' has ${controls.length} live foreground children; steer a child run id instead.` };
534
+ return { ok: true, target: { control: controls[0], workflowRunId, sourceRunId: workflowRunId } };
535
+ }
536
+ function managementError(message) {
537
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
538
+ }
539
+ async function steerWorkflowRun(input) {
540
+ const { state, runId, asyncDir } = input;
541
+ const status = readStatus(asyncDir);
542
+ if (!status || status.mode !== "workflow" || status.runId !== runId || path3.basename(asyncDir) !== runId) {
543
+ return managementError(`Workflow '${runId}' does not match its resolved run directory.`);
544
+ }
545
+ if (state.workflowControllers?.has(runId)) {
546
+ const route = resolveWorkflowForegroundSteeringTarget({ state, workflowRunId: runId, asyncDirRoot: path3.dirname(asyncDir) });
547
+ if (!route.ok) return managementError([route.message, ...workflowAsyncChildSteeringGuidance(status, state)].join("\n"));
548
+ return steerWorkflowForegroundTarget({ target: route.target, message: input.message, mode: input.mode, index: input.index });
549
+ }
550
+ if (!state.currentSessionId) return managementError("Workflow steering requires an active parent session.");
551
+ if (status.sessionId !== state.currentSessionId) return managementError(`Workflow '${runId}' was not found in the active session.`);
552
+ if (status.state !== "running" && status.state !== "queued") return managementError(`Workflow '${runId}' is not running or queued and cannot be steered.`);
553
+ if (typeof status.completionOwnerId !== "string" || !status.completionOwnerId.trim() || status.completionOwnerId === (state.completionOwnerId ?? currentCompletionOwnerId()) || [...state.foregroundControls.values()].some((control) => control.parentWorkflowRunId === runId)) {
554
+ return managementError(`Workflow '${runId}' has no live foreground child and is not a recorded foreign workflow.`);
555
+ }
556
+ if (input.index !== void 0) {
557
+ const step = status.steps?.[input.index];
558
+ if (!Number.isInteger(input.index) || input.index < 0 || !step) return managementError(`Workflow '${runId}' index ${input.index} is out of range or invalid.`);
559
+ if (step.status !== "running" && step.status !== "pending") return managementError(`Workflow '${runId}' child ${input.index} is ${step.status} and cannot be steered.`);
560
+ if (typeof step.workflowKey !== "string" || !step.workflowKey.trim()) return managementError(`Workflow '${runId}' child ${input.index} has no projected workflow key.`);
561
+ }
562
+ const requestId = randomUUID2();
563
+ try {
564
+ requestAsyncSteer(asyncDir, { id: requestId, message: input.message, mode: input.mode, ...input.index !== void 0 ? { targetIndex: input.index } : {}, source: "steer-action" });
565
+ } catch (error) {
566
+ return managementError(`Failed to queue steering for workflow ${runId}: ${error instanceof Error ? error.message : String(error)}`);
567
+ }
568
+ const steering = await waitForSteeringAction({ asyncDir, sourceRunId: runId, requestId, timeoutMs: input.ackTimeoutMs ?? 3e3, signal: input.signal }) ?? { requestId, state: "pending", deliveryStatus: "queued", sourceRunId: runId, targets: input.index === void 0 ? [] : [{ index: input.index, state: "pending" }] };
569
+ const failed = steering.state === "failed" || steering.state === "partial";
570
+ return {
571
+ content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${failed ? steering.state : steering.deliveryStatus === "delivered" ? "delivered" : "queued"} for workflow ${runId} (request ${requestId}).`) }],
572
+ ...failed ? { isError: true } : {},
573
+ details: { mode: "management", results: [], steering }
574
+ };
575
+ }
576
+ async function steerWorkflowForegroundTarget(input) {
577
+ const { control, sourceRunId } = input.target;
578
+ const activeIndexes = [...control.activeChildren?.keys() ?? []].sort((left, right) => left - right);
579
+ const index = input.index ?? (activeIndexes.length === 1 ? activeIndexes[0] : void 0);
580
+ if (index === void 0) {
581
+ return managementError(activeIndexes.length === 0 ? `Foreground run '${control.runId}' has no live child session.` : `Foreground run '${control.runId}' has ${activeIndexes.length} live child sessions; provide index.`);
582
+ }
583
+ const child = control.activeChildren?.get(index);
584
+ if (!child) return managementError(`Foreground run '${control.runId}' child ${index} is not live.`);
585
+ if (!child.steer) return managementError(`Foreground run '${control.runId}' child ${index} does not support steering.`);
586
+ const message = input.message.trim();
587
+ const requestId = randomUUID2();
588
+ const outcome = await child.steer({ message, ...input.mode && input.mode !== "steer" ? { mode: input.mode } : {} });
589
+ const target = outcome.state === "delivered" ? { index, state: "delivered", deliveredAt: Date.now() } : outcome.state === "queued" ? { index, state: "queued" } : { index, state: "failed", reason: outcome.reason };
590
+ const steering = {
591
+ requestId,
592
+ state: outcome.state === "delivered" ? "delivered" : outcome.state === "failed" ? "failed" : "pending",
593
+ deliveryStatus: outcome.state === "delivered" ? "delivered" : "queued",
594
+ sourceRunId,
595
+ targets: [target]
596
+ };
597
+ if (outcome.state === "delivered") {
598
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering delivered for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
599
+ }
600
+ if (outcome.state === "queued") {
601
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering queued for foreground run ${control.runId} (request ${requestId}).`) }], details: { mode: "management", results: [], steering } };
602
+ }
603
+ return { content: [{ type: "text", text: steeringReceipt(message, `Steering failed for foreground run ${control.runId} (request ${requestId}): ${outcome.reason ?? "unknown error"}`) }], isError: true, details: { mode: "management", results: [], steering } };
604
+ }
605
+
606
+ // src/tui/fleet-transcript.ts
607
+ import * as fs3 from "node:fs";
608
+ import * as path4 from "node:path";
609
+ import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
610
+ import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
611
+ var DEFAULT_MAX_RECORDS = 240;
612
+ var DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
613
+ var MAX_MESSAGE_CHARS = 64 * 1024;
614
+ var TOOL_PREVIEW_LINES = 7;
615
+ function sanitizeJsonDisplayValue(value) {
616
+ if (typeof value === "string") {
617
+ const safe = safeTerminalText(value);
618
+ return { value: safe, changed: safe !== value };
619
+ }
620
+ if (Array.isArray(value)) {
621
+ const sanitized = value.map(sanitizeJsonDisplayValue);
622
+ return {
623
+ value: sanitized.map((entry) => entry.value),
624
+ changed: sanitized.some((entry) => entry.changed)
625
+ };
626
+ }
627
+ if (value && typeof value === "object") {
628
+ let changed = false;
629
+ const sanitized = /* @__PURE__ */ Object.create(null);
630
+ for (const [key, nested] of Object.entries(value)) {
631
+ const safeKey = safeTerminalText(key);
632
+ const safeValue = sanitizeJsonDisplayValue(nested);
633
+ sanitized[safeKey] = safeValue.value;
634
+ changed ||= safeKey !== key || safeValue.changed;
635
+ }
636
+ return { value: changed ? sanitized : value, changed };
637
+ }
638
+ return { value, changed: false };
639
+ }
640
+ function safeToolArgsPayload(payload) {
641
+ try {
642
+ const sanitized = sanitizeJsonDisplayValue(JSON.parse(payload));
643
+ return sanitized.changed ? JSON.stringify(sanitized.value) : safeTerminalText(payload);
644
+ } catch {
645
+ return safeTerminalText(payload);
646
+ }
647
+ }
648
+ function objectValue(value) {
649
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
650
+ }
651
+ function stringValue(value) {
652
+ return typeof value === "string" && value.trim() ? value : void 0;
653
+ }
654
+ function numberValue(value) {
655
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
656
+ }
657
+ function pathWithin(base, candidate) {
658
+ const resolvedBase = path4.resolve(base);
659
+ const resolvedCandidate = path4.resolve(candidate);
660
+ return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path4.sep}`);
661
+ }
662
+ function errorMessage(error) {
663
+ return error instanceof Error ? error.message : String(error);
664
+ }
665
+ function isNotFoundError(error) {
666
+ return Boolean(objectValue(error)?.code === "ENOENT");
667
+ }
668
+ function validateTranscriptPath(filePath, trustedRoots, trustedFiles = [], trustedFileRoot) {
669
+ if (trustedRoots.length === 0 && (!trustedFileRoot || trustedFiles.length === 0)) return { warning: `Transcript preview has no trusted root: ${filePath}` };
670
+ const resolvedPath = path4.resolve(filePath);
671
+ const recordedCandidate = trustedFileRoot && pathWithin(trustedFileRoot, resolvedPath) && trustedFiles.some((file) => path4.resolve(file) === resolvedPath);
672
+ if (!trustedRoots.some((root) => pathWithin(root, resolvedPath)) && !recordedCandidate) {
673
+ return { warning: `Transcript is outside trusted roots: ${filePath}` };
674
+ }
675
+ let stat;
676
+ try {
677
+ stat = fs3.lstatSync(resolvedPath);
678
+ } catch (error) {
679
+ if (isNotFoundError(error)) return {};
680
+ return { warning: `Transcript could not be inspected: ${errorMessage(error)}` };
681
+ }
682
+ if (stat.isSymbolicLink()) return { warning: `Transcript preview refused a symlink: ${filePath}` };
683
+ if (!stat.isFile()) return { warning: `Transcript path is not a file: ${filePath}` };
684
+ try {
685
+ const realPath = fs3.realpathSync(resolvedPath);
686
+ const realRoots = trustedRoots.filter((root) => fs3.existsSync(root)).map((root) => fs3.realpathSync(root));
687
+ if (!realRoots.some((root) => pathWithin(root, realPath)) && !isTrustedRecordedSessionFile(realPath, trustedFiles, trustedFileRoot)) {
688
+ return { warning: `Transcript resolves outside trusted roots: ${filePath}` };
689
+ }
690
+ return { resolvedPath: realPath };
691
+ } catch (error) {
692
+ return { warning: `Transcript path could not be resolved: ${errorMessage(error)}` };
693
+ }
694
+ }
695
+ function isCompleteRecord(line) {
696
+ if (!line?.trim()) return false;
697
+ try {
698
+ return objectValue(JSON.parse(line)) !== void 0;
699
+ } catch {
700
+ return false;
701
+ }
702
+ }
703
+ function readTailLines(filePath, maxBytes) {
704
+ let fd;
705
+ try {
706
+ const noFollow = typeof fs3.constants.O_NOFOLLOW === "number" ? fs3.constants.O_NOFOLLOW : 0;
707
+ fd = fs3.openSync(filePath, fs3.constants.O_RDONLY | noFollow);
708
+ const stat = fs3.fstatSync(fd);
709
+ if (!stat.isFile()) return { lines: [], truncated: false, warning: `Transcript path is not a file: ${filePath}` };
710
+ if (stat.size === 0) return { lines: [], truncated: false };
711
+ const bytesToRead = Math.min(stat.size, maxBytes);
712
+ const start = stat.size - bytesToRead;
713
+ const buffer = Buffer.alloc(bytesToRead);
714
+ const bytesRead = fs3.readSync(fd, buffer, 0, bytesToRead, start);
715
+ const content = buffer.subarray(0, bytesRead).toString("utf-8");
716
+ const endsWithNewline = content.endsWith("\n");
717
+ let lines = content.split(/\r?\n/);
718
+ if (start > 0 && lines.length > 0) lines = lines.slice(1);
719
+ if (lines.at(-1) === "") lines = lines.slice(0, -1);
720
+ else if (!endsWithNewline && !isCompleteRecord(lines.at(-1))) lines = lines.slice(0, -1);
721
+ return { lines, truncated: start > 0 };
722
+ } catch (error) {
723
+ return { lines: [], truncated: false, warning: `Transcript could not be read: ${errorMessage(error)}` };
724
+ } finally {
725
+ if (fd !== void 0) fs3.closeSync(fd);
726
+ }
727
+ }
728
+ function clipMessage(text) {
729
+ if (text.length <= MAX_MESSAGE_CHARS) return text;
730
+ return `${text.slice(0, MAX_MESSAGE_CHARS)}
731
+
732
+ \u2026 message truncated`;
733
+ }
734
+ function safeTranscriptEvent(event) {
735
+ if (event.kind === "assistant") {
736
+ return {
737
+ ...event,
738
+ text: safeTerminalText(event.text),
739
+ ...event.model ? { model: safeTerminalText(event.model) } : {}
740
+ };
741
+ }
742
+ if (event.kind === "user" || event.kind === "notice") {
743
+ return { ...event, text: safeTerminalText(event.text) };
744
+ }
745
+ return {
746
+ ...event,
747
+ name: safeTerminalText(event.name),
748
+ ...event.args !== void 0 ? { args: safeTerminalText(event.args) } : {},
749
+ ...event.argsPayload !== void 0 ? { argsPayload: safeToolArgsPayload(event.argsPayload) } : {},
750
+ ...event.output !== void 0 ? { output: safeTerminalText(event.output) } : {},
751
+ ...event.error !== void 0 ? { error: safeTerminalText(event.error) } : {}
752
+ };
753
+ }
754
+ function findTool(events, toolCallId, name) {
755
+ if (toolCallId) {
756
+ for (let index = events.length - 1; index >= 0; index--) {
757
+ const event = events[index];
758
+ if (event?.kind === "tool" && event.toolCallId === toolCallId) return event;
759
+ }
760
+ return void 0;
761
+ }
762
+ for (let index = events.length - 1; index >= 0; index--) {
763
+ const event = events[index];
764
+ if (event?.kind !== "tool") continue;
765
+ const tool = event;
766
+ if ((!name || tool.name === name) && !tool.resultSeen) return tool;
767
+ }
768
+ return void 0;
769
+ }
770
+ function appendTextEvent(events, kind, text, metadata) {
771
+ const clipped = clipMessage(text.trim());
772
+ if (!clipped) return;
773
+ const previous = events.at(-1);
774
+ if (previous?.kind === kind && previous.text === clipped) return;
775
+ events.push({ kind, text: clipped, ...metadata });
776
+ }
777
+ function parseTranscriptLines(lines, conversationStarted = false) {
778
+ const events = [];
779
+ let malformed = 0;
780
+ let explicitTruncation = false;
781
+ let assistantSeen = conversationStarted;
782
+ for (const line of lines) {
783
+ if (!line.trim()) continue;
784
+ let record;
785
+ try {
786
+ record = objectValue(JSON.parse(line));
787
+ } catch {
788
+ malformed++;
789
+ continue;
790
+ }
791
+ if (!record) {
792
+ malformed++;
793
+ continue;
794
+ }
795
+ const recordType = stringValue(record.recordType);
796
+ const timestamp = numberValue(record.ts);
797
+ if (recordType === "truncated") {
798
+ explicitTruncation = true;
799
+ continue;
800
+ }
801
+ if (recordType === "tool_start") {
802
+ const name = stringValue(record.toolName) ?? "tool";
803
+ events.push({
804
+ kind: "tool",
805
+ ...stringValue(record.toolCallId) ? { toolCallId: stringValue(record.toolCallId) } : {},
806
+ name,
807
+ ...stringValue(record.argsPreview) ? { args: stringValue(record.argsPreview) } : {},
808
+ ...stringValue(record.argsPayload) ? { argsPayload: stringValue(record.argsPayload) } : {},
809
+ status: "running",
810
+ ...timestamp !== void 0 ? { timestamp, startedAt: timestamp } : {}
811
+ });
812
+ continue;
813
+ }
814
+ if (recordType === "tool_end") {
815
+ const tool = findTool(events, stringValue(record.toolCallId), stringValue(record.toolName));
816
+ if (tool && !tool.resultSeen) tool.status = record.isError === true ? "error" : "complete";
817
+ if (tool && timestamp !== void 0 && tool.endedAt === void 0) tool.endedAt = timestamp;
818
+ continue;
819
+ }
820
+ if (recordType === "stderr") {
821
+ const text2 = stringValue(record.text);
822
+ if (text2) events.push({ kind: "notice", text: clipMessage(text2), tone: "error", ...timestamp !== void 0 ? { timestamp } : {} });
823
+ continue;
824
+ }
825
+ if (recordType !== "message") continue;
826
+ const message = objectValue(record.message);
827
+ const role = stringValue(record.role) ?? stringValue(message?.role);
828
+ const text = stringValue(record.text) ?? stringValue(message?.text) ?? stringValue(message?.content);
829
+ if (role === "toolResult" || role === "tool_result") {
830
+ const toolCallId = stringValue(record.toolCallId) ?? stringValue(message?.toolCallId);
831
+ const name = stringValue(record.toolName) ?? stringValue(message?.toolName) ?? "tool";
832
+ const failed = record.isError === true || message?.isError === true;
833
+ let tool = findTool(events, toolCallId, name);
834
+ if (!tool) {
835
+ tool = {
836
+ kind: "tool",
837
+ ...toolCallId ? { toolCallId } : {},
838
+ name,
839
+ status: failed ? "error" : "complete",
840
+ ...timestamp !== void 0 ? { timestamp } : {}
841
+ };
842
+ events.push(tool);
843
+ }
844
+ if (!tool.resultSeen) {
845
+ tool.resultSeen = true;
846
+ tool.status = failed ? "error" : "complete";
847
+ if (timestamp !== void 0 && tool.endedAt === void 0) tool.endedAt = timestamp;
848
+ if (text && (!failed || tool.output === void 0)) {
849
+ tool.output = clipMessage(text);
850
+ tool.outputTruncated = record.outputTruncated === true || text.includes("\u2026 payload truncated") || text.includes("[Showing lines");
851
+ }
852
+ if (failed && text) tool.error = clipMessage(text.split(/\r?\n/).find((candidate) => candidate.trim()) ?? text);
853
+ }
854
+ continue;
855
+ }
856
+ if (role === "assistant") {
857
+ assistantSeen = true;
858
+ const rawModel = stringValue(record.model) ?? stringValue(message?.model);
859
+ const provider = stringValue(message?.provider);
860
+ const model = rawModel && provider ? `${provider}/${rawModel}` : rawModel;
861
+ if (text) appendTextEvent(events, "assistant", text, {
862
+ ...model ? { model } : {},
863
+ ...timestamp !== void 0 ? { timestamp } : {}
864
+ });
865
+ continue;
866
+ }
867
+ if (role === "user" && assistantSeen && text) {
868
+ appendTextEvent(events, "user", text, timestamp !== void 0 ? { timestamp } : {});
869
+ }
870
+ }
871
+ for (const event of events) {
872
+ if (event.kind === "tool") delete event.resultSeen;
873
+ }
874
+ return { events: events.map(safeTranscriptEvent), malformed, explicitTruncation };
875
+ }
876
+ function readFleetTranscript(filePath, options) {
877
+ const validated = validateTranscriptPath(filePath, options.trustedRoots, options.trustedFiles, options.trustedFileRoot);
878
+ if (!validated.resolvedPath) {
879
+ return { path: filePath, events: [], truncated: false, readFailed: true, ...validated.warning ? { warning: safeTerminalText(validated.warning) } : {} };
880
+ }
881
+ const maxRecords = Math.max(1, options.maxRecords ?? DEFAULT_MAX_RECORDS);
882
+ const tail = readTailLines(validated.resolvedPath, Math.max(1024, options.maxBytes ?? DEFAULT_MAX_BYTES));
883
+ const recordsOmitted = tail.truncated || tail.lines.length > maxRecords;
884
+ const selectedLines = tail.lines.slice(-maxRecords);
885
+ const parsed = parseTranscriptLines(selectedLines, recordsOmitted);
886
+ const warnings = [
887
+ tail.warning,
888
+ parsed.malformed > 0 ? `Skipped ${parsed.malformed} malformed transcript record${parsed.malformed === 1 ? "" : "s"}.` : void 0
889
+ ].filter((value) => Boolean(value));
890
+ return {
891
+ path: filePath,
892
+ events: parsed.events,
893
+ truncated: tail.truncated || tail.lines.length > maxRecords || parsed.explicitTruncation,
894
+ ...tail.warning ? { readFailed: true } : {},
895
+ ...warnings.length ? { warning: safeTerminalText(warnings.join(" ")) } : {}
896
+ };
897
+ }
898
+ function statusGlyph(event, theme) {
899
+ if (event.status === "running") return theme.fg("warning", "\u25CF");
900
+ if (event.status === "error") return theme.fg("error", "\u2717");
901
+ return theme.fg("success", "\u2713");
902
+ }
903
+ function jsonScalar(value) {
904
+ if (typeof value === "string" && value.trim()) return value;
905
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
906
+ return void 0;
907
+ }
908
+ function parseToolArgs(event) {
909
+ if (!event.argsPayload) return void 0;
910
+ try {
911
+ return objectValue(JSON.parse(event.argsPayload));
912
+ } catch {
913
+ return void 0;
914
+ }
915
+ }
916
+ function toolDuration(event) {
917
+ if (event.startedAt === void 0 || event.endedAt === void 0) return void 0;
918
+ return `${((event.endedAt - event.startedAt) / 1e3).toFixed(1)}s`;
919
+ }
920
+ function renderExpandedTool(event, width, theme) {
921
+ const lines = [];
922
+ const args = parseToolArgs(event);
923
+ const glyph = statusGlyph(event, theme);
924
+ const output = event.output ?? event.error;
925
+ const outputColor = event.status === "error" ? "error" : "toolOutput";
926
+ if (event.name === "bash") {
927
+ const command = jsonScalar(args?.command) ?? event.args ?? "(unknown command)";
928
+ lines.push(railLine(`${glyph} ${theme.fg("toolTitle", theme.bold(`$ ${command}`))}`, width, theme));
929
+ if (output) {
930
+ for (const outputLine of output.replace(/\s+$/, "").split(/\r?\n/)) {
931
+ for (const wrapped of renderWrapped(theme.fg(outputColor, outputLine), Math.max(1, width - 4))) {
932
+ lines.push(railLine(` ${wrapped}`, width, theme));
933
+ }
934
+ }
935
+ }
936
+ const duration = toolDuration(event);
937
+ if (duration) lines.push(railLine(theme.fg("dim", ` Took ${duration}`), width, theme));
938
+ return lines;
939
+ }
940
+ if (event.name === "read") {
941
+ const filePath = jsonScalar(args?.path ?? args?.file_path);
942
+ const language = filePath ? getLanguageFromPath(filePath) : void 0;
943
+ const rendered = !output ? [] : event.status === "error" ? output.split("\n").map((line) => theme.fg("error", line)) : language ? highlightCode(output, language) : output.split("\n");
944
+ lines.push(railLine(`${glyph} ${theme.fg("toolTitle", theme.bold(`read ${filePath ?? event.args ?? ""}`))}`, width, theme));
945
+ for (const line of rendered) {
946
+ for (const wrapped of renderWrapped(line, Math.max(1, width - 4))) lines.push(railLine(` ${wrapped}`, width, theme));
947
+ }
948
+ return lines;
949
+ }
950
+ lines.push(railLine(`${glyph} ${theme.fg("toolTitle", theme.bold(event.name))}`, width, theme));
951
+ if (event.argsPayload) {
952
+ lines.push(railLine(theme.fg("dim", " args"), width, theme));
953
+ for (const argLine of event.argsPayload.split(/\r?\n/)) {
954
+ for (const wrapped of renderWrapped(theme.fg("muted", argLine), Math.max(1, width - 4))) lines.push(railLine(` ${wrapped}`, width, theme));
955
+ }
956
+ }
957
+ if (output) {
958
+ lines.push(railLine(theme.fg(event.status === "error" ? "error" : "dim", event.status === "error" ? " error" : " output"), width, theme));
959
+ for (const outputLine of output.split(/\r?\n/)) {
960
+ for (const wrapped of renderWrapped(theme.fg(outputColor, outputLine), Math.max(1, width - 4))) lines.push(railLine(` ${wrapped}`, width, theme));
961
+ }
962
+ }
963
+ return lines;
964
+ }
965
+ function bounded(text, width) {
966
+ return truncateToWidth(text, Math.max(0, width));
967
+ }
968
+ function railLine(content, width, theme) {
969
+ return bounded(`${theme.fg("borderMuted", "\u2502")} ${content}`, width);
970
+ }
971
+ function renderWrapped(text, width) {
972
+ return wrapTextWithAnsi(text, Math.max(1, width));
973
+ }
974
+ function renderFleetTranscript(transcript, width, theme, markdownTheme, options = {}) {
975
+ if (width <= 0) return [];
976
+ const lines = [];
977
+ if (transcript.truncated) lines.push(bounded(theme.fg("dim", "\u2191 Earlier activity omitted"), width));
978
+ if (transcript.warning) {
979
+ for (const line of renderWrapped(safeTerminalText(transcript.warning), Math.max(1, width - 2))) {
980
+ lines.push(bounded(`${theme.fg("warning", "!")} ${theme.fg("warning", line)}`, width));
981
+ }
982
+ }
983
+ for (const rawEvent of transcript.events) {
984
+ const event = safeTranscriptEvent(rawEvent);
985
+ if (event.kind === "tool") {
986
+ if (options.expandedTools && (event.output || event.argsPayload || event.error)) {
987
+ lines.push(...renderExpandedTool(event, width, theme));
988
+ lines.push(railLine(theme.fg("dim", " x to collapse"), width, theme));
989
+ continue;
990
+ }
991
+ const title = theme.fg("toolTitle", theme.bold(event.name));
992
+ const args = event.args ? ` ${theme.fg("dim", event.args)}` : "";
993
+ const suffix = event.status === "running" ? theme.fg("warning", " running") : "";
994
+ lines.push(bounded(`${theme.fg("borderMuted", "\u251C\u2500")} ${statusGlyph(event, theme)} ${title}${args}${suffix}`, width));
995
+ if (event.output && event.status !== "error" && event.name === "bash") {
996
+ const outputLines = event.output.replace(/\s+$/, "").split(/\r?\n/);
997
+ const visible = outputLines.slice(-TOOL_PREVIEW_LINES);
998
+ const hidden = Math.max(0, outputLines.length - visible.length);
999
+ for (const outputLine of visible) {
1000
+ for (const wrapped of renderWrapped(theme.fg("toolOutput", outputLine), Math.max(1, width - 4))) {
1001
+ lines.push(railLine(` ${wrapped}`, width, theme));
1002
+ }
1003
+ }
1004
+ if (hidden > 0) lines.push(railLine(theme.fg("dim", ` \u2026 ${hidden} earlier lines \xB7 x to expand`), width, theme));
1005
+ const duration = toolDuration(event);
1006
+ lines.push(railLine(theme.fg("dim", ` Took${duration ? ` ${duration}` : ""}`), width, theme));
1007
+ } else if (event.output && event.status !== "error") {
1008
+ const summary = truncateToWidth(event.output.replace(/\s+/g, " ").trim(), Math.max(1, width - 18), "\u2026");
1009
+ if (summary) lines.push(railLine(theme.fg("dim", ` ${summary} \xB7 x to expand`), width, theme));
1010
+ }
1011
+ if (event.error) {
1012
+ for (const errorLine of renderWrapped(event.error, Math.max(1, width - 4))) {
1013
+ lines.push(railLine(theme.fg("error", ` ${errorLine}`), width, theme));
1014
+ }
1015
+ }
1016
+ continue;
1017
+ }
1018
+ if (event.kind === "notice") {
1019
+ const color = event.tone === "error" ? "error" : event.tone === "warning" ? "warning" : "dim";
1020
+ for (const noticeLine of renderWrapped(event.text, Math.max(1, width - 2))) {
1021
+ lines.push(railLine(theme.fg(color, noticeLine), width, theme));
1022
+ }
1023
+ continue;
1024
+ }
1025
+ const assistant = event.kind === "assistant";
1026
+ const label = assistant ? "Assistant" : "Supervisor";
1027
+ const marker = assistant ? theme.fg("accent", "\u25C6") : theme.fg("warning", "\u25C7");
1028
+ const model = assistant && event.model ? theme.fg("dim", ` \xB7 ${event.model}`) : "";
1029
+ lines.push(bounded(`${marker} ${theme.bold(label)}${model}`, width));
1030
+ if (assistant) {
1031
+ const rendered = new Markdown(event.text, 0, 0, markdownTheme).render(Math.max(1, width - 2));
1032
+ for (const markdownLine of rendered) lines.push(railLine(markdownLine, width, theme));
1033
+ } else {
1034
+ for (const userLine of renderWrapped(event.text, Math.max(1, width - 2))) {
1035
+ lines.push(railLine(userLine, width, theme));
1036
+ }
1037
+ }
1038
+ lines.push(theme.fg("borderMuted", "\u2502"));
1039
+ }
1040
+ while (lines.length > 0 && visibleWidth(lines.at(-1) ?? "") === 1) lines.pop();
1041
+ return lines;
1042
+ }
1043
+
1044
+ // src/inspectors/types.ts
1045
+ var INSPECTOR_ACTIONS = ["inspector.open", "inspector.command", "inspector.status", "inspector.close"];
1046
+
1047
+ // src/inspectors/actions.ts
1048
+ import * as fs4 from "node:fs";
1049
+ import * as path5 from "node:path";
1050
+ import { fileURLToPath } from "node:url";
1051
+
1052
+ // src/inspectors/session-roots-codec.ts
1053
+ function encodeSessionRoots(roots) {
1054
+ return Buffer.from(JSON.stringify(roots), "utf-8").toString("base64");
1055
+ }
1056
+
1057
+ // src/inspectors/actions.ts
1058
+ var INSPECTOR_LEASE_WAIT_MS = 3e4;
1059
+ function result(text, isError = false) {
1060
+ const response = {
1061
+ content: [{ type: "text", text }],
1062
+ details: { mode: "management", results: [] }
1063
+ };
1064
+ if (isError) response.isError = true;
1065
+ return response;
1066
+ }
1067
+ function pathWithin2(base, candidate) {
1068
+ const resolvedBase = path5.resolve(base);
1069
+ const resolvedCandidate = path5.resolve(candidate);
1070
+ return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path5.sep}`);
1071
+ }
1072
+ function trustedDir(dir, deps) {
1073
+ try {
1074
+ if (fs4.lstatSync(dir).isSymbolicLink() || !fs4.statSync(dir).isDirectory()) return false;
1075
+ const real = fs4.realpathSync(dir);
1076
+ if ([...deps.state?.asyncJobs.values() ?? [], ...deps.state?.fleetJobs?.values() ?? []].some((job) => {
1077
+ try {
1078
+ return fs4.realpathSync(job.asyncDir) === real;
1079
+ } catch {
1080
+ return false;
1081
+ }
1082
+ })) return true;
1083
+ const root = deps.asyncDirRoot ?? DIRS.async;
1084
+ return fs4.existsSync(root) && pathWithin2(root, dir) && pathWithin2(fs4.realpathSync(root), real);
1085
+ } catch {
1086
+ return false;
1087
+ }
1088
+ }
1089
+ function resolveTarget(params, deps) {
1090
+ const requested = params.id ?? params.runId;
1091
+ let runId;
1092
+ let asyncDir;
1093
+ if (params.dir) {
1094
+ asyncDir = path5.resolve(params.dir);
1095
+ if (!trustedDir(asyncDir, deps)) return { error: `Async run directory '${asyncDir}' is outside trusted run roots.` };
1096
+ const status2 = readStatus(asyncDir);
1097
+ if (!status2) return { error: `No async run status found in '${asyncDir}'.` };
1098
+ if (requested && requested !== status2.runId && !status2.runId.startsWith(requested)) return { error: `Run '${requested}' does not match status run '${status2.runId}'.` };
1099
+ runId = status2.runId;
1100
+ } else {
1101
+ if (!requested) return { error: "Inspector actions require id or dir." };
1102
+ try {
1103
+ const found = resolveSubagentRunId(requested, { state: deps.state, asyncDirRoot: deps.asyncDirRoot ?? DIRS.async, resultsDir: deps.resultsDir ?? DIRS.results });
1104
+ if (!found) return { error: `No subagent run found for '${requested}'.` };
1105
+ if (found.kind !== "async" || !found.location.asyncDir) return { error: `Run '${found.id}' is not an inspectable async run with lifecycle artifacts.` };
1106
+ runId = found.id;
1107
+ asyncDir = found.location.asyncDir;
1108
+ } catch (cause) {
1109
+ return { error: cause instanceof Error ? cause.message : String(cause) };
1110
+ }
1111
+ }
1112
+ const status = readStatus(asyncDir);
1113
+ if (!status) return { error: `No lifecycle status exists for async run '${runId}'.` };
1114
+ if (params.index !== void 0 && (params.index < 0 || params.index >= (status.steps?.length ?? 0))) return { error: `Async run '${runId}' has ${status.steps?.length ?? 0} children. Index ${params.index} is out of range.` };
1115
+ const target = { runId, asyncDir, status: { cwd: status.cwd, state: status.state, steps: status.steps } };
1116
+ if (params.index !== void 0) target.index = params.index;
1117
+ return target;
1118
+ }
1119
+ function missionFor(target, deps) {
1120
+ try {
1121
+ const binding = readMissionBinding(target.asyncDir);
1122
+ if (binding) return { id: binding.missionId, path: missionRecordPath(binding.location, binding.missionId) };
1123
+ const location = resolveMissionStoreLocation(deps.missions ? { projectRoot: deps.cwd, config: deps.missions } : { projectRoot: deps.cwd });
1124
+ const mission = listMissions(location).records.find((record) => record.runs.some((run) => run.runId === target.runId));
1125
+ return mission ? { id: mission.id, path: missionRecordPath(location, mission.id) } : void 0;
1126
+ } catch {
1127
+ return void 0;
1128
+ }
1129
+ }
1130
+ function launchFor(target, deps) {
1131
+ const mission = missionFor(target, deps);
1132
+ const runnerPath = deps.runnerPath ?? fileURLToPath(new URL("../../inspector-runner.mjs", new URL("../src/inspectors/actions.js", import.meta.url).href));
1133
+ const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
1134
+ const sessionRoots = [.../* @__PURE__ */ new Set([...deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? [], ...job?.sessionRoot ? [job.sessionRoot] : []])];
1135
+ const allowSteer = resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto";
1136
+ const allowStop = resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto";
1137
+ const argv = [runnerPath, "--async-dir", target.asyncDir, "--run-id", target.runId, "--allow-steer", String(allowSteer), "--allow-stop", String(allowStop), "--session-roots", encodeSessionRoots(sessionRoots)];
1138
+ if (target.index !== void 0) argv.push("--index", String(target.index));
1139
+ if (mission) argv.push("--mission-path", mission.path);
1140
+ const executable = resolveNodeExecutable();
1141
+ const launch = { executable, argv, displayCommand: formatShellCommand(executable, argv), allowSteer, allowStop, sessionRoots };
1142
+ if (mission) launch.mission = mission;
1143
+ return launch;
1144
+ }
1145
+ async function handleInspectorAction(action, params, deps) {
1146
+ const target = resolveTarget(params, deps);
1147
+ if ("error" in target) return result(target.error, true);
1148
+ const context = {
1149
+ cwd: deps.cwd,
1150
+ signal: deps.signal,
1151
+ env: deps.env ?? process.env,
1152
+ target
1153
+ };
1154
+ if (deps.now) context.now = deps.now;
1155
+ if (action === "inspector.command") return result(launchFor(target, deps).displayCommand);
1156
+ const plugins = deps.plugins ?? [];
1157
+ if (action === "inspector.status") {
1158
+ const owner = plugins.find((plugin) => plugin.owns(context));
1159
+ if (!owner) return result(`No inspector plugin owns this binding for async run ${target.runId}.`);
1160
+ return owner.status ? owner.status(context) : result(`Inspector plugin '${owner.name}' does not support status for async run ${target.runId}.`, true);
1161
+ }
1162
+ const label = target.index === void 0 ? "" : ` child ${target.index}`;
1163
+ const lockPath = path5.join(fs4.realpathSync.native(target.asyncDir), `inspector-${target.index ?? "root"}.lock`);
1164
+ const deadline = Date.now() + (deps.leaseWaitMs ?? INSPECTOR_LEASE_WAIT_MS);
1165
+ let release = tryLease(lockPath);
1166
+ while (!release) {
1167
+ if (deps.signal?.aborted) return result(`Inspector ${action} for async run ${target.runId}${label} was cancelled while waiting for another inspector open or close to finish.`, true);
1168
+ if (Date.now() >= deadline) return result(`Another inspector open or close for async run ${target.runId}${label} is still in progress. Try again when it finishes.`, true);
1169
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
1170
+ release = tryLease(lockPath);
1171
+ }
1172
+ try {
1173
+ if (deps.signal?.aborted) return result(`Inspector ${action} for async run ${target.runId}${label} was cancelled while waiting for another inspector open or close to finish.`, true);
1174
+ if (action === "inspector.open") {
1175
+ for (const plugin of plugins) {
1176
+ if (await plugin.available(context)) return await plugin.open(context, launchFor(target, deps), params);
1177
+ }
1178
+ return result("No inspector plugin is available. Start a supported inspector host, or use inspector.command for a standalone command.", true);
1179
+ }
1180
+ const owner = plugins.find((plugin) => plugin.owns(context));
1181
+ if (!owner) return result(`No inspector plugin owns this binding for async run ${target.runId}.`);
1182
+ return owner.close ? await owner.close(context) : result(`Inspector plugin '${owner.name}' does not support close for async run ${target.runId}.`, true);
1183
+ } finally {
1184
+ release();
1185
+ }
1186
+ }
1187
+
1188
+ // src/runs/foreground/prompt-audit.ts
1189
+ var livePrompts = /* @__PURE__ */ new WeakMap();
1190
+ function fullModelId(model) {
1191
+ return `${model.provider}/${model.id}`;
1192
+ }
1193
+ function textContent(value) {
1194
+ if (typeof value === "string") return value;
1195
+ if (!Array.isArray(value)) return "";
1196
+ return value.map((item) => item && typeof item === "object" && item.type === "text" ? String(item.text ?? "") : "").join("");
1197
+ }
1198
+ function finalAssistantText(agent) {
1199
+ for (let index = agent.state.messages.length - 1; index >= 0; index--) {
1200
+ const message = agent.state.messages[index];
1201
+ if (message && typeof message === "object" && message.role === "assistant") {
1202
+ return textContent(message.content).trim();
1203
+ }
1204
+ }
1205
+ return "";
1206
+ }
1207
+ async function resolveRewriteAuth(ctx, model) {
1208
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
1209
+ if (auth.ok === false) throw new Error(`Prompt redo model auth failed for ${fullModelId(model)}: ${auth.error}`);
1210
+ return {
1211
+ ...auth.apiKey ? { apiKey: auth.apiKey } : {},
1212
+ ...auth.headers ? { headers: auth.headers } : {},
1213
+ ...auth.env ? { env: auth.env } : {}
1214
+ };
1215
+ }
1216
+ async function rewritePromptWithGuidance(input) {
1217
+ const model = input.ctx.model;
1218
+ if (!model) throw new Error("Prompt redo needs the current session model to rewrite the authored task.");
1219
+ const [{ Agent }, { convertToLlm }] = await Promise.all([
1220
+ import("@earendil-works/pi-agent-core"),
1221
+ import("@earendil-works/pi-coding-agent")
1222
+ ]);
1223
+ const auth = await resolveRewriteAuth(input.ctx, model);
1224
+ const baseStreamFn = input.streamFn ?? ((nextModel, context, streamOptions) => input.ctx.modelRegistry.streamSimple(nextModel, context, streamOptions));
1225
+ const sessionId = input.ctx.sessionManager.getSessionId();
1226
+ const streamFn = (nextModel, context, streamOptions) => baseStreamFn(nextModel, context, {
1227
+ ...streamOptions,
1228
+ ...auth.apiKey ? { apiKey: auth.apiKey } : {},
1229
+ env: auth.env || streamOptions?.env ? { ...auth.env ?? {}, ...streamOptions?.env ?? {} } : void 0,
1230
+ headers: { ...opencodeSessionHeaders(nextModel, sessionId), ...streamOptions?.headers ?? {}, ...auth.headers ?? {} }
1231
+ });
1232
+ const ctxThinking = input.ctx.getThinkingLevel?.();
1233
+ const agent = new Agent({
1234
+ initialState: {
1235
+ systemPrompt: [
1236
+ "Rewrite one subagent authored task from Prompt Audit context.",
1237
+ "Return only the revised authored task text.",
1238
+ "Do not add Markdown fences, explanations, labels, or commentary.",
1239
+ "Preserve the original intent unless the guidance explicitly changes it.",
1240
+ "Do not include runtime additions unless they are needed as ordinary task context."
1241
+ ].join("\n"),
1242
+ model,
1243
+ thinkingLevel: ctxThinking ?? "off",
1244
+ tools: []
1245
+ },
1246
+ convertToLlm,
1247
+ ...agentStreamOptions(streamFn),
1248
+ getApiKey: (providerName) => providerName === model.provider ? auth.apiKey : void 0,
1249
+ toolExecution: "sequential"
1250
+ });
1251
+ const abort = () => agent.abort();
1252
+ input.ctx.signal?.addEventListener("abort", abort, { once: true });
1253
+ input.signal?.addEventListener("abort", abort, { once: true });
1254
+ try {
1255
+ await agent.prompt([
1256
+ "Guidance from the human:",
1257
+ input.guidance.trim(),
1258
+ "",
1259
+ "Authored task:",
1260
+ input.authoredTask,
1261
+ "",
1262
+ "Runtime additions for context only:",
1263
+ input.runtimeAdditions,
1264
+ "",
1265
+ "Final effective prompt for context only:",
1266
+ input.finalEffectivePrompt
1267
+ ].join("\n"));
1268
+ } finally {
1269
+ input.ctx.signal?.removeEventListener("abort", abort);
1270
+ input.signal?.removeEventListener("abort", abort);
1271
+ }
1272
+ const rewritten = finalAssistantText(agent);
1273
+ if (!rewritten) throw new Error("Prompt redo rewrite returned an empty prompt.");
1274
+ return rewritten;
1275
+ }
1276
+ function runtimeAdditions(authoredTask, effectivePrompt) {
1277
+ if (!authoredTask) return effectivePrompt;
1278
+ const authoredIndex = effectivePrompt.indexOf(authoredTask);
1279
+ if (authoredIndex < 0) return "(runtime additions unavailable)";
1280
+ const before = effectivePrompt.slice(0, authoredIndex).trim();
1281
+ const after = effectivePrompt.slice(authoredIndex + authoredTask.length).trim();
1282
+ return [before, after].filter(Boolean).join("\n\n") || "(none)";
1283
+ }
1284
+ function registerLivePromptAudit(control, index, authoredTask, effectivePrompt, metadata = {}) {
1285
+ let prompts = livePrompts.get(control);
1286
+ if (!prompts) {
1287
+ prompts = /* @__PURE__ */ new Map();
1288
+ livePrompts.set(control, prompts);
1289
+ }
1290
+ prompts.set(index, {
1291
+ authoredTask,
1292
+ runtimeAdditions: runtimeAdditions(authoredTask, effectivePrompt),
1293
+ finalEffectivePrompt: effectivePrompt,
1294
+ ...metadata.cwd ? { cwd: metadata.cwd } : {},
1295
+ ...metadata.outputPath ? { outputPath: metadata.outputPath } : {},
1296
+ ...metadata.rerun ? { rerun: metadata.rerun } : {}
1297
+ });
1298
+ }
1299
+ function updateLiveEffectivePrompt(control, index, effectivePrompt) {
1300
+ const prompt = livePrompts.get(control)?.get(index);
1301
+ if (!prompt) return;
1302
+ prompt.finalEffectivePrompt = effectivePrompt;
1303
+ prompt.runtimeAdditions = runtimeAdditions(prompt.authoredTask, effectivePrompt);
1304
+ }
1305
+ function getLivePromptAudit(control, index) {
1306
+ return livePrompts.get(control)?.get(index);
1307
+ }
1308
+ function removeLivePromptAudit(control, index) {
1309
+ const prompts = livePrompts.get(control);
1310
+ if (!prompts) return;
1311
+ prompts.delete(index);
1312
+ if (prompts.size === 0) livePrompts.delete(control);
1313
+ }
1314
+
1315
+ export {
1316
+ steeringReceipt,
1317
+ createSteeringStatus,
1318
+ steeringStatus,
1319
+ recordSteeringRequest,
1320
+ updateSteeringTarget,
1321
+ waitForSteeringAction,
1322
+ canQueueRetainedAsyncFollowUp,
1323
+ steerAsyncRun,
1324
+ workflowAsyncChildSteeringGuidance,
1325
+ resolveWorkflowForegroundSteeringTarget,
1326
+ steerWorkflowRun,
1327
+ steerWorkflowForegroundTarget,
1328
+ readFleetTranscript,
1329
+ renderFleetTranscript,
1330
+ INSPECTOR_ACTIONS,
1331
+ handleInspectorAction,
1332
+ rewritePromptWithGuidance,
1333
+ registerLivePromptAudit,
1334
+ updateLiveEffectivePrompt,
1335
+ getLivePromptAudit,
1336
+ removeLivePromptAudit
1337
+ };