@llblab/pi-actors 0.54.0 → 0.56.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 (188) hide show
  1. package/AGENTS.md +110 -20
  2. package/BACKLOG.md +207 -4
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +33 -3
  5. package/dist/index.js +2 -1
  6. package/dist/lib/async-runs.d.ts +37 -0
  7. package/dist/lib/async-runs.js +131 -62
  8. package/dist/lib/automatic-review-runtime.d.ts +2 -0
  9. package/dist/lib/automatic-review-runtime.js +3 -2
  10. package/dist/lib/command-templates.d.ts +37 -0
  11. package/dist/lib/command-templates.js +263 -129
  12. package/dist/lib/execution-files.d.ts +32 -0
  13. package/dist/lib/execution-files.js +128 -0
  14. package/dist/lib/execution-output.js +5 -4
  15. package/dist/lib/execution-sessions.d.ts +19 -1
  16. package/dist/lib/execution-sessions.js +62 -26
  17. package/dist/lib/execution.d.ts +4 -1
  18. package/dist/lib/execution.js +6 -1
  19. package/dist/lib/extension-runtime.d.ts +10 -2
  20. package/dist/lib/extension-runtime.js +95 -14
  21. package/dist/lib/file-state.js +21 -2
  22. package/dist/lib/inspector-command.js +2 -2
  23. package/dist/lib/inspector-overlay.d.ts +25 -7
  24. package/dist/lib/inspector-overlay.js +562 -262
  25. package/dist/lib/inspector.d.ts +20 -10
  26. package/dist/lib/inspector.js +80 -91
  27. package/dist/lib/limits.d.ts +1 -0
  28. package/dist/lib/limits.js +1 -0
  29. package/dist/lib/observability.d.ts +15 -1
  30. package/dist/lib/observability.js +216 -99
  31. package/dist/lib/pi.d.ts +9 -3
  32. package/dist/lib/pi.js +80 -12
  33. package/dist/lib/recipe-projection.d.ts +18 -0
  34. package/dist/lib/recipe-projection.js +220 -0
  35. package/dist/lib/recipes-context.d.ts +2 -0
  36. package/dist/lib/recipes-context.js +30 -6
  37. package/dist/lib/registry.js +270 -36
  38. package/dist/lib/run-delivery.d.ts +213 -5
  39. package/dist/lib/run-delivery.js +1909 -40
  40. package/dist/lib/run-supervision.d.ts +107 -0
  41. package/dist/lib/run-supervision.js +412 -0
  42. package/dist/lib/run-ui-runtime.d.ts +10 -2
  43. package/dist/lib/run-ui-runtime.js +347 -124
  44. package/dist/lib/runs-artifacts.d.ts +26 -1
  45. package/dist/lib/runs-artifacts.js +68 -2
  46. package/dist/lib/runs-context-exports.d.ts +451 -0
  47. package/dist/lib/runs-context-exports.js +2315 -0
  48. package/dist/lib/runs-context-index.d.ts +97 -0
  49. package/dist/lib/runs-context-index.js +368 -0
  50. package/dist/lib/runs-control-delivery.js +46 -7
  51. package/dist/lib/runs-control.d.ts +78 -2
  52. package/dist/lib/runs-control.js +334 -2
  53. package/dist/lib/runs-controls.js +4 -1
  54. package/dist/lib/runs-descendant-launches.d.ts +80 -0
  55. package/dist/lib/runs-descendant-launches.js +287 -0
  56. package/dist/lib/runs-execution-evidence.d.ts +110 -0
  57. package/dist/lib/runs-execution-evidence.js +282 -0
  58. package/dist/lib/runs-index.d.ts +66 -1
  59. package/dist/lib/runs-index.js +349 -13
  60. package/dist/lib/runs-native-launches.d.ts +73 -0
  61. package/dist/lib/runs-native-launches.js +227 -0
  62. package/dist/lib/runs-outcomes.d.ts +141 -0
  63. package/dist/lib/runs-outcomes.js +292 -0
  64. package/dist/lib/runs-ownership.d.ts +10 -1
  65. package/dist/lib/runs-ownership.js +36 -4
  66. package/dist/lib/runs-process-windows-mi.d.ts +46 -0
  67. package/dist/lib/runs-process-windows-mi.js +114 -0
  68. package/dist/lib/runs-process.d.ts +34 -1
  69. package/dist/lib/runs-process.js +158 -26
  70. package/dist/lib/runs-results.d.ts +116 -0
  71. package/dist/lib/runs-results.js +518 -0
  72. package/dist/lib/runs-retention.d.ts +15 -2
  73. package/dist/lib/runs-retention.js +148 -9
  74. package/dist/lib/runs-session-exports.d.ts +95 -0
  75. package/dist/lib/runs-session-exports.js +470 -0
  76. package/dist/lib/runs-snapshot.d.ts +58 -0
  77. package/dist/lib/runs-snapshot.js +92 -0
  78. package/dist/lib/runs-start.d.ts +2 -0
  79. package/dist/lib/runs-start.js +44 -3
  80. package/dist/lib/runs-status.d.ts +15 -1
  81. package/dist/lib/runs-status.js +337 -13
  82. package/dist/lib/runs-supervision.d.ts +105 -0
  83. package/dist/lib/runs-supervision.js +501 -0
  84. package/dist/lib/runs-trace.d.ts +1 -0
  85. package/dist/lib/runs-trace.js +14 -3
  86. package/dist/lib/runtime.js +22 -3
  87. package/dist/lib/session-evidence.d.ts +37 -0
  88. package/dist/lib/session-evidence.js +59 -3
  89. package/dist/lib/state-readers.d.ts +4 -1
  90. package/dist/lib/state-readers.js +14 -8
  91. package/dist/lib/temp.d.ts +2 -4
  92. package/dist/lib/temp.js +4 -56
  93. package/dist/lib/tools-access.d.ts +3 -0
  94. package/dist/lib/tools-access.js +13 -0
  95. package/dist/lib/tools-inspect.d.ts +1 -0
  96. package/dist/lib/tools-inspect.js +110 -43
  97. package/dist/lib/tools-local.d.ts +3 -0
  98. package/dist/lib/tools-local.js +18 -6
  99. package/dist/lib/tools-message.d.ts +4 -1
  100. package/dist/lib/tools-message.js +126 -31
  101. package/dist/lib/tools-response.d.ts +31 -3
  102. package/dist/lib/tools-response.js +311 -47
  103. package/dist/lib/tools-spawn.d.ts +3 -0
  104. package/dist/lib/tools-spawn.js +29 -7
  105. package/dist/lib/trace-projection.d.ts +20 -0
  106. package/dist/lib/trace-projection.js +192 -43
  107. package/dist/scripts/async-runner.mjs +272 -194
  108. package/dist/scripts/conformance.mjs +14 -2
  109. package/dist/skills/actors/SKILL.md +47 -3
  110. package/dist/skills/actors/references/diagnostics.md +40 -0
  111. package/dist/skills/actors/references/runs.md +56 -2
  112. package/docs/README.md +9 -1
  113. package/docs/actor-inspector.md +84 -15
  114. package/docs/agent-experience.md +216 -0
  115. package/docs/async-runs.md +21 -3
  116. package/docs/composition-transport.md +103 -0
  117. package/docs/coordinator-delivery.md +226 -24
  118. package/docs/inspection.md +38 -2
  119. package/docs/output-projection-map.md +130 -0
  120. package/docs/releasing.md +19 -2
  121. package/docs/run-supervision.md +1622 -0
  122. package/docs/template-recipes.md +4 -0
  123. package/docs/windows-process-observation.md +248 -0
  124. package/index.ts +2 -1
  125. package/lib/async-runs.ts +164 -73
  126. package/lib/automatic-review-runtime.ts +4 -2
  127. package/lib/command-templates.ts +287 -138
  128. package/lib/execution-files.ts +118 -0
  129. package/lib/execution-output.ts +4 -4
  130. package/lib/execution-sessions.ts +57 -27
  131. package/lib/execution.ts +8 -0
  132. package/lib/extension-runtime.ts +106 -12
  133. package/lib/file-state.ts +16 -2
  134. package/lib/inspector-command.ts +2 -2
  135. package/lib/inspector-overlay.ts +524 -261
  136. package/lib/inspector.ts +101 -130
  137. package/lib/limits.ts +1 -0
  138. package/lib/observability.ts +235 -116
  139. package/lib/pi.ts +89 -17
  140. package/lib/recipe-projection.ts +232 -0
  141. package/lib/recipes-context.ts +27 -6
  142. package/lib/registry.ts +266 -36
  143. package/lib/run-delivery.ts +1737 -49
  144. package/lib/run-supervision.ts +411 -0
  145. package/lib/run-ui-runtime.ts +293 -126
  146. package/lib/runs-artifacts.ts +73 -1
  147. package/lib/runs-context-exports.ts +1871 -0
  148. package/lib/runs-context-index.ts +372 -0
  149. package/lib/runs-control-delivery.ts +36 -18
  150. package/lib/runs-control.ts +297 -1
  151. package/lib/runs-controls.ts +3 -1
  152. package/lib/runs-descendant-launches.ts +338 -0
  153. package/lib/runs-execution-evidence.ts +302 -0
  154. package/lib/runs-index.ts +345 -17
  155. package/lib/runs-native-launches.ts +270 -0
  156. package/lib/runs-outcomes.ts +377 -0
  157. package/lib/runs-ownership.ts +37 -4
  158. package/lib/runs-process-windows-mi.ts +113 -0
  159. package/lib/runs-process.ts +210 -34
  160. package/lib/runs-results.ts +601 -0
  161. package/lib/runs-retention.ts +134 -10
  162. package/lib/runs-session-exports.ts +406 -0
  163. package/lib/runs-snapshot.ts +143 -0
  164. package/lib/runs-start.ts +44 -2
  165. package/lib/runs-status.ts +252 -10
  166. package/lib/runs-supervision.ts +394 -0
  167. package/lib/runs-trace.ts +14 -3
  168. package/lib/runtime.ts +22 -3
  169. package/lib/session-evidence.ts +76 -4
  170. package/lib/state-readers.ts +16 -7
  171. package/lib/temp.ts +3 -56
  172. package/lib/tools-access.ts +13 -0
  173. package/lib/tools-inspect.ts +110 -51
  174. package/lib/tools-local.ts +37 -24
  175. package/lib/tools-message.ts +120 -37
  176. package/lib/tools-response.ts +273 -50
  177. package/lib/tools-spawn.ts +31 -10
  178. package/lib/trace-projection.ts +165 -42
  179. package/package.json +10 -6
  180. package/scripts/async-runner.mjs +272 -194
  181. package/scripts/conformance.mjs +14 -2
  182. package/skills/actors/SKILL.md +47 -3
  183. package/skills/actors/references/diagnostics.md +40 -0
  184. package/skills/actors/references/runs.md +56 -2
  185. package/banner.jpg +0 -0
  186. package/dist/lib/inspector-actions.d.ts +0 -16
  187. package/dist/lib/inspector-actions.js +0 -57
  188. package/lib/inspector-actions.ts +0 -79
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Ambient run observability runtime.
3
3
  * Zones: run watcher lifecycle, terminal reconciliation, status animation, shutdown teardown
