poly-weaver 0.15.2 → 1.0.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 (194) hide show
  1. package/dist/agents/implementors/handler.d.ts.map +1 -1
  2. package/dist/agents/implementors/handler.js +7 -2
  3. package/dist/agents/implementors/handler.js.map +1 -1
  4. package/dist/agents/interviewers/handler.d.ts.map +1 -1
  5. package/dist/agents/interviewers/handler.js +3 -1
  6. package/dist/agents/interviewers/handler.js.map +1 -1
  7. package/dist/agents/planners/adapter.d.ts.map +1 -1
  8. package/dist/agents/planners/adapter.js +30 -11
  9. package/dist/agents/planners/adapter.js.map +1 -1
  10. package/dist/agents/planners/handler.d.ts.map +1 -1
  11. package/dist/agents/planners/handler.js +7 -2
  12. package/dist/agents/planners/handler.js.map +1 -1
  13. package/dist/agents/reviewers/adapter.d.ts.map +1 -1
  14. package/dist/agents/reviewers/adapter.js +1 -0
  15. package/dist/agents/reviewers/adapter.js.map +1 -1
  16. package/dist/agents/reviewers/handler.d.ts.map +1 -1
  17. package/dist/agents/reviewers/handler.js +23 -4
  18. package/dist/agents/reviewers/handler.js.map +1 -1
  19. package/dist/agents/reviewers/types.d.ts +2 -0
  20. package/dist/agents/reviewers/types.d.ts.map +1 -1
  21. package/dist/agents/runner.d.ts +18 -1
  22. package/dist/agents/runner.d.ts.map +1 -1
  23. package/dist/agents/runner.js +136 -20
  24. package/dist/agents/runner.js.map +1 -1
  25. package/dist/agents/simplifiers/handler.d.ts.map +1 -1
  26. package/dist/agents/simplifiers/handler.js +7 -2
  27. package/dist/agents/simplifiers/handler.js.map +1 -1
  28. package/dist/auto-update/detect-agents.d.ts +1 -0
  29. package/dist/auto-update/detect-agents.d.ts.map +1 -1
  30. package/dist/auto-update/detect-agents.js +8 -7
  31. package/dist/auto-update/detect-agents.js.map +1 -1
  32. package/dist/cli.js +16 -4
  33. package/dist/cli.js.map +1 -1
  34. package/dist/crosscheck/coordinator.d.ts +5 -0
  35. package/dist/crosscheck/coordinator.d.ts.map +1 -1
  36. package/dist/crosscheck/coordinator.js +216 -96
  37. package/dist/crosscheck/coordinator.js.map +1 -1
  38. package/dist/crosscheck/live-agent-session.d.ts +10 -1
  39. package/dist/crosscheck/live-agent-session.d.ts.map +1 -1
  40. package/dist/crosscheck/live-agent-session.js +68 -7
  41. package/dist/crosscheck/live-agent-session.js.map +1 -1
  42. package/dist/crosscheck/state.d.ts +3 -0
  43. package/dist/crosscheck/state.d.ts.map +1 -1
  44. package/dist/crosscheck/state.js +17 -2
  45. package/dist/crosscheck/state.js.map +1 -1
  46. package/dist/dump/collector.d.ts.map +1 -1
  47. package/dist/dump/collector.js +3 -0
  48. package/dist/dump/collector.js.map +1 -1
  49. package/dist/dump/service.d.ts.map +1 -1
  50. package/dist/dump/service.js +3 -0
  51. package/dist/dump/service.js.map +1 -1
  52. package/dist/dump/types.d.ts +3 -0
  53. package/dist/dump/types.d.ts.map +1 -1
  54. package/dist/dump/types.js.map +1 -1
  55. package/dist/flow/executor.d.ts.map +1 -1
  56. package/dist/flow/executor.js +5 -0
  57. package/dist/flow/executor.js.map +1 -1
  58. package/dist/flow/types.d.ts +5 -0
  59. package/dist/flow/types.d.ts.map +1 -1
  60. package/dist/flow/types.js.map +1 -1
  61. package/dist/offscreen/controller.d.ts +19 -5
  62. package/dist/offscreen/controller.d.ts.map +1 -1
  63. package/dist/offscreen/controller.js +336 -159
  64. package/dist/offscreen/controller.js.map +1 -1
  65. package/dist/offscreen/fork-launch.d.ts +2 -2
  66. package/dist/offscreen/fork-launch.d.ts.map +1 -1
  67. package/dist/offscreen/fork-launch.js +2 -2
  68. package/dist/offscreen/fork-launch.js.map +1 -1
  69. package/dist/offscreen/fork.d.ts +16 -2
  70. package/dist/offscreen/fork.d.ts.map +1 -1
  71. package/dist/offscreen/fork.js +79 -40
  72. package/dist/offscreen/fork.js.map +1 -1
  73. package/dist/offscreen/inquiry-pane.d.ts +2 -1
  74. package/dist/offscreen/inquiry-pane.d.ts.map +1 -1
  75. package/dist/offscreen/inquiry-pane.js +8 -2
  76. package/dist/offscreen/inquiry-pane.js.map +1 -1
  77. package/dist/offscreen/inquiry-sender.d.ts +1 -0
  78. package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
  79. package/dist/offscreen/inquiry-sender.js +1 -0
  80. package/dist/offscreen/inquiry-sender.js.map +1 -1
  81. package/dist/offscreen/panes.d.ts +1 -0
  82. package/dist/offscreen/panes.d.ts.map +1 -1
  83. package/dist/offscreen/registry.d.ts +2 -2
  84. package/dist/offscreen/registry.d.ts.map +1 -1
  85. package/dist/offscreen/registry.js +5 -1
  86. package/dist/offscreen/registry.js.map +1 -1
  87. package/dist/offscreen/resolve.d.ts +2 -0
  88. package/dist/offscreen/resolve.d.ts.map +1 -1
  89. package/dist/offscreen/resolve.js +5 -1
  90. package/dist/offscreen/resolve.js.map +1 -1
  91. package/dist/offscreen/session-turn-monitor.d.ts +34 -33
  92. package/dist/offscreen/session-turn-monitor.d.ts.map +1 -1
  93. package/dist/offscreen/session-turn-monitor.js +327 -281
  94. package/dist/offscreen/session-turn-monitor.js.map +1 -1
  95. package/dist/offscreen/session-turn-receipt.d.ts +28 -0
  96. package/dist/offscreen/session-turn-receipt.d.ts.map +1 -0
  97. package/dist/offscreen/session-turn-receipt.js +53 -0
  98. package/dist/offscreen/session-turn-receipt.js.map +1 -0
  99. package/dist/offscreen/split-inquiry.d.ts.map +1 -1
  100. package/dist/offscreen/split-inquiry.js +5 -0
  101. package/dist/offscreen/split-inquiry.js.map +1 -1
  102. package/dist/offscreen/starting-pane.d.ts +13 -0
  103. package/dist/offscreen/starting-pane.d.ts.map +1 -0
  104. package/dist/offscreen/starting-pane.js +24 -0
  105. package/dist/offscreen/starting-pane.js.map +1 -0
  106. package/dist/offscreen/surface.d.ts +1 -0
  107. package/dist/offscreen/surface.d.ts.map +1 -1
  108. package/dist/offscreen/surface.js +5 -3
  109. package/dist/offscreen/surface.js.map +1 -1
  110. package/dist/orchestrator.d.ts.map +1 -1
  111. package/dist/orchestrator.js +7 -1
  112. package/dist/orchestrator.js.map +1 -1
  113. package/dist/providers/base-completion.d.ts +1 -0
  114. package/dist/providers/base-completion.d.ts.map +1 -1
  115. package/dist/providers/base-completion.js +4 -2
  116. package/dist/providers/base-completion.js.map +1 -1
  117. package/dist/providers/codex/completion.d.ts +4 -2
  118. package/dist/providers/codex/completion.d.ts.map +1 -1
  119. package/dist/providers/codex/completion.js +99 -14
  120. package/dist/providers/codex/completion.js.map +1 -1
  121. package/dist/providers/codex/strategy.d.ts.map +1 -1
  122. package/dist/providers/codex/strategy.js +3 -2
  123. package/dist/providers/codex/strategy.js.map +1 -1
  124. package/dist/providers/copilot/completion.d.ts +32 -21
  125. package/dist/providers/copilot/completion.d.ts.map +1 -1
  126. package/dist/providers/copilot/completion.js +110 -166
  127. package/dist/providers/copilot/completion.js.map +1 -1
  128. package/dist/providers/copilot/environment.d.ts +2 -0
  129. package/dist/providers/copilot/environment.d.ts.map +1 -0
  130. package/dist/providers/copilot/environment.js +8 -0
  131. package/dist/providers/copilot/environment.js.map +1 -0
  132. package/dist/providers/copilot/fork-state.d.ts +7 -0
  133. package/dist/providers/copilot/fork-state.d.ts.map +1 -0
  134. package/dist/providers/copilot/fork-state.js +150 -0
  135. package/dist/providers/copilot/fork-state.js.map +1 -0
  136. package/dist/providers/copilot/observation.d.ts +275 -0
  137. package/dist/providers/copilot/observation.d.ts.map +1 -0
  138. package/dist/providers/copilot/observation.js +2072 -0
  139. package/dist/providers/copilot/observation.js.map +1 -0
  140. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.d.mts +2 -0
  141. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.d.mts.map +1 -0
  142. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs +426 -0
  143. package/dist/providers/copilot/observer-extension/com.github.copilot/extensions/poly-weaver-observer/extension.mjs.map +1 -0
  144. package/dist/providers/copilot/observer-extension/plugin.json +6 -0
  145. package/dist/providers/copilot/record-properties.d.ts +4 -0
  146. package/dist/providers/copilot/record-properties.d.ts.map +1 -0
  147. package/dist/providers/copilot/record-properties.js +14 -0
  148. package/dist/providers/copilot/record-properties.js.map +1 -0
  149. package/dist/providers/copilot/session.d.ts +8 -33
  150. package/dist/providers/copilot/session.d.ts.map +1 -1
  151. package/dist/providers/copilot/session.js +10 -176
  152. package/dist/providers/copilot/session.js.map +1 -1
  153. package/dist/providers/copilot/strategy.d.ts +4 -5
  154. package/dist/providers/copilot/strategy.d.ts.map +1 -1
  155. package/dist/providers/copilot/strategy.js +73 -67
  156. package/dist/providers/copilot/strategy.js.map +1 -1
  157. package/dist/providers/fork-cleanup.d.ts +4 -0
  158. package/dist/providers/fork-cleanup.d.ts.map +1 -0
  159. package/dist/providers/fork-cleanup.js +21 -0
  160. package/dist/providers/fork-cleanup.js.map +1 -0
  161. package/dist/providers/fork.d.ts +24 -3
  162. package/dist/providers/fork.d.ts.map +1 -1
  163. package/dist/providers/fork.js.map +1 -1
  164. package/dist/providers/invoke.d.ts.map +1 -1
  165. package/dist/providers/invoke.js +85 -34
  166. package/dist/providers/invoke.js.map +1 -1
  167. package/dist/providers/live.d.ts.map +1 -1
  168. package/dist/providers/live.js +62 -17
  169. package/dist/providers/live.js.map +1 -1
  170. package/dist/providers/observer-run.d.ts +4 -0
  171. package/dist/providers/observer-run.d.ts.map +1 -0
  172. package/dist/providers/observer-run.js +23 -0
  173. package/dist/providers/observer-run.js.map +1 -0
  174. package/dist/providers/session-launch.d.ts +5 -1
  175. package/dist/providers/session-launch.d.ts.map +1 -1
  176. package/dist/providers/session-launch.js +42 -16
  177. package/dist/providers/session-launch.js.map +1 -1
  178. package/dist/providers/types.d.ts +156 -4
  179. package/dist/providers/types.d.ts.map +1 -1
  180. package/dist/providers/types.js.map +1 -1
  181. package/dist/session/resume.d.ts.map +1 -1
  182. package/dist/session/resume.js +16 -2
  183. package/dist/session/resume.js.map +1 -1
  184. package/dist/terminal/paste-submit.d.ts +1 -0
  185. package/dist/terminal/paste-submit.d.ts.map +1 -1
  186. package/dist/terminal/paste-submit.js +45 -32
  187. package/dist/terminal/paste-submit.js.map +1 -1
  188. package/dist/user/host-curate-prompt.d.ts.map +1 -1
  189. package/dist/user/host-curate-prompt.js +9 -6
  190. package/dist/user/host-curate-prompt.js.map +1 -1
  191. package/dist/user/host-prompt.d.ts.map +1 -1
  192. package/dist/user/host-prompt.js +9 -6
  193. package/dist/user/host-prompt.js.map +1 -1
  194. package/package.json +4 -2