4
- * Owns event-driven run UI coordination without owning actor execution semantics.
4
+ * Owns event-driven run UI/delivery coordination and callback-generation fences; excludes actor execution semantics and public stream activation.
5
5
  */
6
+ import { Container, Markdown, Spacer, Text, stripTerminalSequences } from "@earendil-works/pi-tui";
6
7
  import * as AsyncRuns from "./async-runs.js";
7
8
  import * as Limits from "./limits.js";
8
9
  import * as Observability from "./observability.js";
@@ -10,10 +11,101 @@ import * as Paths from "./paths.js";
10
11
  import * as Pi from "./pi.js";
11
12
  import * as RunDelivery from "./run-delivery.js";
12
13
  import * as RunDeliveryLineage from "./run-delivery-lineage.js";
14
+ import * as RunIndex from "./runs-index.js";
15
+ import { formatLiteralBlock, formatMarkdownField } from "./tools-response.js";
16
+ function formatSteerDiagnosticMessage(eventId, stage, message) {
17
+ const fields = [formatMarkdownField("event", eventId), formatMarkdownField("attention", "steer"),
18
+ formatMarkdownField("stage", stage), formatMarkdownField("result", stage === "admission" ? "remains retryable" : "request failed")];
19
+ if (stage === "delivery")
20
+ fields.push(formatMarkdownField("delivery", "unconfirmed"), formatMarkdownField("presentation", "unconfirmed"));
21
+ let end = Math.min(message.length, 240);
22
+ const last = message.charCodeAt(end - 1), next = message.charCodeAt(end);
23
+ if (last >= 0xd800 && last <= 0xdbff && next >= 0xdc00 && next <= 0xdfff)
24
+ end -= 1;
25
+ const preview = message.slice(0, end), clipped = end < message.length;
26
+ if (Buffer.from(preview).toString("utf8") !== preview) {
27
+ fields.push(formatMarkdownField("diagnostic", "unavailable"), formatMarkdownField("diagnostic_reason", "malformed legacy preview"), formatMarkdownField("diagnostic_omission", "[omitted: unknown]"));
28
+ }
29
+ else {
30
+ fields.push(formatMarkdownField("diagnostic", preview + (clipped ? "…" : "")));
31
+ if (clipped)
32
+ fields.push(formatMarkdownField("diagnostic_omission", "[omitted: unknown]"));
33
+ }
34
+ return fields.join("\n");
35
+ }
36
+ /** Transcript-only view of the exact delivered content; never re-read a Run or infer body labels. */
37
+ export function registerRunCompletionRenderer(pi) {
38
+ pi.registerMessageRenderer(Pi.RUN_COMPLETION_BATCH_CUSTOM_TYPE, (message, { expanded, outputPad }, theme) => {
39
+ if (typeof message.content !== "string")
40
+ return undefined;
41
+ let presentation;
42
+ try {
43
+ presentation = Object.getOwnPropertyDescriptor(message.details, "pi_actors_presentation")?.value;
44
+ }
45
+ catch { /* no layout hint */ }
46
+ const view = RunDelivery.readRunCompletionPresentation(message.content, presentation);
47
+ if (!view)
48
+ return undefined;
49
+ if (!expanded && view.segments) {
50
+ const component = new Container(), markdownTheme = { ...Pi.getMarkdownTheme(), codeBlockIndent: "" };
51
+ component.addChild(new Spacer(1));
52
+ let pieces = 0;
53
+ for (const segment of view.segments) {
54
+ // Only compiler separator/marker chunks drive spacing; whitespace bodies remain Markdown.
55
+ if (segment.kind === "separator" || (segment.kind === "marker" && /^\n+$/u.test(segment.text)))
56
+ continue;
57
+ if (pieces++)
58
+ component.addChild(new Spacer(1));
59
+ const text = stripTerminalSequences(segment.text).replaceAll(/\r\n|\r/gu, "\n");
60
+ component.addChild(segment.kind === "body"
61
+ ? new Markdown(text, outputPad, 0, markdownTheme, { color: value => theme.fg("customMessageText", value) })
62
+ : new Text(theme.fg("muted", text), outputPad, 0));
63
+ }
64
+ component.addChild(new Spacer(1));
65
+ component.addChild(new Text(theme.fg("muted", "Expand message for technical details."), outputPad, 0));
66
+ component.addChild(new Spacer(1));
67
+ return component;
68
+ }
69
+ if (!expanded && view.blocks) {
70
+ const component = new Container(), markdownTheme = { ...Pi.getMarkdownTheme(), codeBlockIndent: "" };
71
+ component.addChild(new Spacer(1));
72
+ for (const [index, [kind, start, end]] of view.blocks.entries()) {
73
+ if (index)
74
+ component.addChild(new Spacer(1));
75
+ let text = stripTerminalSequences(message.content.slice(start, end)).replaceAll(/\r\n|\r/gu, "\n");
76
+ if (kind === "header") {
77
+ // Only validated compiler headers may hide whole reference rows; body lookalikes stay visible.
78
+ text = text.split("\n").filter(line => !line.startsWith("references: ") && !line.startsWith("run_files: ")).join("\n").replace(/\n$/u, "");
79
+ }
80
+ component.addChild(kind === "body"
81
+ ? new Markdown(text, outputPad, 0, markdownTheme, { color: value => theme.fg("customMessageText", value) })
82
+ : new Text(theme.fg(kind === "header" || kind === "marker" ? "muted" : "customMessageText", text), outputPad, 0));
83
+ }
84
+ component.addChild(new Spacer(1));
85
+ component.addChild(new Text(theme.fg("muted", "Expand message for technical details."), outputPad, 0));
86
+ component.addChild(new Spacer(1));
87
+ return component;
88
+ }
89
+ const text = expanded ? message.content : view.text;
90
+ const label = view.yaml || (view.records && !view.single) ? "" : theme.fg("customMessageLabel", view.single ? "Run result" : "Run Batch") + "\n\n";
91
+ return new Text(label + theme.fg("customMessageText", stripTerminalSequences(text).replaceAll(/\r\n|\r/gu, "\n"))
92
+ + (!expanded && view.compact ? "\n\n" + theme.fg("muted", "Expand message for technical details.") : ""), outputPad, 1);
93
+ });
94
+ }
13
95
  export function createRunUiRuntime(deps) {
96
+ if (deps.captureCompletionBatch && deps.completionStreamSessionAdmission)
97
+ throw new Error("Candidate completion capture cannot use completion streams");
98
+ const fencedCompletionSession = Boolean(deps.captureCompletionBatch || deps.completionStreamSessionAdmission);
14
99
  let activeContext;
15
100
  let activeOwnerId;
101
+ let sessionTicket;
102
+ let sessionManager;
103
+ let streamSessionAdmitted = false;
104
+ const contextTickets = new WeakMap();
105
+ const seenManagers = new WeakSet();
16
106
  let animationInterval;
107
+ let displayRunningSubagents = 0;
108
+ let displayFrame = 0;
17
109
  let deliveryTimeout;
18
110
  let notifyTimeout;
19
111
  let recoverQueuedSteers = false;
@@ -23,8 +115,14 @@ export function createRunUiRuntime(deps) {
23
115
  const deliveryObservation = Observability.createRunUiObservationState();
24
116
  const retirementAttempts = new Set();
25
117
  const steerDiagnostics = new Set();
118
+ const completionDiagnostics = new Set();
26
119
  const close = () => {
27
120
  running = false;
121
+ displayRunningSubagents = 0;
122
+ displayFrame = 0;
123
+ sessionTicket = undefined;
124
+ sessionManager = undefined;
125
+ streamSessionAdmitted = false;
28
126
  activeContext = undefined;
29
127
  activeOwnerId = undefined;
30
128
  try {
@@ -47,6 +145,7 @@ export function createRunUiRuntime(deps) {
47
145
  deliveryTimeout = undefined;
48
146
  recoverQueuedSteers = false;
49
147
  steerDiagnostics.clear();
148
+ completionDiagnostics.clear();
50
149
  if (animationInterval)
51
150
  clearInterval(animationInterval);
52
151
  animationInterval = undefined;
@@ -63,14 +162,15 @@ export function createRunUiRuntime(deps) {
63
162
  }
64
163
  const message = error instanceof Error ? error.message : String(error);
65
164
  try {
66
- expectedContext.ui.notify(`Actor ${label} failed: ${message}`, "error");
165
+ expectedContext.ui.notify([formatMarkdownField("callback", label), formatMarkdownField("result", "failed"),
166
+ formatMarkdownField("diagnostic", message)].join("\n"), "error");
67
167
  }
68
168
  catch {
69
169
  /* stale context or unavailable UI */
70
170
  }
71
171
  };
72
- const runActiveCallback = (label, callback) => {
73
- if (!running || !activeContext || !activeOwnerId)
172
+ const runActiveCallback = (label, callback, expectedTicket) => {
173
+ if (expectedTicket !== sessionTicket || !running || !activeContext || !activeOwnerId)
74
174
  return;
75
175
  const ctx = activeContext;
76
176
  try {
@@ -79,7 +179,8 @@ export function createRunUiRuntime(deps) {
79
179
  callback(ctx, activeOwnerId);
80
180
  }
81
181
  catch (error) {
82
- stopAfterCallbackFailure(label, error, ctx);
182
+ if (expectedTicket === sessionTicket)
183
+ stopAfterCallbackFailure(label, error, ctx);
83
184
  }
84
185
  };
85
186
  const retireCandidateRuns = (ctx, summary) => {
@@ -124,17 +225,6 @@ export function createRunUiRuntime(deps) {
124
225
  finishPresentedSteer(ownerId, steer);
125
226
  }
126
227
  };
127
- const boundedSteerContent = (event) => {
128
- const text = Observability.formatRunAttentionMessage(event);
129
- if (Buffer.byteLength(text, "utf8") <= Limits.RUN_DELIVERY_STEER_MAX_BYTES) {
130
- return text;
131
- }
132
- let end = Math.min(text.length, Limits.RUN_DELIVERY_STEER_MAX_BYTES - 3);
133
- while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf8") >
134
- Limits.RUN_DELIVERY_STEER_MAX_BYTES - 3)
135
- end -= 1;
136
- return `${text.slice(0, end)}…`;
137
- };
138
228
  const admitSteerEvents = (ctx, ownerId, events) => {
139
229
  for (const event of events.filter(Observability.isRunSteerAttentionEvent)) {
140
230
  if (!event.runInstanceId) {
@@ -142,13 +232,16 @@ export function createRunUiRuntime(deps) {
142
232
  const key = `${event.stateDir}:${event.id}:missing_generation`;
143
233
  if (!steerDiagnostics.has(key)) {
144
234
  steerDiagnostics.add(key);
145
- ctx.ui.notify(`Actor urgent steer ${event.id} remains retryable because its Run generation is unavailable.`, "warning");
235
+ ctx.ui.notify(formatSteerDiagnosticMessage(event.id, "admission", "Run generation unavailable."), "warning");
146
236
  }
147
237
  continue;
148
238
  }
149
239
  try {
240
+ if (journal(ownerId).steers.some(steer => steer.run === event.run && steer.state_dir === event.stateDir &&
241
+ steer.run_instance_id === event.runInstanceId && steer.event_id === event.id))
242
+ continue;
150
243
  RunDelivery.admitRunSteerEnvelope({
151
- content: boundedSteerContent(event),
244
+ content: Observability.formatRunSteerMessage(event),
152
245
  eventId: event.id,
153
246
  kind: event.kind,
154
247
  level: event.level,
@@ -166,25 +259,132 @@ export function createRunUiRuntime(deps) {
166
259
  const key = `${event.stateDir}:${event.id}:${message}`;
167
260
  if (!steerDiagnostics.has(key)) {
168
261
  steerDiagnostics.add(key);
169
- ctx.ui.notify(`Actor urgent steer ${event.id} remains retryable: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "warning");
262
+ ctx.ui.notify(formatSteerDiagnosticMessage(event.id, "admission", message), "warning");
170
263
  }
171
264
  }
172
265
  }
173
266
  };
174
- const admitCompletionTransitions = (ownerId, snapshot) => {
267
+ const admitCompletionTransitions = (ctx, ownerId, snapshot) => {
175
268
  const existing = journal(ownerId).completion_batch;
176
269
  if (existing)
177
270
  return true;
178
271
  const members = Observability.collectRunCompletionBatchMembers(snapshot.transitions, snapshot.summary.runs).slice(0, Limits.RUN_DELIVERY_BATCH_MAX_MEMBERS);
179
- if (members.length === 0)
272
+ if (members.length === 0 || !isCurrentOwner(ctx))
273
+ return false;
274
+ const fileNavigation = [];
275
+ for (const member of RunDelivery.orderRunCompletionMembers(members, true)) {
276
+ let locations;
277
+ try {
278
+ const observed = snapshot.summary.runs.find(run => run.run === member.run && run.runInstanceId === member.run_instance_id && run.stateDir === member.state_dir);
279
+ locations = !observed ? Object.freeze({ state: "unavailable", reason: "identity_unavailable" })
280
+ : observed.ownerId !== ownerId ? Object.freeze({ state: "unavailable", reason: "session_mismatch" })
281
+ : RunIndex.readRunDirectoryNavigation(member.state_dir, { run: member.run, run_instance_id: member.run_instance_id, ownerId: observed.ownerId }, Pi.getSessionId(ctx));
282
+ }
283
+ catch {
284
+ locations = Object.freeze({ state: "unavailable", reason: "navigation_unavailable" });
285
+ }
286
+ fileNavigation.push({ member_index: members.indexOf(member), locations });
287
+ if (Buffer.byteLength(JSON.stringify(fileNavigation), "utf8") > Limits.RUN_DELIVERY_MODEL_MAX_BYTES) {
288
+ fileNavigation.pop();
289
+ break;
290
+ }
291
+ }
292
+ if (!isCurrentOwner(ctx))
180
293
  return false;
181
- RunDelivery.admitRunCompletionBatch({
294
+ if (!canUseCompletionSession(ctx, ownerId))
295
+ return false;
296
+ const capture = deps.completionStreamSessionAdmission ? RunDelivery.captureRunCompletionStreamBatch : deps.captureCompletionBatch ?? RunDelivery.admitRunCompletionBatch;
297
+ capture({
182
298
  members,
299
+ fileNavigation,
183
300
  ownerId,
184
301
  tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
185
302
  });
186
303
  return true;
187
304
  };
305
+ const acceptCompletionBatch = (ownerId, batchId) => {
306
+ const input = { batchId, ownerId, tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir };
307
+ if (!RunDelivery.markRunCompletionBatchQueued(input))
308
+ return false;
309
+ if (!RunDelivery.markRunCompletionBatchPresented(input))
310
+ return false;
311
+ const accepted = journal(ownerId).completion_batch;
312
+ return accepted ? finishPresentedBatch(ownerId, accepted) : false;
313
+ };
314
+ const reconcileCompletionBatch = (ctx, ownerId) => {
315
+ const batch = journal(ownerId).completion_batch;
316
+ if (!batch)
317
+ return false;
318
+ if (batch.phase === "pending") {
319
+ // Returning boundary drafts is not acceptance: prove the exact entry before acknowledging it.
320
+ const evidence = Pi.inspectRunCompletionBatchSessionEvidence(ctx, {
321
+ batchId: batch.batch_id,
322
+ content: RunDelivery.formatRunCompletionBatchMessage(batch),
323
+ });
324
+ if (evidence.status === "absent")
325
+ return false;
326
+ if (evidence.status !== "present") {
327
+ const key = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
328
+ if (!completionDiagnostics.has(key)) {
329
+ completionDiagnostics.add(key);
330
+ ctx.ui.notify("\n" + [formatMarkdownField("batch", batch.batch_id), formatMarkdownField("stage", "completion recovery"),
331
+ formatMarkdownField("evidence", evidence.status), formatMarkdownField("phase", "pending"),
332
+ formatMarkdownField("reason", evidence.reason ?? "conflicting session evidence")].join("\n"), "warning");
333
+ }
334
+ return true;
335
+ }
336
+ }
337
+ if (fencedCompletionSession && !canUseCompletionSession(ctx, ownerId))
338
+ return true;
339
+ return !acceptCompletionBatch(ownerId, batch.batch_id);
340
+ };
341
+ const pendingCompletionBatch = (ctx, ownerId) => {
342
+ const existing = journal(ownerId).completion_batch;
343
+ if (existing)
344
+ return existing;
345
+ const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
346
+ admitCompletionTransitions(ctx, ownerId, snapshot);
347
+ Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
348
+ return journal(ownerId).completion_batch;
349
+ };
350
+ const isCurrentOwner = (ctx) => {
351
+ if (!running || !activeOwnerId)
352
+ return false;
353
+ if (!fencedCompletionSession) {
354
+ try {
355
+ return Pi.getSessionId(ctx) === activeOwnerId;
356
+ }
357
+ catch {
358
+ return false;
359
+ }
360
+ }
361
+ const ticket = sessionTicket, ownerId = activeOwnerId, known = contextTickets.get(ctx);
362
+ if (!ticket || (known && known !== ticket))
363
+ return false;
364
+ try {
365
+ if (deps.getActiveContext() !== activeContext)
366
+ return false;
367
+ const manager = ctx.sessionManager;
368
+ if (manager !== sessionManager || manager.getSessionId() !== ownerId || ticket !== sessionTicket || !running)
369
+ return false;
370
+ contextTickets.set(ctx, ticket);
371
+ return true;
372
+ }
373
+ catch {
374
+ return false;
375
+ }
376
+ };
377
+ const canUseCompletionSession = (ctx, ownerId) => {
378
+ if (deps.captureCompletionBatch && !isCurrentOwner(ctx))
379
+ return false;
380
+ if (!deps.completionStreamSessionAdmission)
381
+ return !journal(ownerId).completion_stream;
382
+ if (!isCurrentOwner(ctx))
383
+ return false;
384
+ const ticket = sessionTicket, state = journal(ownerId).completion_stream;
385
+ // No metadata is not disconnected. The opt-in path never bootstraps or falls back to local indices.
386
+ return streamSessionAdmitted && Boolean(state) && ticket === sessionTicket && isCurrentOwner(ctx);
387
+ };
188
388
  const isIdle = (ctx) => typeof ctx.isIdle !== "function" || ctx.isIdle();
189
389
  const isDeliveryRoot = (ownerId) => {
190
390
  const inheritedOwner = RunDeliveryLineage.getProcessDeliveryOwnerId();
@@ -213,7 +413,9 @@ export function createRunUiRuntime(deps) {
213
413
  const key = `${steer.steer_id}:${evidence.status}:${evidence.reason ?? ""}`;
214
414
  if (!steerDiagnostics.has(key)) {
215
415
  steerDiagnostics.add(key);
216
- ctx.ui.notify(`Actor urgent steer recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Event ${steer.event_id} remains queued.`, "warning");
416
+ ctx.ui.notify([formatMarkdownField("event", steer.event_id), formatMarkdownField("attention", "steer"),
417
+ formatMarkdownField("stage", "recovery"), formatMarkdownField("evidence", evidence.status),
418
+ formatMarkdownField("phase", "queued"), formatMarkdownField("reason", evidence.reason ?? "conflicting session evidence")].join("\n"), "warning");
217
419
  }
218
420
  continue;
219
421
  }
@@ -242,7 +444,7 @@ export function createRunUiRuntime(deps) {
242
444
  tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
243
445
  });
244
446
  const message = error instanceof Error ? error.message : String(error);
245
- ctx.ui.notify(`Actor urgent steer delivery failed for event ${steer.event_id}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "error");
447
+ ctx.ui.notify(formatSteerDiagnosticMessage(steer.event_id, "delivery", message), "error");
246
448
  continue;
247
449
  }
248
450
  RunDelivery.markRunSteerQueued({
@@ -257,28 +459,45 @@ export function createRunUiRuntime(deps) {
257
459
  const scheduleCompletionFlush = () => {
258
460
  if (!running)
259
461
  return;
462
+ const ticket = sessionTicket;
260
463
  if (deliveryTimeout)
261
464
  clearTimeout(deliveryTimeout);
262
465
  deliveryTimeout = setTimeout(() => {
466
+ if (ticket !== sessionTicket)
467
+ return;
263
468
  deliveryTimeout = undefined;
264
469
  runActiveCallback("completion delivery callback", (ctx) => {
265
470
  flushCompletionBatch(ctx);
266
- });
471
+ }, ticket);
267
472
  }, deps.deliveryDebounceMs ?? 100);
268
473
  deliveryTimeout.unref?.();
269
474
  };
270
475
  const update = (ctx, ownerId, notify = false, terminalOnly = false) => {
271
- const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
272
- ctx.ui.setStatus("zz-pi-actors-runs", snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined);
273
- if (!notify)
476
+ // Derive both projections before any UI/retirement callback. The subsequent
477
+ // flush still observes afresh; these facts only drive display/candidate scheduling.
478
+ const ticket = sessionTicket;
479
+ const pair = notify && isDeliveryRoot(ownerId)
480
+ ? Observability.readRunUiAndDeliverySnapshots(observation, deliveryObservation, ownerId)
481
+ : undefined;
482
+ const snapshot = pair?.ui ?? Observability.readRunUiSnapshot(observation, ownerId);
483
+ if (ticket !== sessionTicket || (fencedCompletionSession && !isCurrentOwner(ctx)))
484
+ return false;
485
+ // Animation retains display data only; all admission paths continue observing independently.
486
+ displayRunningSubagents = snapshot.summary.runningSubagents;
487
+ displayFrame = 0;
488
+ const status = snapshot.status && ctx.mode === "tui" ? ctx.ui.theme.fg("dim", snapshot.status) : snapshot.status;
489
+ if (ticket !== sessionTicket)
490
+ return false;
491
+ ctx.ui.setStatus("zz-pi-actors-runs", status);
492
+ if (ticket !== sessionTicket)
493
+ return false;
494
+ if (!notify || (fencedCompletionSession && !isCurrentOwner(ctx)))
274
495
  return false;
275
496
  const sink = Pi.createNotificationSink(deps.pi, ctx);
276
497
  retireCandidateRuns(ctx, snapshot.summary);
277
- const deliverySnapshot = isDeliveryRoot(ownerId)
278
- ? Observability.readRunDeliverySnapshot(deliveryObservation, ownerId)
279
- : undefined;
498
+ const deliverySnapshot = pair?.delivery;
280
499
  const hasCompletionCandidates = deliverySnapshot
281
- ? Observability.collectRunCompletionBatchMembers(deliverySnapshot.transitions, deliverySnapshot.summary.runs).length > 0
500
+ ? Observability.hasRunCompletionBatchMembers(deliverySnapshot.transitions, deliverySnapshot.summary.runs)
282
501
  : false;
283
502
  const hasCompletionBatch = isDeliveryRoot(ownerId) &&
284
503
  Boolean(journal(ownerId).completion_batch);
@@ -304,30 +523,35 @@ export function createRunUiRuntime(deps) {
304
523
  ctx.ui.notify(diagnostic.message, diagnostic.code === "rearmed" ? "info" : "warning");
305
524
  }
306
525
  };
307
- const scheduleUpdate = () => {
308
- if (!running)
526
+ const scheduleUpdate = (ticket) => {
527
+ if (ticket !== sessionTicket || !running)
309
528
  return;
310
529
  if (notifyTimeout)
311
530
  clearTimeout(notifyTimeout);
312
531
  notifyTimeout = setTimeout(() => {
532
+ if (ticket !== sessionTicket)
533
+ return;
313
534
  runActiveCallback("Run watcher callback", (ctx, ownerId) => {
314
535
  watcher.refresh();
315
536
  const completionDeferred = update(ctx, ownerId, true);
537
+ if (ticket !== sessionTicket)
538
+ return;
316
539
  if (!completionDeferred)
317
540
  deps.onRunEvent();
318
- reportDiagnostics(ctx);
319
- });
541
+ if (ticket === sessionTicket)
542
+ reportDiagnostics(ctx);
543
+ }, ticket);
320
544
  }, deps.notificationDelayMs ?? 50);
321
545
  notifyTimeout.unref?.();
322
546
  };
323
- const watcher = (deps.createRunStateWatcher ?? Observability.createRunStateWatcher)({
547
+ const createWatcher = (ticket) => (deps.createRunStateWatcher ?? Observability.createRunStateWatcher)({
324
548
  stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
325
- onChange: scheduleUpdate,
549
+ onChange: () => scheduleUpdate(ticket),
326
550
  });
327
- const reconciliation = (deps.createRunTerminalReconciliationLoop ??
551
+ const createReconciliation = (ticket) => (deps.createRunTerminalReconciliationLoop ??
328
552
  Observability.createRunTerminalReconciliationLoop)({
329
553
  onError: (error) => {
330
- if (!running || !activeContext)
554
+ if (ticket !== sessionTicket || !running || !activeContext)
331
555
  return;
332
556
  stopAfterCallbackFailure("terminal reconciliation callback", error, activeContext);
333
557
  },
@@ -335,67 +559,41 @@ export function createRunUiRuntime(deps) {
335
559
  runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
336
560
  update(ctx, ownerId, true);
337
561
  reportDiagnostics(ctx);
338
- });
562
+ }, ticket);
339
563
  },
340
564
  refreshWatcher: () => {
341
- if (running)
565
+ if (ticket === sessionTicket && running)
342
566
  watcher.refresh();
343
567
  },
344
568
  });
569
+ let watcher = createWatcher(undefined);
570
+ let reconciliation = createReconciliation(undefined);
345
571
  flushCompletionBatch = (ctx) => {
346
572
  const ownerId = activeOwnerId;
347
- if (!running || !ownerId)
348
- return false;
349
- try {
350
- if (Pi.getSessionId(ctx) !== ownerId)
351
- return false;
352
- }
353
- catch {
573
+ if (!ownerId || !isCurrentOwner(ctx))
354
574
  return false;
355
- }
356
575
  if (flushSteers(ctx, ownerId))
357
576
  return true;
358
577
  if (!isDeliveryRoot(ownerId))
359
578
  return false;
360
- let batch = journal(ownerId).completion_batch;
361
- if (!batch) {
362
- const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
363
- admitCompletionTransitions(ownerId, snapshot);
364
- Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
365
- batch = journal(ownerId).completion_batch;
366
- }
579
+ if (!canUseCompletionSession(ctx, ownerId))
580
+ return true;
581
+ if (reconcileCompletionBatch(ctx, ownerId))
582
+ return true;
583
+ const batch = pendingCompletionBatch(ctx, ownerId);
367
584
  if (!batch)
368
585
  return false;
369
- if (batch.phase === "presented") {
370
- finishPresentedBatch(ownerId, batch);
371
- const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
372
- admitCompletionTransitions(ownerId, snapshot);
373
- Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
374
- batch = journal(ownerId).completion_batch;
375
- if (!batch)
376
- return false;
377
- }
378
586
  if (!isIdle(ctx))
379
587
  return true;
380
- const content = RunDelivery.formatRunCompletionBatchMessage(batch);
381
- if (batch.phase === "queued") {
382
- if (!RunDelivery.markRunCompletionBatchPresented({
383
- batchId: batch.batch_id,
384
- ownerId,
385
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
386
- }))
387
- return true;
388
- const accepted = journal(ownerId).completion_batch;
389
- if (accepted)
390
- finishPresentedBatch(ownerId, accepted);
391
- return flushCompletionBatch(ctx);
392
- }
393
- if (!isIdle(ctx))
588
+ const projected = RunDelivery.projectRunCompletionBatchPresentation(batch);
589
+ if (!isIdle(ctx) || !isCurrentOwner(ctx))
394
590
  return true;
395
591
  try {
396
- Pi.sendRunCompletionBatch(deps.pi, batch.batch_id, content);
592
+ Pi.sendRunCompletionBatch(deps.pi, batch.batch_id, projected.content, projected.presentation);
397
593
  }
398
594
  catch (error) {
595
+ if (fencedCompletionSession && !isCurrentOwner(ctx))
596
+ throw error;
399
597
  RunDelivery.recordRunCompletionBatchDeliveryFailure({
400
598
  batchId: batch.batch_id,
401
599
  error,
@@ -404,40 +602,38 @@ export function createRunUiRuntime(deps) {
404
602
  });
405
603
  throw error;
406
604
  }
407
- if (!RunDelivery.markRunCompletionBatchQueued({
408
- batchId: batch.batch_id,
409
- ownerId,
410
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
411
- })) {
412
- throw new Error("Completion batch changed before queue acknowledgment");
413
- }
414
- if (!RunDelivery.markRunCompletionBatchPresented({
415
- batchId: batch.batch_id,
416
- ownerId,
417
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
418
- })) {
605
+ if (fencedCompletionSession && !canUseCompletionSession(ctx, ownerId))
606
+ return true;
607
+ if (!acceptCompletionBatch(ownerId, batch.batch_id)) {
419
608
  throw new Error("Completion batch changed before durable acceptance");
420
609
  }
421
- const accepted = journal(ownerId).completion_batch;
422
- if (accepted)
423
- finishPresentedBatch(ownerId, accepted);
424
- ctx.ui.notify(`Actor completions ready: ${batch.members.length}`, "info");
425
610
  return true;
426
611
  };
427
612
  return {
428
613
  close,
614
+ isCurrentSessionContext: isCurrentOwner,
429
615
  flushCompletionBatch,
616
+ onTurnEnd(event, ctx) {
617
+ const ownerId = activeOwnerId;
618
+ if (!ownerId || !isCurrentOwner(ctx) || !isDeliveryRoot(ownerId) ||
619
+ event.outcome !== "completed" || isIdle(ctx) || ctx.signal?.aborted ||
620
+ ctx.hasPendingMessages?.() || event.context.pendingMessages.length > 0)
621
+ return undefined;
622
+ if (flushSteers(ctx, ownerId) || !canUseCompletionSession(ctx, ownerId) || reconcileCompletionBatch(ctx, ownerId))
623
+ return undefined;
624
+ const batch = pendingCompletionBatch(ctx, ownerId);
625
+ if (!batch || !isCurrentOwner(ctx))
626
+ return undefined;
627
+ const projected = RunDelivery.projectRunCompletionBatchPresentation(batch);
628
+ const entry = Pi.createRunCompletionBatchEntry(batch.batch_id, projected.content, projected.presentation);
629
+ if (!canUseCompletionSession(ctx, ownerId))
630
+ return undefined;
631
+ return { entries: [...event.entries, entry], continue: true };
632
+ },
430
633
  projectContext(messages, ctx) {
431
634
  const ownerId = activeOwnerId;
432
- if (!running || !ownerId)
635
+ if (!ownerId || !isCurrentOwner(ctx))
433
636
  return messages;
434
- try {
435
- if (Pi.getSessionId(ctx) !== ownerId)
436
- return messages;
437
- }
438
- catch {
439
- return messages;
440
- }
441
637
  const steerContext = Pi.dedupeRunSteerContext(messages);
442
638
  let contextMessages = steerContext.messages;
443
639
  for (const steer of journal(ownerId).steers) {
@@ -464,6 +660,8 @@ export function createRunUiRuntime(deps) {
464
660
  if (durable)
465
661
  finishPresentedSteer(ownerId, durable);
466
662
  }
663
+ if (!canUseCompletionSession(ctx, ownerId))
664
+ return contextMessages;
467
665
  const projected = Pi.dedupeRunCompletionBatchContext(contextMessages);
468
666
  const batch = journal(ownerId).completion_batch;
469
667
  if (!batch)
@@ -472,21 +670,8 @@ export function createRunUiRuntime(deps) {
472
670
  if (content !== RunDelivery.formatRunCompletionBatchMessage(batch)) {
473
671
  return Pi.removeRunCompletionBatchFromContext(projected.messages, batch.batch_id);
474
672
  }
475
- if (!RunDelivery.markRunCompletionBatchQueued({
476
- batchId: batch.batch_id,
477
- ownerId,
478
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
479
- }))
480
- return projected.messages;
481
- if (!RunDelivery.markRunCompletionBatchPresented({
482
- batchId: batch.batch_id,
483
- ownerId,
484
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
485
- }))
486
- return projected.messages;
487
- const presented = journal(ownerId).completion_batch;
488
- if (presented)
489
- finishPresentedBatch(ownerId, presented);
673
+ if (!fencedCompletionSession || canUseCompletionSession(ctx, ownerId))
674
+ acceptCompletionBatch(ownerId, batch.batch_id);
490
675
  return projected.messages;
491
676
  },
492
677
  shutdown(eventReason, ownerId, ctx) {
@@ -496,27 +681,65 @@ export function createRunUiRuntime(deps) {
496
681
  if (teardown.failed === 0 || !ctx)
497
682
  return;
498
683
  try {
499
- ctx.ui.notify(`Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`, "warning");
684
+ ctx.ui.notify([formatMarkdownField("stage", "shutdown teardown"), formatMarkdownField("killed", String(teardown.killed)),
685
+ formatMarkdownField("failed", String(teardown.failed)), formatMarkdownField("skipped", String(teardown.skipped)),
686
+ formatMarkdownField("discovery_failed", String(teardown.discoveryFailed)),
687
+ formatMarkdownField("summary", teardown.summaryPath == null ? "unavailable" : formatLiteralBlock(teardown.summaryPath)),
688
+ formatMarkdownField("basis", "teardown report; not scope closure")].join("\n"), "warning");
500
689
  }
501
690
  catch {
502
691
  /* stale shutdown context */
503
692
  }
504
693
  },
505
- start(ctx, ownerId) {
694
+ start(ctx, ownerId, event) {
506
695
  close();
696
+ const ticket = {};
697
+ sessionTicket = ticket;
507
698
  activeContext = ctx;
508
699
  activeOwnerId = ownerId;
509
700
  recoverQueuedSteers = true;
510
701
  running = true;
511
702
  try {
512
- recoverPresentedBatch(ownerId);
703
+ if (fencedCompletionSession) {
704
+ sessionManager = ctx.sessionManager;
705
+ const fresh = !seenManagers.has(sessionManager) && !contextTickets.has(ctx);
706
+ seenManagers.add(sessionManager);
707
+ if (!contextTickets.has(ctx))
708
+ contextTickets.set(ctx, ticket);
709
+ if (deps.captureCompletionBatch && !fresh)
710
+ sessionManager = undefined;
711
+ if (deps.completionStreamSessionAdmission && fresh && event && (event.reason === "new" || event.reason === "fork") && isCurrentOwner(ctx)) {
712
+ const admitted = deps.completionStreamSessionAdmission(ctx, event) === true && isCurrentOwner(ctx);
713
+ if (ticket !== sessionTicket)
714
+ return;
715
+ streamSessionAdmitted = admitted;
716
+ }
717
+ }
718
+ if (ticket !== sessionTicket)
719
+ return;
720
+ watcher = createWatcher(ticket);
721
+ reconciliation = createReconciliation(ticket);
722
+ if (canUseCompletionSession(ctx, ownerId))
723
+ recoverPresentedBatch(ownerId);
513
724
  recoverPresentedSteers(ownerId);
514
725
  Observability.primeRunAttentionState(observation, ownerId);
515
726
  update(ctx, ownerId, true, true);
727
+ if (ticket !== sessionTicket)
728
+ return;
516
729
  watcher.refresh();
730
+ if (ticket !== sessionTicket)
731
+ return;
517
732
  reconciliation.start();
733
+ if (ticket !== sessionTicket || ctx.mode !== "tui")
734
+ return;
518
735
  animationInterval = setInterval(() => {
519
- runActiveCallback("status animation callback", (current, currentOwnerId) => update(current, currentOwnerId));
736
+ runActiveCallback("status animation callback", (current) => {
737
+ const status = Observability.renderSubagentStatus(displayRunningSubagents, ++displayFrame);
738
+ const themed = status ? current.ui.theme.fg("dim", status) : status;
739
+ if (ticket !== sessionTicket)
740
+ return;
741
+ current.ui.setStatus("zz-pi-actors-runs", themed);
742
+ }, ticket);
520
743
  }, deps.animationIntervalMs ?? 1000);
521
744
  animationInterval.unref?.();
522
745
  }