@@ -4,7 +4,7 @@ import { bold, dim, MUTED_YELLOW_BG, padOrClip, reset, TRUE_BLACK_FG, } from "..
4
4
  import { APP_VERSION } from "../version.js";
5
5
  import { buildExecutorInitialPrompt, buildExecutorCrosscheckPrompt, buildExecutorEffectiveTaskContext, buildExecutorFollowUpPrompt, buildExecutorReconcilePrompt, buildExecutorSteerFreshPrompt, buildExecutorSteerPrompt, } from "../agents/executors/prompts.js";
6
6
  import { sendInquiryQuestion } from "../offscreen/inquiry-sender.js";
7
- import { SessionTurnMonitor } from "../offscreen/session-turn-monitor.js";
7
+ import { SessionTurnMonitor, SessionTurnReceipt } from "../offscreen/session-turn-monitor.js";
8
8
  import { resolveProvider } from "../providers/registry.js";
9
9
  import { compactProviderConfigurationTokens, } from "../providers/configuration-presentation.js";
10
10
  import { readSessionFileWithStatus } from "../session/reader.js";
@@ -87,6 +87,7 @@ export class CrosscheckCoordinator {
87
87
  resultRefresh;
88
88
  launchAbort;
89
89
  sessionsReady = false;
90
+ launchReconciliationPending = false;
90
91
  transientFlash;
91
92
  crosscheckProgress;
92
93
  selectionAgent;
@@ -110,7 +111,10 @@ export class CrosscheckCoordinator {
110
111
  const layout = this.options.host.getLayout();
111
112
  const dimensions = this.paneDimensions(layout.columns, layout.childRows);
112
113
  await this.reconcilePersistedHistory();
114
+ if (this.finishing)
115
+ return { kind: "crosscheck", interrupted: true };
113
116
  const persisted = this.store.snapshot();
117
+ this.launchReconciliationPending = true;
114
118
  const launchAbort = new AbortController();
115
119
  this.launchAbort = launchAbort;
116
120
  const completion = new Promise((resolve, reject) => {
@@ -119,11 +123,12 @@ export class CrosscheckCoordinator {
119
123
  if (this.pendingFailure !== undefined)
120
124
  reject(this.pendingFailure);
121
125
  });
126
+ completion.catch(() => { });
122
127
  let hasLaunchFailure = false;
123
128
  let firstLaunchFailure;
124
129
  const launches = ["agent-1", "agent-2"].map(async (agent) => {
130
+ const config = this.options.configurations[agent];
125
131
  try {
126
- const config = this.options.configurations[agent];
127
132
  const resultFile = persisted.generation.resultFiles[agent];
128
133
  const resultState = persisted.generation.rewind?.[agent];
129
134
  const resultReady = resultState ? false : await fileHasContent(resultFile);
@@ -160,7 +165,9 @@ export class CrosscheckCoordinator {
160
165
  : activityState === "working"
161
166
  ? RESUME_CONTINUE_PROMPT
162
167
  : resumePrompt;
168
+ const launchTurnReceipt = new SessionTurnReceipt();
163
169
  const session = await (this.options.launchSession ?? LiveAgentSession.launch)({
170
+ launchTurnReceipt,
164
171
  driver: config.driver,
165
172
  workdir: this.options.workdir,
166
173
  prompt,
@@ -176,6 +183,7 @@ export class CrosscheckCoordinator {
176
183
  cols: dimensions[agent].cols,
177
184
  rows: dimensions[agent].rows,
178
185
  sessionFileWaitMs: this.options.sessionFileWaitMs,
186
+ observerRun: this.options.providerObserverRuns?.get(config.driver),
179
187
  onRender: () => this.options.host.scheduleParentUiRender(),
180
188
  onOscClipboard: (data) => this.options.host.forwardOscClipboard(data),
181
189
  onSpawn: (session) => {
@@ -187,28 +195,11 @@ export class CrosscheckCoordinator {
187
195
  this.fail(new Error(`${config.driver} ${agentLabel(agent)} exited unexpectedly (code ${event.exitCode})`));
188
196
  }
189
197
  },
190
- });
191
- this.launchingSessions.delete(agent);
192
- const monitor = new SessionTurnMonitor(config.driver, session.identity.sessionFilePath, session.baseline, {
193
- initialHistorySnapshot: persisted.sessions[agent]?.historySnapshot,
194
- onHistorySnapshot: (snapshot) => {
195
- void this.store.updateSessionHistory(agent, snapshot);
196
- },
197
- onHistoryRegression: (_regression, snapshot) => {
198
- this.runDispatchOperation(agent, this.invalidateGenerationForRewind(agent, snapshot), "invalidate rewound generation");
199
- },
200
- onChange: (idle) => {
201
- this.refreshStatusHint();
202
- this.options.host.scheduleParentUiRender();
203
- if (idle) {
204
- const progressIdentity = this.takeCrosscheckTurnIdentity(agent);
205
- this.runDispatchOperation(agent, this.handleAgentIdle(agent, progressIdentity), "prepare resumed crosscheck");
206
- }
207
- },
208
- onReadFailure: () => {
209
- this.options.output.warn(`${config.driver} ${agentLabel(agent)} session log is unreadable; crosscheck remains disarmed`);
198
+ onObserverError: (error) => {
199
+ this.fail(observerFailureError(config.driver, agent, error));
210
200
  },
211
201
  });
202
+ this.launchingSessions.set(agent, session);
212
203
  const persistedDispatch = persisted.generation.dispatch;
213
204
  const freshForPendingDispatch = persistedDispatch !== undefined
214
205
  && (persistedDispatch.phase === "crosscheck"
@@ -216,23 +207,14 @@ export class CrosscheckCoordinator {
216
207
  : true)
217
208
  && (prior?.pendingDispatchBaselineReset === true
218
209
  || session.resumed !== true);
219
- this.agents.set(agent, {
220
- config,
221
- session,
222
- monitor,
223
- resultReady,
224
- resultState,
225
- freshForPendingDispatch,
226
- launchTurnExpected: session.resumed !== true || prompt.length > 0,
227
- });
228
- monitor.startHistoryPolling();
210
+ // Initialize before observation can publish a new checkpoint or rewind.
229
211
  await this.store.update((state) => {
230
212
  const dispatch = state.generation.dispatch;
231
213
  if (freshForPendingDispatch
232
214
  && state.generation.id === persisted.generation.id
233
215
  && dispatch !== undefined
234
- && persisted.generation.dispatch !== undefined
235
- && dispatch.token === persisted.generation.dispatch.token) {
216
+ && persistedDispatch !== undefined
217
+ && dispatch.token === persistedDispatch.token) {
236
218
  if (dispatch.phase === "crosscheck") {
237
219
  state.generation.consumed = false;
238
220
  }
@@ -244,27 +226,73 @@ export class CrosscheckCoordinator {
244
226
  ...(freshForPendingDispatch && {
245
227
  pendingDispatchBaselineReset: true,
246
228
  }),
247
- ...(persisted.sessions[agent]?.historySnapshot && {
248
- historySnapshot: persisted.sessions[agent].historySnapshot,
229
+ ...(prior?.historySnapshot && {
230
+ historySnapshot: prior.historySnapshot,
249
231
  }),
250
232
  };
251
233
  });
252
- if (freshForPendingDispatch
253
- && session.resumed === true
254
- && activityState === "idle"
255
- && !this.agents.get(agent)?.launchTurnExpected) {
256
- await this.handleAgentIdle(agent);
234
+ let usingObserverMonitor = false;
235
+ const monitorOptions = {
236
+ pendingEvidencePolicy: "wait",
237
+ initialHistorySnapshot: persisted.sessions[agent]?.historySnapshot,
238
+ onHistorySnapshot: (snapshot) => {
239
+ if (this.finishing)
240
+ return;
241
+ this.runDispatchOperation(agent, this.store.updateSessionHistory(agent, snapshot), "persist session history");
242
+ },
243
+ onHistoryRegression: (_regression, snapshot) => {
244
+ if (this.finishing)
245
+ return;
246
+ this.runDispatchOperation(agent, this.invalidateGenerationForRewind(agent, snapshot), "invalidate rewound generation");
247
+ },
248
+ onChange: (idle) => {
249
+ if (this.finishing)
250
+ return;
251
+ this.refreshStatusHint();
252
+ this.options.host.scheduleParentUiRender();
253
+ if (idle && this.sessionsReady) {
254
+ const progressIdentity = this.takeCrosscheckTurnIdentity(agent);
255
+ this.runDispatchOperation(agent, this.handleAgentIdle(agent, progressIdentity), "prepare resumed crosscheck");
256
+ }
257
+ },
258
+ onReadFailure: () => {
259
+ this.options.output.warn(usingObserverMonitor
260
+ ? `${config.driver} ${agentLabel(agent)} session observation is unavailable; crosscheck remains disarmed`
261
+ : `${config.driver} ${agentLabel(agent)} session log is unreadable; crosscheck remains disarmed`);
262
+ },
263
+ };
264
+ const observerMonitor = session.createTurnMonitor?.(monitorOptions);
265
+ usingObserverMonitor = observerMonitor !== undefined;
266
+ const monitor = observerMonitor ?? new SessionTurnMonitor(config.driver, session.identity.sessionFilePath, session.baseline, monitorOptions);
267
+ this.agents.set(agent, {
268
+ config,
269
+ session,
270
+ monitor,
271
+ resultReady,
272
+ resultState,
273
+ freshForPendingDispatch,
274
+ launchTurnExpected: session.resumed !== true || prompt.length > 0,
275
+ launchTurnReceipt,
276
+ });
277
+ this.launchingSessions.delete(agent);
278
+ if (this.agents.get(agent).launchTurnExpected) {
279
+ if (typeof monitor.attachTurn === "function")
280
+ monitor.attachTurn(launchTurnReceipt);
281
+ else
282
+ monitor.beginTurn();
257
283
  }
284
+ monitor.startHistoryPolling();
258
285
  }
259
286
  catch (err) {
260
287
  this.launchingSessions.get(agent)?.close();
261
288
  this.launchingSessions.delete(agent);
289
+ const launchFailure = observerLaunchFailureError(config.driver, agent, err);
262
290
  if (!hasLaunchFailure) {
263
291
  hasLaunchFailure = true;
264
- firstLaunchFailure = err;
265
- launchAbort.abort(err);
292
+ firstLaunchFailure = launchFailure;
293
+ launchAbort.abort(launchFailure);
266
294
  }
267
- throw err;
295
+ throw launchFailure;
268
296
  }
269
297
  });
270
298
  const launchResults = await Promise.allSettled(launches);
@@ -274,15 +302,14 @@ export class CrosscheckCoordinator {
274
302
  throw firstLaunchFailure;
275
303
  if (this.finishing)
276
304
  return await completion;
277
- this.sessionsReady = true;
278
- this.refreshStatusHint();
279
- this.options.host.scheduleParentUiRender();
280
305
  for (const runtime of this.agents.values()) {
281
306
  if (runtime.launchTurnExpected)
282
- runtime.monitor.beginTurn();
307
+ runtime.monitor.beginTurn(runtime.launchTurnReceipt);
283
308
  }
284
- await this.reconcilePersistedDispatch();
285
- await this.maybeSettleReconcileGate();
309
+ this.sessionsReady = true;
310
+ this.refreshStatusHint();
311
+ this.options.host.scheduleParentUiRender();
312
+ await this.reconcileLaunchDispatch();
286
313
  this.broadcast.reconcileMode(this.currentReadiness(), this.currentGenerationPresentation());
287
314
  this.refreshMouseModes();
288
315
  this.resultTimer = setInterval(() => {
@@ -542,6 +569,8 @@ export class CrosscheckCoordinator {
542
569
  continue;
543
570
  }
544
571
  if (isControlKey(event, 0x18)) {
572
+ if (this.finishing)
573
+ continue;
545
574
  flush();
546
575
  const canRetry = this.canRetryPendingDispatch();
547
576
  const canCrosscheck = this.isCrosscheckArmed();
@@ -567,6 +596,8 @@ export class CrosscheckCoordinator {
567
596
  if (this.routeMouse(event, mouseFrame))
568
597
  continue;
569
598
  }
599
+ if (this.finishing)
600
+ continue;
570
601
  if (this.focus === "broadcast") {
571
602
  flush();
572
603
  const submissionState = this.currentSubmissionState();
@@ -604,6 +635,8 @@ export class CrosscheckCoordinator {
604
635
  return true;
605
636
  }
606
637
  onClipboardPasteRequest() {
638
+ if (this.finishing)
639
+ return true;
607
640
  if (this.focus !== "broadcast")
608
641
  return false;
609
642
  this.requestBroadcastPaste();
@@ -697,6 +730,8 @@ export class CrosscheckCoordinator {
697
730
  const runtime = this.agents.get(target.agent);
698
731
  if (!runtime || point.row <= HEADER_ROWS)
699
732
  return true;
733
+ if (this.finishing && event.kind !== "wheel")
734
+ return true;
700
735
  const localRow = point.row - HEADER_ROWS;
701
736
  if (event.kind === "wheel"
702
737
  && !runtime.session.vt.isAlternate
@@ -738,6 +773,8 @@ export class CrosscheckCoordinator {
738
773
  return true;
739
774
  }
740
775
  forwardMouseFrameToAgent(agent, frame) {
776
+ if (this.finishing)
777
+ return;
741
778
  const runtime = this.agents.get(agent);
742
779
  if (!runtime)
743
780
  return;
@@ -893,9 +930,13 @@ export class CrosscheckCoordinator {
893
930
  return "ready";
894
931
  }
895
932
  requestBroadcastPaste() {
933
+ if (this.finishing)
934
+ return;
896
935
  void handleImagePasteIntent(this.broadcast.attachments, () => {
897
936
  this.flashTransient(PASTING_IMAGE_HINT, 3000);
898
937
  }).then((outcome) => {
938
+ if (this.finishing)
939
+ return;
899
940
  if (outcome.inserted !== undefined) {
900
941
  this.broadcast.applyInsertedText(outcome.inserted);
901
942
  this.resizeSessions();
@@ -913,7 +954,7 @@ export class CrosscheckCoordinator {
913
954
  return this.isCrosscheckArmed() || this.isReconcileArmed();
914
955
  }
915
956
  isCrosscheckArmed() {
916
- if (!this.store || !this.sessionsReady)
957
+ if (this.finishing || !this.store || !this.sessionsReady)
917
958
  return false;
918
959
  if (this.store.generationInvalidated())
919
960
  return false;
@@ -923,7 +964,7 @@ export class CrosscheckCoordinator {
923
964
  return this.allResultsReady() && this.allAgentsIdle();
924
965
  }
925
966
  isReconcileArmed() {
926
- if (!this.store || !this.sessionsReady)
967
+ if (this.finishing || !this.store || !this.sessionsReady)
927
968
  return false;
928
969
  if (this.store.generationInvalidated())
929
970
  return false;
@@ -952,11 +993,12 @@ export class CrosscheckCoordinator {
952
993
  return status !== undefined && status !== "delivered";
953
994
  }
954
995
  canRetryPendingDispatch() {
955
- if (!this.store)
996
+ if (this.finishing || !this.store)
956
997
  return false;
957
998
  const dispatchPhase = this.store.dispatchPhase();
958
999
  if (this.dispatching
959
1000
  || !this.sessionsReady
1001
+ || this.launchReconciliationPending
960
1002
  || dispatchPhase === undefined
961
1003
  || (this.store.generationConsumed() && dispatchPhase === "crosscheck")
962
1004
  || !this.allResultsReady()
@@ -1120,28 +1162,50 @@ export class CrosscheckCoordinator {
1120
1162
  let submitted = false;
1121
1163
  const provider = resolveProvider(runtime.config.driver);
1122
1164
  const releaseInputLock = this.lockAutomatedInput(agent);
1165
+ let receipt;
1166
+ const markSubmitted = () => {
1167
+ receipt?.markWritten();
1168
+ if (!receipt || submitted || !this.isCurrentGenerationOperation(generationId, operation))
1169
+ return;
1170
+ submitted = true;
1171
+ if (progressAttemptId) {
1172
+ this.markCrosscheckSlotSubmitted(agent, generationId, dispatch.token, progressAttemptId);
1173
+ const identity = { generationId, dispatchToken: dispatch.token, attemptId: progressAttemptId };
1174
+ receipt.onSettled((result) => {
1175
+ if ((result !== "completed" && result !== "cancelled")
1176
+ || !this.isCurrentGenerationOperation(generationId, operation))
1177
+ return;
1178
+ this.settleCrosscheckSlot(agent, identity);
1179
+ this.runDispatchOperation(agent, this.handleAgentIdle(agent, identity), "settle crosscheck request");
1180
+ });
1181
+ }
1182
+ };
1123
1183
  try {
1184
+ receipt = await runtime.monitor.reserveTurnFromSession();
1124
1185
  await (this.options.sendInquiry ?? sendInquiryQuestion)(runtime.session, this.buildDispatchPrompt(agent, generationId, dispatch.phase, dispatch.round), {
1125
1186
  signal: operation.signal,
1126
1187
  composer: provider.composer,
1188
+ onSubmitWritten: markSubmitted,
1127
1189
  onSubmitted: () => {
1128
1190
  if (!this.isCurrentGenerationOperation(generationId, operation))
1129
1191
  return;
1130
- submitted = true;
1131
- if (progressAttemptId) {
1132
- this.markCrosscheckSlotSubmitted(agent, generationId, dispatch.token, progressAttemptId);
1133
- }
1134
- runtime.monitor.beginTurn();
1192
+ markSubmitted();
1193
+ runtime.monitor.beginTurn(receipt);
1135
1194
  },
1136
1195
  });
1137
1196
  }
1138
1197
  finally {
1139
1198
  releaseInputLock();
1199
+ receipt?.release();
1200
+ if (submitted && this.isCurrentGenerationOperation(generationId, operation)) {
1201
+ await this.transitionDispatchStatus(agent, generationId, dispatch.token, "pending", "submitted");
1202
+ if (runtime.monitor.isIdle)
1203
+ await this.handleAgentIdle(agent);
1204
+ }
1140
1205
  }
1141
1206
  if (!submitted || !this.isCurrentGenerationOperation(generationId, operation))
1142
1207
  return;
1143
- await this.transitionDispatchStatus(agent, generationId, dispatch.token, "pending", "submitted");
1144
- const classification = await provider.live.classifyDispatchDelivery(runtime.session.identity.sessionFilePath, dispatch.agents[agent].baseline);
1208
+ const classification = await this.classifyRuntimeDispatchDelivery(runtime, dispatch.agents[agent].baseline);
1145
1209
  if (!this.isCurrentGenerationOperation(generationId, operation))
1146
1210
  return;
1147
1211
  if (classification === "present") {
@@ -1150,6 +1214,8 @@ export class CrosscheckCoordinator {
1150
1214
  if (!this.isCurrentGenerationOperation(generationId, operation))
1151
1215
  return;
1152
1216
  await this.finalizeDispatchIfComplete(generationId);
1217
+ // The matching turns may have settled before delivery confirmation finished.
1218
+ await this.maybeSettleReconcileGate();
1153
1219
  }
1154
1220
  buildDispatchPrompt(agent, generationId, phase, round) {
1155
1221
  const other = otherAgent(agent);
@@ -1165,7 +1231,23 @@ export class CrosscheckCoordinator {
1165
1231
  const ownJsonFile = reconcileJsonFilePath(this.options.persistentDir, agent, generationId, round);
1166
1232
  return buildExecutorReconcilePrompt(otherJsonFile, ownJsonFile);
1167
1233
  }
1234
+ async reconcileLaunchDispatch() {
1235
+ if (!this.launchReconciliationPending
1236
+ || !this.sessionsReady
1237
+ || !this.allAgentsIdle())
1238
+ return;
1239
+ // Release once: later dispatches still reconcile each side independently.
1240
+ this.launchReconciliationPending = false;
1241
+ for (const agent of this.agents.keys())
1242
+ await this.handleAgentIdle(agent);
1243
+ await this.reconcilePersistedDispatch();
1244
+ await this.maybeSettleReconcileGate();
1245
+ }
1168
1246
  async reconcilePersistedDispatch() {
1247
+ if (this.launchReconciliationPending) {
1248
+ await this.reconcileLaunchDispatch();
1249
+ return;
1250
+ }
1169
1251
  const snapshot = this.store.snapshot();
1170
1252
  const generationId = snapshot.generation.id;
1171
1253
  const dispatch = snapshot.generation.dispatch;
@@ -1185,6 +1267,7 @@ export class CrosscheckCoordinator {
1185
1267
  async retryPersistedDispatch() {
1186
1268
  if (this.dispatching
1187
1269
  || !this.sessionsReady
1270
+ || this.launchReconciliationPending
1188
1271
  || !this.allResultsReady()
1189
1272
  || !this.allAgentsIdle())
1190
1273
  return;
@@ -1235,18 +1318,9 @@ export class CrosscheckCoordinator {
1235
1318
  async reconcileDispatchSide(agent, generationId, dispatch, expectedStatus, onAbsent, operation = this.currentDispatchOperationContext()) {
1236
1319
  if (!this.isCurrentGenerationOperation(generationId, operation))
1237
1320
  return false;
1238
- const target = onAbsent
1239
- ? (() => {
1240
- const runtime = this.agents.get(agent);
1241
- return runtime
1242
- ? {
1243
- driver: runtime.config.driver,
1244
- sessionFilePath: runtime.session.identity.sessionFilePath,
1245
- }
1246
- : undefined;
1247
- })()
1248
- : this.store.snapshot().sessions[agent];
1249
- if (!target) {
1321
+ const runtimeTarget = onAbsent ? this.agents.get(agent) : undefined;
1322
+ const persistedTarget = onAbsent ? undefined : this.store.snapshot().sessions[agent];
1323
+ if (!runtimeTarget && !persistedTarget) {
1250
1324
  if (!onAbsent
1251
1325
  &&
1252
1326
  this.isCurrentDispatchSide(agent, generationId, dispatch.token, expectedStatus)) {
@@ -1254,11 +1328,13 @@ export class CrosscheckCoordinator {
1254
1328
  }
1255
1329
  return true;
1256
1330
  }
1257
- if ("pendingDispatchBaselineReset" in target && target.pendingDispatchBaselineReset) {
1331
+ if (persistedTarget?.pendingDispatchBaselineReset) {
1258
1332
  return true;
1259
1333
  }
1260
- const classification = await resolveProvider(target.driver)
1261
- .live.classifyDispatchDelivery(target.sessionFilePath, dispatch.agents[agent].baseline);
1334
+ const classification = runtimeTarget
1335
+ ? await this.classifyRuntimeDispatchDelivery(runtimeTarget, dispatch.agents[agent].baseline)
1336
+ : await resolveProvider(persistedTarget.driver)
1337
+ .live.classifyDispatchDelivery(persistedTarget.sessionFilePath, dispatch.agents[agent].baseline);
1262
1338
  if (!this.isCurrentGenerationOperation(generationId, operation))
1263
1339
  return false;
1264
1340
  if (!this.isCurrentDispatch(generationId, dispatch.token))
@@ -1334,6 +1410,8 @@ export class CrosscheckCoordinator {
1334
1410
  this.refreshStatusHint();
1335
1411
  }
1336
1412
  async maybeSettleReconcileGate() {
1413
+ if (this.finishing)
1414
+ return;
1337
1415
  const snapshot = this.store.snapshot();
1338
1416
  if (snapshot.generation.reconcilePendingSettlement
1339
1417
  && !snapshot.generation.dispatch
@@ -1404,6 +1482,12 @@ export class CrosscheckCoordinator {
1404
1482
  this.refreshStatusHint();
1405
1483
  this.options.host.scheduleParentUiRender();
1406
1484
  }
1485
+ async classifyRuntimeDispatchDelivery(runtime, baseline) {
1486
+ const liveClassification = runtime.monitor.classifyDeliverySince?.(baseline);
1487
+ if (liveClassification)
1488
+ return liveClassification;
1489
+ return await resolveProvider(runtime.config.driver).live.classifyDispatchDelivery(runtime.session.identity.sessionFilePath, baseline);
1490
+ }
1407
1491
  markDispatchDelivered(agent, generationId, dispatchToken, expectedStatus) {
1408
1492
  return this.transitionDispatchStatus(agent, generationId, dispatchToken, expectedStatus, "delivered");
1409
1493
  }
@@ -1433,6 +1517,8 @@ export class CrosscheckCoordinator {
1433
1517
  }
1434
1518
  /** Sends the committed Broadcast text and, for Steer/Follow-up, starts the next generation first. */
1435
1519
  async submitBroadcast() {
1520
+ if (this.finishing)
1521
+ return;
1436
1522
  const text = this.broadcast.submissionText();
1437
1523
  if (text === undefined)
1438
1524
  return;
@@ -1538,6 +1624,7 @@ export class CrosscheckCoordinator {
1538
1624
  : text;
1539
1625
  const provider = resolveProvider(runtime.config.driver);
1540
1626
  const releaseInputLock = this.lockAutomatedInput(agent);
1627
+ let receipt;
1541
1628
  try {
1542
1629
  if (generationKind && provider.cancelSequence) {
1543
1630
  await sendCancelSequence(runtime.session.pty, provider.cancelSequence, operation.signal, runtime.monitor.isIdle);
@@ -1545,19 +1632,22 @@ export class CrosscheckCoordinator {
1545
1632
  if (!this.isCurrentGenerationOperation(generationId, operation, { allowRewound: conversational })) {
1546
1633
  return;
1547
1634
  }
1635
+ receipt = await runtime.monitor.reserveTurnFromSession();
1548
1636
  await (this.options.sendInquiry ?? sendInquiryQuestion)(runtime.session, prompt, {
1549
1637
  signal: operation.signal,
1550
1638
  composer: provider.composer,
1639
+ onSubmitWritten: () => receipt?.markWritten(),
1551
1640
  onSubmitted: () => {
1552
1641
  if (!this.isCurrentGenerationOperation(generationId, operation, { allowRewound: conversational }))
1553
1642
  return;
1554
- runtime.monitor.beginTurn();
1643
+ runtime.monitor.beginTurn(receipt);
1555
1644
  },
1556
1645
  ...(generationKind ? { escapeBeforePaste: true } : {}),
1557
1646
  });
1558
1647
  }
1559
1648
  finally {
1560
1649
  releaseInputLock();
1650
+ receipt?.release();
1561
1651
  }
1562
1652
  }
1563
1653
  refreshResults() {
@@ -1579,8 +1669,9 @@ export class CrosscheckCoordinator {
1579
1669
  agent,
1580
1670
  ready: await fileHasContent(generation.resultFiles[agent]),
1581
1671
  })));
1582
- // Guard against a late read from a generation the coordinator has since superseded.
1583
- if (this.store.snapshot().generation.id !== generation.id)
1672
+ // A same-generation rewind also invalidates an outstanding result read.
1673
+ const current = this.store.snapshot().generation;
1674
+ if (this.finishing || current.id !== generation.id || current.rewind)
1584
1675
  return;
1585
1676
  for (const { agent, ready } of readiness) {
1586
1677
  const runtime = this.agents.get(agent);
@@ -1649,6 +1740,7 @@ export class CrosscheckCoordinator {
1649
1740
  if (this.finishing
1650
1741
  || this.dispatching
1651
1742
  || !this.sessionsReady
1743
+ || this.launchReconciliationPending
1652
1744
  || this.resendAfterLaunch.size === 0
1653
1745
  || !this.allResultsReady()
1654
1746
  || [...this.agents.values()].some((runtime) => runtime.freshForPendingDispatch)
@@ -1658,6 +1750,12 @@ export class CrosscheckCoordinator {
1658
1750
  this.runDispatchOperation(undefined, this.retryPersistedDispatch(), "resume crosscheck");
1659
1751
  }
1660
1752
  async handleAgentIdle(agent, progressIdentity = this.takeCrosscheckTurnIdentity(agent)) {
1753
+ if (this.finishing)
1754
+ return;
1755
+ if (this.launchReconciliationPending) {
1756
+ await this.reconcileLaunchDispatch();
1757
+ return;
1758
+ }
1661
1759
  this.settleCrosscheckSlot(agent, progressIdentity);
1662
1760
  const runtime = this.agents.get(agent);
1663
1761
  if (runtime?.freshForPendingDispatch
@@ -1694,8 +1792,7 @@ export class CrosscheckCoordinator {
1694
1792
  if (runtime
1695
1793
  && dispatch?.agents[agent].status === "submitted"
1696
1794
  && !runtime.freshForPendingDispatch) {
1697
- const classification = await resolveProvider(runtime.config.driver)
1698
- .live.classifyDispatchDelivery(runtime.session.identity.sessionFilePath, dispatch.agents[agent].baseline);
1795
+ const classification = await this.classifyRuntimeDispatchDelivery(runtime, dispatch.agents[agent].baseline);
1699
1796
  if (!this.isCurrentDispatch(snapshot.generation.id, dispatch.token)) {
1700
1797
  this.maybeRetryPersistedDispatch();
1701
1798
  return;
@@ -1773,6 +1870,7 @@ export class CrosscheckCoordinator {
1773
1870
  this.launchAbort?.abort(new Error("Crosscheck launch cancelled"));
1774
1871
  this.dispatchAbort.abort(new Error("Crosscheck coordinator stopped"));
1775
1872
  this.abortActiveDispatchOperations(new Error("Crosscheck coordinator stopped"));
1873
+ this.options.host.scheduleParentUiRender();
1776
1874
  this.finishResolve?.({ kind: "crosscheck", interrupted: true });
1777
1875
  }
1778
1876
  fail(error) {
@@ -1799,24 +1897,32 @@ export class CrosscheckCoordinator {
1799
1897
  if (this.activityTimer)
1800
1898
  clearInterval(this.activityTimer);
1801
1899
  this.activityTimer = undefined;
1802
- if (this.transientFlash) {
1803
- clearTimeout(this.transientFlash.timer);
1804
- this.transientFlash = undefined;
1900
+ try {
1901
+ const monitors = await Promise.allSettled([...this.agents.values()].map((runtime) => runtime.monitor.shutdown()));
1902
+ await Promise.allSettled([...this.dispatchOperations]);
1903
+ await this.resultRefresh;
1904
+ this.trackActivity();
1905
+ await Promise.allSettled([...this.activityOperations]);
1906
+ await this.store?.drain();
1907
+ const failedMonitor = monitors.find((result) => result.status === "rejected");
1908
+ if (failedMonitor?.status === "rejected")
1909
+ throw failedMonitor.reason;
1805
1910
  }
1806
- this.detachUi?.();
1807
- this.detachUi = undefined;
1808
- this.options.host.clearMouseModeSource(MOUSE_SOURCE);
1809
- await Promise.all([...this.agents.values()].map((runtime) => runtime.monitor.shutdown()));
1810
- await Promise.allSettled([...this.dispatchOperations]);
1811
- this.trackActivity();
1812
- await Promise.allSettled([...this.activityOperations]);
1813
- for (const runtime of this.agents.values()) {
1814
- runtime.session.close();
1911
+ finally {
1912
+ for (const runtime of this.agents.values())
1913
+ runtime.session.close();
1914
+ for (const session of this.launchingSessions.values())
1915
+ session.close();
1916
+ if (this.transientFlash) {
1917
+ clearTimeout(this.transientFlash.timer);
1918
+ this.transientFlash = undefined;
1919
+ }
1920
+ this.detachUi?.();
1921
+ this.detachUi = undefined;
1922
+ this.options.host.clearMouseModeSource(MOUSE_SOURCE);
1923
+ this.agents.clear();
1924
+ this.launchingSessions.clear();
1815
1925
  }
1816
- for (const session of this.launchingSessions.values())
1817
- session.close();
1818
- this.agents.clear();
1819
- this.launchingSessions.clear();
1820
1926
  }
1821
1927
  runDispatchOperation(agent, operation, action) {
1822
1928
  const tracked = operation
@@ -1921,6 +2027,7 @@ export class CrosscheckCoordinator {
1921
2027
  catch {
1922
2028
  // Result may not exist while the executor is still working.
1923
2029
  }
2030
+ const observerDiagnostics = runtime?.session.observerDiagnostics?.() ?? [];
1924
2031
  return {
1925
2032
  seq: index,
1926
2033
  stepId: agent,
@@ -1934,6 +2041,9 @@ export class CrosscheckCoordinator {
1934
2041
  sessionFilePaths: runtime ? [runtime.session.identity.sessionFilePath] : [],
1935
2042
  inFlight: true,
1936
2043
  ...(runtime && { ptyTailSnapshot: runtime.session.ptyTail.snapshot() }),
2044
+ ...(observerDiagnostics && observerDiagnostics.length > 0 && {
2045
+ observerDiagnostics,
2046
+ }),
1937
2047
  };
1938
2048
  }));
1939
2049
  }
@@ -1983,6 +2093,16 @@ export function buildCrosscheckConfigurations(values) {
1983
2093
  function otherAgent(agent) {
1984
2094
  return agent === "agent-1" ? "agent-2" : "agent-1";
1985
2095
  }
2096
+ function observerLaunchFailureError(driver, agent, cause) {
2097
+ if (!(cause instanceof Error))
2098
+ return cause;
2099
+ return /\bobserver\b/i.test(cause.message)
2100
+ ? observerFailureError(driver, agent, cause)
2101
+ : cause;
2102
+ }
2103
+ function observerFailureError(driver, agent, cause) {
2104
+ return new Error(`${driver} ${agentLabel(agent)} observer failed: ${cause.message}`, { cause });
2105
+ }
1986
2106
  function agentLabel(agent) {
1987
2107
  return agent === "agent-1" ? "Agent 1" : "Agent 2";
1988
2108
  }