poly-weaver 0.13.4 → 0.14.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 (230) hide show
  1. package/dist/agents/executors/dsl.d.ts +11 -0
  2. package/dist/agents/executors/dsl.d.ts.map +1 -0
  3. package/dist/agents/executors/dsl.js +11 -0
  4. package/dist/agents/executors/dsl.js.map +1 -0
  5. package/dist/agents/executors/index.d.ts +8 -0
  6. package/dist/agents/executors/index.d.ts.map +1 -0
  7. package/dist/agents/executors/index.js +17 -0
  8. package/dist/agents/executors/index.js.map +1 -0
  9. package/dist/agents/executors/prompts.d.ts +18 -0
  10. package/dist/agents/executors/prompts.d.ts.map +1 -0
  11. package/dist/agents/executors/prompts.js +62 -0
  12. package/dist/agents/executors/prompts.js.map +1 -0
  13. package/dist/agents/executors/types.d.ts +8 -0
  14. package/dist/agents/executors/types.d.ts.map +1 -0
  15. package/dist/agents/executors/types.js +2 -0
  16. package/dist/agents/executors/types.js.map +1 -0
  17. package/dist/agents/index.d.ts +2 -0
  18. package/dist/agents/index.d.ts.map +1 -1
  19. package/dist/agents/index.js +4 -0
  20. package/dist/agents/index.js.map +1 -1
  21. package/dist/agents/registry.d.ts +1 -1
  22. package/dist/agents/registry.d.ts.map +1 -1
  23. package/dist/agents/registry.js +3 -0
  24. package/dist/agents/registry.js.map +1 -1
  25. package/dist/agents/runner.d.ts.map +1 -1
  26. package/dist/agents/runner.js +20 -45
  27. package/dist/agents/runner.js.map +1 -1
  28. package/dist/ansi.d.ts +4 -0
  29. package/dist/ansi.d.ts.map +1 -1
  30. package/dist/ansi.js +4 -0
  31. package/dist/ansi.js.map +1 -1
  32. package/dist/cli.js +1 -0
  33. package/dist/cli.js.map +1 -1
  34. package/dist/config.d.ts +2 -2
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/crosscheck/broadcast-composer.d.ts +89 -0
  37. package/dist/crosscheck/broadcast-composer.d.ts.map +1 -0
  38. package/dist/crosscheck/broadcast-composer.js +431 -0
  39. package/dist/crosscheck/broadcast-composer.js.map +1 -0
  40. package/dist/crosscheck/coordinator.d.ts +154 -0
  41. package/dist/crosscheck/coordinator.d.ts.map +1 -0
  42. package/dist/crosscheck/coordinator.js +1695 -0
  43. package/dist/crosscheck/coordinator.js.map +1 -0
  44. package/dist/crosscheck/key-helpers.d.ts +16 -0
  45. package/dist/crosscheck/key-helpers.d.ts.map +1 -0
  46. package/dist/crosscheck/key-helpers.js +42 -0
  47. package/dist/crosscheck/key-helpers.js.map +1 -0
  48. package/dist/crosscheck/live-agent-session.d.ts +38 -0
  49. package/dist/crosscheck/live-agent-session.d.ts.map +1 -0
  50. package/dist/crosscheck/live-agent-session.js +126 -0
  51. package/dist/crosscheck/live-agent-session.js.map +1 -0
  52. package/dist/crosscheck/reconcile.d.ts +10 -0
  53. package/dist/crosscheck/reconcile.d.ts.map +1 -0
  54. package/dist/crosscheck/reconcile.js +29 -0
  55. package/dist/crosscheck/reconcile.js.map +1 -0
  56. package/dist/crosscheck/state.d.ts +102 -0
  57. package/dist/crosscheck/state.d.ts.map +1 -0
  58. package/dist/crosscheck/state.js +221 -0
  59. package/dist/crosscheck/state.js.map +1 -0
  60. package/dist/dsl.d.ts +2 -0
  61. package/dist/dsl.d.ts.map +1 -1
  62. package/dist/dsl.js +2 -1
  63. package/dist/dsl.js.map +1 -1
  64. package/dist/dump/service.d.ts +6 -3
  65. package/dist/dump/service.d.ts.map +1 -1
  66. package/dist/dump/service.js +32 -12
  67. package/dist/dump/service.js.map +1 -1
  68. package/dist/flow/built-in/crosscheck-factory.d.ts +15 -0
  69. package/dist/flow/built-in/crosscheck-factory.d.ts.map +1 -0
  70. package/dist/flow/built-in/crosscheck-factory.js +60 -0
  71. package/dist/flow/built-in/crosscheck-factory.js.map +1 -0
  72. package/dist/flow/built-in/crosscheck.d.ts +6 -0
  73. package/dist/flow/built-in/crosscheck.d.ts.map +1 -0
  74. package/dist/flow/built-in/crosscheck.js +6 -0
  75. package/dist/flow/built-in/crosscheck.js.map +1 -0
  76. package/dist/flow/built-in/default-factory.d.ts.map +1 -1
  77. package/dist/flow/built-in/default-factory.js +1 -0
  78. package/dist/flow/built-in/default-factory.js.map +1 -1
  79. package/dist/flow/built-in/simplify-factory.d.ts.map +1 -1
  80. package/dist/flow/built-in/simplify-factory.js +1 -0
  81. package/dist/flow/built-in/simplify-factory.js.map +1 -1
  82. package/dist/flow/custom/load.d.ts.map +1 -1
  83. package/dist/flow/custom/load.js +4 -0
  84. package/dist/flow/custom/load.js.map +1 -1
  85. package/dist/flow/dsl.d.ts +1 -0
  86. package/dist/flow/dsl.d.ts.map +1 -1
  87. package/dist/flow/dsl.js +1 -0
  88. package/dist/flow/dsl.js.map +1 -1
  89. package/dist/flow/index.d.ts +2 -1
  90. package/dist/flow/index.d.ts.map +1 -1
  91. package/dist/flow/index.js +2 -1
  92. package/dist/flow/index.js.map +1 -1
  93. package/dist/flow/params.d.ts +7 -1
  94. package/dist/flow/params.d.ts.map +1 -1
  95. package/dist/flow/params.js +28 -13
  96. package/dist/flow/params.js.map +1 -1
  97. package/dist/flow/types.d.ts +8 -1
  98. package/dist/flow/types.d.ts.map +1 -1
  99. package/dist/flow/types.js.map +1 -1
  100. package/dist/offscreen/controller.d.ts +9 -3
  101. package/dist/offscreen/controller.d.ts.map +1 -1
  102. package/dist/offscreen/controller.js +149 -75
  103. package/dist/offscreen/controller.js.map +1 -1
  104. package/dist/offscreen/fork-busy.d.ts +2 -1
  105. package/dist/offscreen/fork-busy.d.ts.map +1 -1
  106. package/dist/offscreen/fork-busy.js +5 -2
  107. package/dist/offscreen/fork-busy.js.map +1 -1
  108. package/dist/offscreen/fork.d.ts +1 -1
  109. package/dist/offscreen/fork.d.ts.map +1 -1
  110. package/dist/offscreen/fork.js +4 -2
  111. package/dist/offscreen/fork.js.map +1 -1
  112. package/dist/offscreen/inquiry-sender.d.ts +4 -9
  113. package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
  114. package/dist/offscreen/inquiry-sender.js +5 -54
  115. package/dist/offscreen/inquiry-sender.js.map +1 -1
  116. package/dist/offscreen/session-turn-monitor.d.ts +66 -0
  117. package/dist/offscreen/session-turn-monitor.d.ts.map +1 -0
  118. package/dist/offscreen/session-turn-monitor.js +257 -0
  119. package/dist/offscreen/session-turn-monitor.js.map +1 -0
  120. package/dist/orchestrator.d.ts +7 -6
  121. package/dist/orchestrator.d.ts.map +1 -1
  122. package/dist/orchestrator.js +122 -61
  123. package/dist/orchestrator.js.map +1 -1
  124. package/dist/preview-panel.d.ts +1 -0
  125. package/dist/preview-panel.d.ts.map +1 -1
  126. package/dist/preview-panel.js +24 -20
  127. package/dist/preview-panel.js.map +1 -1
  128. package/dist/providers/claude/completion.d.ts +1 -0
  129. package/dist/providers/claude/completion.d.ts.map +1 -1
  130. package/dist/providers/claude/completion.js +5 -0
  131. package/dist/providers/claude/completion.js.map +1 -1
  132. package/dist/providers/claude/composer.d.ts +6 -0
  133. package/dist/providers/claude/composer.d.ts.map +1 -0
  134. package/dist/providers/claude/composer.js +7 -0
  135. package/dist/providers/claude/composer.js.map +1 -0
  136. package/dist/providers/claude/session.d.ts +1 -0
  137. package/dist/providers/claude/session.d.ts.map +1 -1
  138. package/dist/providers/claude/session.js +21 -1
  139. package/dist/providers/claude/session.js.map +1 -1
  140. package/dist/providers/claude/strategy.d.ts +6 -1
  141. package/dist/providers/claude/strategy.d.ts.map +1 -1
  142. package/dist/providers/claude/strategy.js +40 -11
  143. package/dist/providers/claude/strategy.js.map +1 -1
  144. package/dist/providers/codex/composer.d.ts +6 -0
  145. package/dist/providers/codex/composer.d.ts.map +1 -0
  146. package/dist/providers/codex/composer.js +143 -0
  147. package/dist/providers/codex/composer.js.map +1 -0
  148. package/dist/providers/codex/plan-mode-activator.d.ts +4 -6
  149. package/dist/providers/codex/plan-mode-activator.d.ts.map +1 -1
  150. package/dist/providers/codex/plan-mode-activator.js +16 -21
  151. package/dist/providers/codex/plan-mode-activator.js.map +1 -1
  152. package/dist/providers/codex/strategy.d.ts +5 -1
  153. package/dist/providers/codex/strategy.d.ts.map +1 -1
  154. package/dist/providers/codex/strategy.js +8 -3
  155. package/dist/providers/codex/strategy.js.map +1 -1
  156. package/dist/providers/composer-utils.d.ts +9 -0
  157. package/dist/providers/composer-utils.d.ts.map +1 -0
  158. package/dist/providers/composer-utils.js +218 -0
  159. package/dist/providers/composer-utils.js.map +1 -0
  160. package/dist/providers/copilot/completion.d.ts +6 -2
  161. package/dist/providers/copilot/completion.d.ts.map +1 -1
  162. package/dist/providers/copilot/completion.js +56 -11
  163. package/dist/providers/copilot/completion.js.map +1 -1
  164. package/dist/providers/copilot/composer.d.ts +6 -0
  165. package/dist/providers/copilot/composer.d.ts.map +1 -0
  166. package/dist/providers/copilot/composer.js +10 -0
  167. package/dist/providers/copilot/composer.js.map +1 -0
  168. package/dist/providers/copilot/strategy.d.ts +6 -1
  169. package/dist/providers/copilot/strategy.d.ts.map +1 -1
  170. package/dist/providers/copilot/strategy.js +69 -12
  171. package/dist/providers/copilot/strategy.js.map +1 -1
  172. package/dist/providers/fork.d.ts +3 -1
  173. package/dist/providers/fork.d.ts.map +1 -1
  174. package/dist/providers/fork.js.map +1 -1
  175. package/dist/providers/index.d.ts +1 -1
  176. package/dist/providers/index.d.ts.map +1 -1
  177. package/dist/providers/live.d.ts +17 -0
  178. package/dist/providers/live.d.ts.map +1 -0
  179. package/dist/providers/live.js +201 -0
  180. package/dist/providers/live.js.map +1 -0
  181. package/dist/providers/session-launch.d.ts +25 -0
  182. package/dist/providers/session-launch.d.ts.map +1 -0
  183. package/dist/providers/session-launch.js +54 -0
  184. package/dist/providers/session-launch.js.map +1 -0
  185. package/dist/providers/types.d.ts +75 -4
  186. package/dist/providers/types.d.ts.map +1 -1
  187. package/dist/providers/types.js.map +1 -1
  188. package/dist/resume-tui.d.ts.map +1 -1
  189. package/dist/resume-tui.js +3 -1
  190. package/dist/resume-tui.js.map +1 -1
  191. package/dist/session/list.d.ts.map +1 -1
  192. package/dist/session/list.js +5 -3
  193. package/dist/session/list.js.map +1 -1
  194. package/dist/session/locator.d.ts.map +1 -1
  195. package/dist/session/locator.js +6 -22
  196. package/dist/session/locator.js.map +1 -1
  197. package/dist/session/reader.d.ts +8 -0
  198. package/dist/session/reader.d.ts.map +1 -1
  199. package/dist/session/reader.js +12 -2
  200. package/dist/session/reader.js.map +1 -1
  201. package/dist/session/resume.d.ts.map +1 -1
  202. package/dist/session/resume.js +1 -0
  203. package/dist/session/resume.js.map +1 -1
  204. package/dist/session/session-store.d.ts +3 -0
  205. package/dist/session/session-store.d.ts.map +1 -1
  206. package/dist/session/session-store.js +18 -6
  207. package/dist/session/session-store.js.map +1 -1
  208. package/dist/startup-tui.js +2 -2
  209. package/dist/startup-tui.js.map +1 -1
  210. package/dist/status-bar.d.ts +3 -0
  211. package/dist/status-bar.d.ts.map +1 -1
  212. package/dist/status-bar.js.map +1 -1
  213. package/dist/terminal/host.d.ts +19 -3
  214. package/dist/terminal/host.d.ts.map +1 -1
  215. package/dist/terminal/host.js +45 -7
  216. package/dist/terminal/host.js.map +1 -1
  217. package/dist/terminal/kitty-negotiator.js +1 -1
  218. package/dist/terminal/kitty-negotiator.js.map +1 -1
  219. package/dist/terminal/paste-submit.d.ts +18 -27
  220. package/dist/terminal/paste-submit.d.ts.map +1 -1
  221. package/dist/terminal/paste-submit.js +81 -128
  222. package/dist/terminal/paste-submit.js.map +1 -1
  223. package/dist/terminal-input.d.ts.map +1 -1
  224. package/dist/terminal-input.js +3 -4
  225. package/dist/terminal-input.js.map +1 -1
  226. package/dist/util/abortable-sleep.d.ts +6 -0
  227. package/dist/util/abortable-sleep.d.ts.map +1 -0
  228. package/dist/util/abortable-sleep.js +40 -0
  229. package/dist/util/abortable-sleep.js.map +1 -0
  230. package/package.json +1 -1
@@ -0,0 +1,1695 @@
1
+ import { open, readFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { bold, MUTED_YELLOW_BG, padOrClip, reset, TRUE_BLACK_FG, } from "../ansi.js";
4
+ import { APP_VERSION } from "../version.js";
5
+ import { buildExecutorInitialPrompt, buildExecutorCrosscheckPrompt, buildExecutorEffectiveTaskContext, buildExecutorFollowUpPrompt, buildExecutorReconcilePrompt, buildExecutorSteerFreshPrompt, buildExecutorSteerPrompt, } from "../agents/executors/prompts.js";
6
+ import { sendInquiryQuestion } from "../offscreen/inquiry-sender.js";
7
+ import { SessionTurnMonitor } from "../offscreen/session-turn-monitor.js";
8
+ import { resolveProvider } from "../providers/registry.js";
9
+ import { encodeKeysToBytes } from "../terminal/key-to-bytes.js";
10
+ import { parseSgrMouseFrame, WHEEL_LINES_PER_EVENT, } from "../terminal/input-router.js";
11
+ import { renderViewportLines, shouldShowVtCursor } from "../terminal/render.js";
12
+ import { buildSplitSurface, computeSplitMetrics, SPLIT_COLLAPSE_AT_COLS, } from "../user/split-surface.js";
13
+ import { LiveAgentSession } from "./live-agent-session.js";
14
+ import { crosscheckJsonFilePath, CrosscheckStateStore, reconcileJsonFilePath, } from "./state.js";
15
+ import { abortableSleep } from "../util/abortable-sleep.js";
16
+ import { readReviewResult } from "./reconcile.js";
17
+ import { availableBroadcastModes, BroadcastComposer, } from "./broadcast-composer.js";
18
+ import { isControlKey, isEnterKey } from "./key-helpers.js";
19
+ import { TASK } from "../flow/types.js";
20
+ import { isArtifactViewerKey } from "../dump/hotkey.js";
21
+ const HEADER_ROWS = 1;
22
+ const RESULT_POLL_MS = 300;
23
+ const ACTIVITY_HEARTBEAT_MS = 60_000;
24
+ const MOUSE_SOURCE = "crosscheck";
25
+ const RESUME_CONTINUE_PROMPT = "Continue your work from where you were interrupted.";
26
+ /** Rows reserved for the two agent panes even when Broadcast wants to grow further. */
27
+ const MIN_AGENT_ROWS = 3;
28
+ function dispatchAbortReason(signal) {
29
+ return signal.reason ?? new Error("Crosscheck dispatch cancelled");
30
+ }
31
+ function throwIfDispatchAborted(signal) {
32
+ if (signal.aborted)
33
+ throw dispatchAbortReason(signal);
34
+ }
35
+ function waitForCancelDelay(delayMs, signal) {
36
+ if (delayMs <= 0)
37
+ return Promise.resolve();
38
+ return abortableSleep(delayMs, signal, { onAbort: "reject-reason" });
39
+ }
40
+ async function sendCancelSequence(pty, sequence, signal) {
41
+ const delayMs = Math.max(0, sequence.delayMs ?? 0);
42
+ for (const [index, input] of sequence.inputs.entries()) {
43
+ throwIfDispatchAborted(signal);
44
+ pty.write(input);
45
+ if (index < sequence.inputs.length - 1) {
46
+ await waitForCancelDelay(delayMs, signal);
47
+ }
48
+ }
49
+ }
50
+ export class CrosscheckCoordinator {
51
+ options;
52
+ agents = new Map();
53
+ launchingSessions = new Map();
54
+ focus = "broadcast";
55
+ /** The agent shown in the narrow single-pane layout; retained while Broadcast is focused. */
56
+ visibleAgent = "agent-1";
57
+ broadcast = new BroadcastComposer({
58
+ agent1Ready: false,
59
+ agent2Ready: false,
60
+ });
61
+ store;
62
+ resultTimer;
63
+ activityTimer;
64
+ detachUi;
65
+ finishResolve;
66
+ finishReject;
67
+ finishing = false;
68
+ pendingFailure;
69
+ dispatching = false;
70
+ broadcastSendActive = false;
71
+ dispatchAbort = new AbortController();
72
+ dispatchOperations = new Set();
73
+ activityOperations = new Set();
74
+ resendAfterLaunch = new Set();
75
+ reconciliationBlocked = new Set();
76
+ freshDispatchPreparations = new Set();
77
+ automatedInputOwners = new Set();
78
+ automatedInputDepth = new Map();
79
+ resultRefresh;
80
+ launchAbort;
81
+ sessionsReady = false;
82
+ transientFlash;
83
+ crosscheckProgress;
84
+ selectionAgent;
85
+ crosscheckTurnIdentities = new Map();
86
+ constructor(options) {
87
+ this.options = options;
88
+ }
89
+ sessionFor(agent) {
90
+ return this.agents.get(agent)?.session ?? this.launchingSessions.get(agent);
91
+ }
92
+ async run() {
93
+ this.store = await CrosscheckStateStore.open(this.options.persistentDir);
94
+ await this.store.ensureEffectiveTask(this.options.task);
95
+ await this.options.touchActivity?.();
96
+ try {
97
+ this.startActivityHeartbeat();
98
+ this.options.dumpService?.initConcurrent("crosscheck", this.options.flowParams, this.options.workdir, () => this.dumpRecords(), this.options.resumedFrom);
99
+ this.refreshStatusHint();
100
+ this.detachUi = this.options.host.attachParentUi(this);
101
+ const layout = this.options.host.getLayout();
102
+ const dimensions = this.paneDimensions(layout.columns, layout.childRows);
103
+ const persisted = this.store.snapshot();
104
+ const launchAbort = new AbortController();
105
+ this.launchAbort = launchAbort;
106
+ const completion = new Promise((resolve, reject) => {
107
+ this.finishResolve = resolve;
108
+ this.finishReject = reject;
109
+ if (this.pendingFailure !== undefined)
110
+ reject(this.pendingFailure);
111
+ });
112
+ let hasLaunchFailure = false;
113
+ let firstLaunchFailure;
114
+ const launches = ["agent-1", "agent-2"].map(async (agent) => {
115
+ try {
116
+ const config = this.options.configurations[agent];
117
+ const resultFile = persisted.generation.resultFiles[agent];
118
+ const resultReady = await fileHasContent(resultFile);
119
+ const prior = persisted.sessions[agent];
120
+ const resumeIdentity = prior?.driver === config.driver
121
+ ? {
122
+ sessionId: prior.sessionId,
123
+ sessionFilePath: prior.sessionFilePath,
124
+ }
125
+ : undefined;
126
+ const resumePrompt = persisted.generation.kind === "steer"
127
+ ? buildExecutorSteerPrompt(persisted.generation.prompt, resultFile)
128
+ : persisted.generation.kind === "follow-up"
129
+ ? buildExecutorFollowUpPrompt(persisted.generation.prompt, resultFile)
130
+ : buildExecutorInitialPrompt(this.options.task, resultFile);
131
+ const freshPrompt = persisted.generation.kind === "steer"
132
+ ? buildExecutorSteerFreshPrompt(persisted.generation.effectiveTask ?? this.options.task, persisted.generation.steerHistory, resultFile)
133
+ : resumePrompt;
134
+ const activityState = resumeIdentity
135
+ ? await resolveProvider(config.driver).live.classifyActivityState(prior.sessionFilePath)
136
+ : undefined;
137
+ const priorSubmitCount = persisted.generation.deliverySubmitCounts?.[agent];
138
+ const generationDelivered = resultReady
139
+ ? true
140
+ : resumeIdentity && priorSubmitCount !== undefined
141
+ ? await resolveProvider(config.driver).live.classifyGenerationDelivery(prior.sessionFilePath, priorSubmitCount) === "present"
142
+ : undefined;
143
+ const prompt = generationDelivered === false
144
+ ? resumePrompt
145
+ : activityState === "idle"
146
+ ? ""
147
+ : activityState === "working"
148
+ ? RESUME_CONTINUE_PROMPT
149
+ : resumePrompt;
150
+ const session = await (this.options.launchSession ?? LiveAgentSession.launch)({
151
+ driver: config.driver,
152
+ workdir: this.options.workdir,
153
+ prompt,
154
+ freshPrompt,
155
+ yolo: this.options.yolo,
156
+ model: config.model,
157
+ effort: config.effort,
158
+ resumeIdentity,
159
+ output: this.options.output,
160
+ signal: launchAbort.signal,
161
+ cols: dimensions[agent].cols,
162
+ rows: dimensions[agent].rows,
163
+ sessionFileWaitMs: this.options.sessionFileWaitMs,
164
+ onRender: () => this.options.host.scheduleParentUiRender(),
165
+ onOscClipboard: (data) => this.options.host.forwardOscClipboard(data),
166
+ onSpawn: (session) => {
167
+ this.launchingSessions.set(agent, session);
168
+ this.options.host.scheduleParentUiRender();
169
+ },
170
+ onExit: (event) => {
171
+ if (!this.finishing) {
172
+ this.fail(new Error(`${config.driver} ${agentLabel(agent)} exited unexpectedly (code ${event.exitCode})`));
173
+ }
174
+ },
175
+ });
176
+ this.launchingSessions.delete(agent);
177
+ const monitor = new SessionTurnMonitor(config.driver, session.identity.sessionFilePath, session.baseline, {
178
+ onChange: (idle) => {
179
+ this.refreshStatusHint();
180
+ this.options.host.scheduleParentUiRender();
181
+ if (idle) {
182
+ const progressIdentity = this.takeCrosscheckTurnIdentity(agent);
183
+ this.runDispatchOperation(agent, this.handleAgentIdle(agent, progressIdentity), "prepare resumed crosscheck");
184
+ }
185
+ },
186
+ onReadFailure: () => {
187
+ this.options.output.warn(`${config.driver} ${agentLabel(agent)} session log is unreadable; crosscheck remains disarmed`);
188
+ },
189
+ });
190
+ const persistedDispatch = persisted.generation.dispatch;
191
+ const freshForPendingDispatch = persistedDispatch !== undefined
192
+ && (persistedDispatch.phase === "crosscheck"
193
+ ? !persisted.generation.consumed
194
+ : true)
195
+ && (prior?.pendingDispatchBaselineReset === true
196
+ || session.resumed !== true);
197
+ this.agents.set(agent, {
198
+ config,
199
+ session,
200
+ monitor,
201
+ resultReady,
202
+ freshForPendingDispatch,
203
+ launchTurnExpected: session.resumed !== true || prompt.length > 0,
204
+ });
205
+ await this.store.update((state) => {
206
+ const dispatch = state.generation.dispatch;
207
+ if (freshForPendingDispatch
208
+ && state.generation.id === persisted.generation.id
209
+ && dispatch !== undefined
210
+ && persisted.generation.dispatch !== undefined
211
+ && dispatch.token === persisted.generation.dispatch.token) {
212
+ if (dispatch.phase === "crosscheck") {
213
+ state.generation.consumed = false;
214
+ }
215
+ dispatch.agents[agent].status = "pending";
216
+ }
217
+ state.sessions[agent] = {
218
+ driver: config.driver,
219
+ ...session.identity,
220
+ ...(freshForPendingDispatch && {
221
+ pendingDispatchBaselineReset: true,
222
+ }),
223
+ };
224
+ });
225
+ if (freshForPendingDispatch
226
+ && session.resumed === true
227
+ && activityState === "idle"
228
+ && !this.agents.get(agent)?.launchTurnExpected) {
229
+ await this.handleAgentIdle(agent);
230
+ }
231
+ }
232
+ catch (err) {
233
+ this.launchingSessions.get(agent)?.close();
234
+ this.launchingSessions.delete(agent);
235
+ if (!hasLaunchFailure) {
236
+ hasLaunchFailure = true;
237
+ firstLaunchFailure = err;
238
+ launchAbort.abort(err);
239
+ }
240
+ throw err;
241
+ }
242
+ });
243
+ const launchResults = await Promise.allSettled(launches);
244
+ const failedLaunch = launchResults.find((result) => result.status === "rejected");
245
+ this.launchAbort = undefined;
246
+ if (failedLaunch && !this.finishing)
247
+ throw firstLaunchFailure;
248
+ if (this.finishing)
249
+ return await completion;
250
+ this.sessionsReady = true;
251
+ this.refreshStatusHint();
252
+ this.options.host.scheduleParentUiRender();
253
+ for (const runtime of this.agents.values()) {
254
+ if (runtime.launchTurnExpected)
255
+ runtime.monitor.beginTurn();
256
+ }
257
+ await this.reconcilePersistedDispatch();
258
+ await this.maybeSettleReconcileGate();
259
+ this.broadcast.reconcileMode(this.currentReadiness());
260
+ this.refreshMouseModes();
261
+ this.resultTimer = setInterval(() => {
262
+ void this.refreshResults();
263
+ }, RESULT_POLL_MS);
264
+ await this.refreshResults();
265
+ this.maybeRetryPersistedDispatch();
266
+ return await completion;
267
+ }
268
+ finally {
269
+ await this.dispose();
270
+ }
271
+ }
272
+ getSurface() {
273
+ const layout = this.options.host.getLayout();
274
+ const totalRows = layout.childRows;
275
+ const broadcastRows = this.broadcastRowCount(layout.columns, totalRows);
276
+ const agentAreaRows = Math.max(1, totalRows - broadcastRows);
277
+ const metrics = computeSplitMetrics({
278
+ cols: layout.columns,
279
+ rows: agentAreaRows,
280
+ collapseAt: SPLIT_COLLAPSE_AT_COLS,
281
+ });
282
+ const splitOptions = {
283
+ cols: layout.columns,
284
+ rows: agentAreaRows,
285
+ collapseAt: SPLIT_COLLAPSE_AT_COLS,
286
+ };
287
+ const top = metrics.collapsed
288
+ ? buildSplitSurface(this.renderPane(this.visibleAgent, metrics.leftWidth, agentAreaRows), null, splitOptions)
289
+ : buildSplitSurface(this.renderPane("agent-1", metrics.leftWidth, agentAreaRows), this.renderPane("agent-2", metrics.rightWidth, agentAreaRows), splitOptions, this.focus === "agent-2" ? "right" : "left");
290
+ const crosscheck = this.currentCrosscheckStatus();
291
+ const submissionState = this.currentSubmissionState(crosscheck);
292
+ const broadcastPane = this.broadcast.render(layout.columns, this.focus === "broadcast", this.currentReadiness(), this.currentTrackedTask(), crosscheck, submissionState, broadcastRows, {
293
+ focusSwitchHint: this.focusSwitchHint(),
294
+ version: `poly-weaver v${APP_VERSION}`,
295
+ transient: this.currentTransientFlash(),
296
+ });
297
+ const lines = [...top.lines.slice(0, agentAreaRows), ...broadcastPane.lines];
298
+ while (lines.length < totalRows)
299
+ lines.push("");
300
+ if (lines.length > totalRows)
301
+ lines.length = totalRows;
302
+ const surface = { lines };
303
+ if (this.focus === "broadcast") {
304
+ surface.cursor = {
305
+ row: agentAreaRows + broadcastPane.cursor.row,
306
+ col: broadcastPane.cursor.col,
307
+ };
308
+ surface.showCursor = broadcastPane.showCursor;
309
+ }
310
+ else if (top.cursor) {
311
+ surface.cursor = top.cursor;
312
+ surface.showCursor = top.showCursor;
313
+ }
314
+ return surface;
315
+ }
316
+ selectionRegionAt(column, row) {
317
+ const layout = this.options.host.getLayout();
318
+ const totalRows = layout.childRows;
319
+ const broadcastRows = this.broadcastRowCount(layout.columns, totalRows);
320
+ const agentAreaRows = Math.max(1, totalRows - broadcastRows);
321
+ if (row <= agentAreaRows) {
322
+ if (row <= HEADER_ROWS)
323
+ return null;
324
+ const target = this.agentAtColumn(column);
325
+ if (!target)
326
+ return null; // separator column
327
+ const session = this.sessionFor(target.agent);
328
+ if (!session || session.vt.isAlternate)
329
+ return null;
330
+ const metrics = computeSplitMetrics({
331
+ cols: layout.columns,
332
+ rows: agentAreaRows,
333
+ collapseAt: SPLIT_COLLAPSE_AT_COLS,
334
+ });
335
+ if (metrics.collapsed) {
336
+ return {
337
+ rowStart: HEADER_ROWS + 1,
338
+ rowEnd: agentAreaRows,
339
+ colStart: 1,
340
+ colEnd: layout.columns,
341
+ };
342
+ }
343
+ if (column <= metrics.leftWidth) {
344
+ return {
345
+ rowStart: HEADER_ROWS + 1,
346
+ rowEnd: agentAreaRows,
347
+ colStart: 1,
348
+ colEnd: metrics.leftWidth,
349
+ };
350
+ }
351
+ return {
352
+ rowStart: HEADER_ROWS + 1,
353
+ rowEnd: agentAreaRows,
354
+ colStart: metrics.leftWidth + 2,
355
+ colEnd: layout.columns,
356
+ };
357
+ }
358
+ return {
359
+ rowStart: agentAreaRows + 1,
360
+ rowEnd: totalRows,
361
+ colStart: 1,
362
+ colEnd: layout.columns,
363
+ };
364
+ }
365
+ selectionTargetAt(column, row) {
366
+ const layout = this.options.host.getLayout();
367
+ const totalRows = layout.childRows;
368
+ const broadcastRows = this.broadcastRowCount(layout.columns, totalRows);
369
+ const agentAreaRows = Math.max(1, totalRows - broadcastRows);
370
+ if (row <= HEADER_ROWS || row > agentAreaRows)
371
+ return null;
372
+ const target = this.agentAtColumn(column);
373
+ if (!target)
374
+ return null;
375
+ const session = this.sessionFor(target.agent);
376
+ if (!session || session.vt.isAlternate)
377
+ return null;
378
+ return "offscreen-inquiry";
379
+ }
380
+ forkSelectionPlacement() {
381
+ const agent = this.selectionAgent;
382
+ if (!agent)
383
+ return undefined;
384
+ const session = this.sessionFor(agent);
385
+ if (!session || session.vt.isAlternate)
386
+ return undefined;
387
+ const layout = this.options.host.getLayout();
388
+ const totalRows = layout.childRows;
389
+ const broadcastRows = this.broadcastRowCount(layout.columns, totalRows);
390
+ const agentAreaRows = Math.max(1, totalRows - broadcastRows);
391
+ const metrics = computeSplitMetrics({
392
+ cols: layout.columns,
393
+ rows: agentAreaRows,
394
+ collapseAt: SPLIT_COLLAPSE_AT_COLS,
395
+ });
396
+ if (metrics.collapsed) {
397
+ if (agent !== this.visibleAgent)
398
+ return undefined;
399
+ return {
400
+ region: {
401
+ rowStart: HEADER_ROWS + 1,
402
+ rowEnd: agentAreaRows,
403
+ colStart: 1,
404
+ colEnd: layout.columns,
405
+ },
406
+ vt: session.vt,
407
+ };
408
+ }
409
+ if (agent === "agent-1") {
410
+ return {
411
+ region: {
412
+ rowStart: HEADER_ROWS + 1,
413
+ rowEnd: agentAreaRows,
414
+ colStart: 1,
415
+ colEnd: metrics.leftWidth,
416
+ },
417
+ vt: session.vt,
418
+ };
419
+ }
420
+ return {
421
+ region: {
422
+ rowStart: HEADER_ROWS + 1,
423
+ rowEnd: agentAreaRows,
424
+ colStart: metrics.leftWidth + 2,
425
+ colEnd: layout.columns,
426
+ },
427
+ vt: session.vt,
428
+ };
429
+ }
430
+ scrollSelectableSurfaceAt(column, row, direction) {
431
+ const layout = this.options.host.getLayout();
432
+ const totalRows = layout.childRows;
433
+ const broadcastRows = this.broadcastRowCount(layout.columns, totalRows);
434
+ const agentAreaRows = Math.max(1, totalRows - broadcastRows);
435
+ if (row <= HEADER_ROWS || row > agentAreaRows)
436
+ return 0;
437
+ const target = this.agentAtColumn(column);
438
+ if (!target)
439
+ return 0;
440
+ const session = this.sessionFor(target.agent);
441
+ if (!session || session.vt.isAlternate)
442
+ return 0;
443
+ const before = session.vt.viewportY;
444
+ if (direction === "back")
445
+ session.vt.scrollBack(1);
446
+ else
447
+ session.vt.scrollForward(1);
448
+ return Math.abs(session.vt.viewportY - before);
449
+ }
450
+ onSelectionGestureStart(row, column) {
451
+ const layout = this.options.host.getLayout();
452
+ const totalRows = layout.childRows;
453
+ const broadcastRows = this.broadcastRowCount(layout.columns, totalRows);
454
+ const agentAreaRows = Math.max(1, totalRows - broadcastRows);
455
+ if (row > HEADER_ROWS && row <= agentAreaRows) {
456
+ const target = this.agentAtColumn(column);
457
+ if (target) {
458
+ this.focus = target.agent;
459
+ this.visibleAgent = target.agent;
460
+ this.selectionAgent = target.agent;
461
+ this.resizeSessions();
462
+ this.refreshStatusHint();
463
+ this.options.host.scheduleParentUiRender();
464
+ }
465
+ return;
466
+ }
467
+ if (row > agentAreaRows && row <= totalRows) {
468
+ this.focus = "broadcast";
469
+ }
470
+ }
471
+ suppressesStatusBar() {
472
+ return true;
473
+ }
474
+ flashTransient(message, durationMs = 3000, variant = "default") {
475
+ if (this.transientFlash) {
476
+ clearTimeout(this.transientFlash.timer);
477
+ }
478
+ const timer = setTimeout(() => {
479
+ if (this.transientFlash?.timer !== timer)
480
+ return;
481
+ this.transientFlash = undefined;
482
+ this.options.host.scheduleParentUiRender();
483
+ }, durationMs);
484
+ this.transientFlash = { message, variant, timer };
485
+ this.options.host.scheduleParentUiRender();
486
+ }
487
+ onEvents(events) {
488
+ let forwarded = [];
489
+ let forwardedSide = this.focus === "broadcast" ? undefined : this.focus;
490
+ const flush = () => {
491
+ if (forwarded.length === 0)
492
+ return;
493
+ if (forwardedSide)
494
+ this.forwardEvents(forwardedSide, forwarded);
495
+ forwarded = [];
496
+ };
497
+ let broadcastChanged = false;
498
+ for (const event of events) {
499
+ if (this.focus === "broadcast"
500
+ && event.kind !== "escape"
501
+ && event.kind !== "printable") {
502
+ this.broadcast.endInputSequence();
503
+ }
504
+ if (isControlKey(event, 0x03)) {
505
+ flush();
506
+ this.finish();
507
+ break;
508
+ }
509
+ if (isArtifactViewerKey(event)) {
510
+ flush();
511
+ this.cycleFocus();
512
+ forwardedSide = this.focus === "broadcast" ? undefined : this.focus;
513
+ this.refreshStatusHint();
514
+ this.options.host.scheduleParentUiRender();
515
+ continue;
516
+ }
517
+ if (isControlKey(event, 0x18)) {
518
+ flush();
519
+ const canRetry = this.canRetryPendingDispatch();
520
+ const canCrosscheck = this.isCrosscheckArmed();
521
+ const canReconcile = this.isReconcileArmed();
522
+ if (this.focus === "broadcast" && canRetry) {
523
+ this.runDispatchOperation(undefined, this.retryPersistedDispatch(), "retry crosscheck");
524
+ }
525
+ else if (this.focus === "broadcast" && canCrosscheck) {
526
+ this.runDispatchOperation(undefined, this.startCrosscheckDispatch(), "start crosscheck");
527
+ }
528
+ else if (this.focus === "broadcast" && canReconcile) {
529
+ this.runDispatchOperation(undefined, this.startReconcileDispatch(), "start reconcile");
530
+ }
531
+ continue;
532
+ }
533
+ if (event.kind === "wheel" || event.kind === "raw") {
534
+ flush();
535
+ forwardedSide = this.focus === "broadcast" ? undefined : this.focus;
536
+ if (this.routeMouse(event))
537
+ continue;
538
+ }
539
+ if (this.focus === "broadcast") {
540
+ flush();
541
+ const submissionState = this.currentSubmissionState();
542
+ const outcome = this.broadcast.handleKey(event, this.currentReadiness(), submissionState);
543
+ if (outcome === "submit") {
544
+ this.runDispatchOperation(undefined, this.submitBroadcast(), "broadcast send");
545
+ }
546
+ if (outcome === "changed" || outcome === "mode-cycle")
547
+ broadcastChanged = true;
548
+ continue;
549
+ }
550
+ if (forwardedSide !== this.focus) {
551
+ flush();
552
+ forwardedSide = this.focus;
553
+ }
554
+ if (this.automatedInputOwners.has(this.focus)
555
+ || this.hasPendingCrosscheck(this.focus))
556
+ continue;
557
+ const runtime = this.agents.get(this.focus);
558
+ if (runtime && isEnterKey(event, runtime.session.childKittyFlags)) {
559
+ runtime.monitor.beginTurnAttempt();
560
+ this.refreshStatusHint();
561
+ }
562
+ forwarded.push(event);
563
+ }
564
+ flush();
565
+ this.broadcast.endInputSequence();
566
+ if (broadcastChanged) {
567
+ this.resizeSessions();
568
+ this.refreshStatusHint();
569
+ this.options.host.scheduleParentUiRender();
570
+ }
571
+ return true;
572
+ }
573
+ onResize() {
574
+ this.resizeSessions();
575
+ }
576
+ cycleFocus() {
577
+ const order = ["broadcast", "agent-1", "agent-2"];
578
+ const next = order[(order.indexOf(this.focus) + 1) % order.length];
579
+ this.focus = next;
580
+ if (next !== "broadcast")
581
+ this.visibleAgent = next;
582
+ this.resizeSessions();
583
+ }
584
+ nextFocusTarget() {
585
+ const order = ["broadcast", "agent-1", "agent-2"];
586
+ return order[(order.indexOf(this.focus) + 1) % order.length];
587
+ }
588
+ renderPane(agent, width, rows) {
589
+ const runtime = this.agents.get(agent);
590
+ const launchingSession = this.launchingSessions.get(agent);
591
+ const focused = this.focus === agent;
592
+ const config = runtime?.config ?? this.options.configurations[agent];
593
+ const monitor = runtime?.monitor;
594
+ const label = [
595
+ config.driver,
596
+ config.model,
597
+ config.effort,
598
+ monitor ? (monitor.isIdle ? "idle" : "working") : "starting",
599
+ runtime?.resultReady ? "done" : undefined,
600
+ ].filter(Boolean).join(" | ");
601
+ const color = focused
602
+ ? `${MUTED_YELLOW_BG}${TRUE_BLACK_FG}${bold}`
603
+ : "\x1b[7m";
604
+ const header = `${color}${padOrClip(` ${label} `, width)}${reset}`;
605
+ const session = runtime?.session ?? launchingSession;
606
+ if (!session) {
607
+ return { lines: [header] };
608
+ }
609
+ return {
610
+ lines: [
611
+ header,
612
+ ...renderViewportLines(session.vt, Math.max(0, rows - HEADER_ROWS), width),
613
+ ],
614
+ cursor: {
615
+ row: HEADER_ROWS + session.vt.cursorY,
616
+ col: session.vt.cursorX,
617
+ },
618
+ showCursor: focused && shouldShowVtCursor(session.vt),
619
+ };
620
+ }
621
+ routeMouse(event) {
622
+ const point = event.kind === "wheel"
623
+ ? { column: event.column, row: event.row, button: undefined, final: undefined }
624
+ : parseSgrMouseFrame(event);
625
+ if (!point)
626
+ return false;
627
+ const layout = this.options.host.getLayout();
628
+ const totalRows = layout.childRows;
629
+ const broadcastRows = this.broadcastRowCount(layout.columns, totalRows);
630
+ const agentAreaRows = Math.max(1, totalRows - broadcastRows);
631
+ if (point.row > agentAreaRows) {
632
+ if (event.kind === "raw"
633
+ && point.final === "M"
634
+ && point.button !== undefined
635
+ && (point.button & 3) === 0
636
+ && this.focus !== "broadcast") {
637
+ this.focus = "broadcast";
638
+ this.refreshStatusHint();
639
+ this.options.host.scheduleParentUiRender();
640
+ }
641
+ return true;
642
+ }
643
+ const target = this.agentAtColumn(point.column);
644
+ if (!target)
645
+ return true;
646
+ if (event.kind === "raw"
647
+ && point.final === "M"
648
+ && point.button !== undefined
649
+ && (point.button & 3) === 0) {
650
+ this.focus = target.agent;
651
+ this.visibleAgent = target.agent;
652
+ this.resizeSessions();
653
+ this.refreshStatusHint();
654
+ this.options.host.scheduleParentUiRender();
655
+ }
656
+ const runtime = this.agents.get(target.agent);
657
+ if (!runtime || point.row <= HEADER_ROWS)
658
+ return true;
659
+ const localRow = point.row - HEADER_ROWS;
660
+ if (event.kind === "wheel"
661
+ && !runtime.session.vt.isAlternate
662
+ && !runtime.session.hasMouseReporting()) {
663
+ if (event.direction === "up")
664
+ runtime.session.vt.scrollBack(WHEEL_LINES_PER_EVENT);
665
+ else
666
+ runtime.session.vt.scrollForward(WHEEL_LINES_PER_EVENT);
667
+ this.options.host.scheduleParentUiRender();
668
+ return true;
669
+ }
670
+ if (!runtime.session.hasMouseReporting() && !runtime.session.vt.isAlternate)
671
+ return true;
672
+ const translated = event.kind === "wheel"
673
+ ? { ...event, column: target.localColumn, row: localRow }
674
+ : {
675
+ kind: "raw",
676
+ bytes: Buffer.from(`\x1b[<${point.button};${target.localColumn};${localRow}${point.final}`, "ascii"),
677
+ };
678
+ const bytes = encodeKeysToBytes([translated], { wheelMode: "sgr" });
679
+ if (bytes.length > 0)
680
+ runtime.session.pty.write(bytes);
681
+ return true;
682
+ }
683
+ agentAtColumn(column) {
684
+ const layout = this.options.host.getLayout();
685
+ const totalRows = layout.childRows;
686
+ const agentAreaRows = Math.max(1, totalRows - this.broadcastRowCount(layout.columns, totalRows));
687
+ const metrics = computeSplitMetrics({
688
+ cols: layout.columns,
689
+ rows: agentAreaRows,
690
+ collapseAt: SPLIT_COLLAPSE_AT_COLS,
691
+ });
692
+ if (metrics.collapsed) {
693
+ return { agent: this.visibleAgent, localColumn: Math.max(1, column) };
694
+ }
695
+ if (column <= metrics.leftWidth) {
696
+ return { agent: "agent-1", localColumn: Math.max(1, column) };
697
+ }
698
+ if (column === metrics.leftWidth + 1)
699
+ return undefined;
700
+ return {
701
+ agent: "agent-2",
702
+ localColumn: Math.max(1, column - metrics.leftWidth - 1),
703
+ };
704
+ }
705
+ currentReadiness() {
706
+ return {
707
+ agent1Ready: !!this.agents.get("agent-1")?.resultReady,
708
+ agent2Ready: !!this.agents.get("agent-2")?.resultReady,
709
+ };
710
+ }
711
+ currentResultFile(agent) {
712
+ return this.store.snapshot().generation.resultFiles[agent];
713
+ }
714
+ currentTrackedTask() {
715
+ if (!this.store)
716
+ return this.options.task;
717
+ const generation = this.store.snapshot().generation;
718
+ return generation.id === 0 ? this.options.task : generation.prompt;
719
+ }
720
+ currentCrosscheckStatus() {
721
+ const progress = this.crosscheckProgress;
722
+ if (progress
723
+ && this.store
724
+ && this.store.generationId() === progress.generationId) {
725
+ const completed = Object.values(progress.slots)
726
+ .filter((slot) => slot.settled).length;
727
+ return { kind: "crosschecking", phase: progress.phase, completed };
728
+ }
729
+ const retryPhase = this.retryDispatchPhase();
730
+ if (retryPhase)
731
+ return { kind: "actionable", phase: retryPhase };
732
+ if (this.isCrosscheckArmed()) {
733
+ return { kind: "actionable", phase: "crosscheck" };
734
+ }
735
+ if (this.isReconcileArmed()) {
736
+ return { kind: "actionable", phase: "reconcile" };
737
+ }
738
+ return { kind: "unavailable" };
739
+ }
740
+ currentSubmissionState(crosscheck = this.currentCrosscheckStatus()) {
741
+ if (!this.sessionsReady)
742
+ return "launching";
743
+ if (crosscheck.kind === "crosschecking")
744
+ return "crosschecking";
745
+ if (this.broadcastSendActive)
746
+ return "broadcasting";
747
+ return "ready";
748
+ }
749
+ isArmed() {
750
+ return this.isCrosscheckArmed() || this.isReconcileArmed();
751
+ }
752
+ isCrosscheckArmed() {
753
+ if (!this.store || !this.sessionsReady)
754
+ return false;
755
+ if (this.dispatching || this.store.generationConsumed() || this.store.hasDispatch()) {
756
+ return false;
757
+ }
758
+ return this.allResultsReady() && this.allAgentsIdle();
759
+ }
760
+ isReconcileArmed() {
761
+ if (!this.store || !this.sessionsReady)
762
+ return false;
763
+ if (this.dispatching
764
+ || !this.store.generationConsumed()
765
+ || this.store.hasDispatch()
766
+ || !this.store.reconcileAllowed()
767
+ || !this.store.reconcileSettled()
768
+ || this.store.reconcilePendingSettlement()
769
+ || this.store.reconcileJsonMissing())
770
+ return false;
771
+ return this.allAgentsIdle();
772
+ }
773
+ allResultsReady() {
774
+ const agent1 = this.agents.get("agent-1");
775
+ const agent2 = this.agents.get("agent-2");
776
+ return !!agent1?.resultReady && !!agent2?.resultReady;
777
+ }
778
+ allAgentsIdle() {
779
+ const agent1 = this.agents.get("agent-1");
780
+ const agent2 = this.agents.get("agent-2");
781
+ return !!agent1 && !!agent2 && agent1.monitor.isIdle && agent2.monitor.isIdle;
782
+ }
783
+ hasPendingCrosscheck(agent) {
784
+ const status = this.store?.dispatchAgentStatus(agent);
785
+ return status !== undefined && status !== "delivered";
786
+ }
787
+ canRetryPendingDispatch() {
788
+ if (!this.store)
789
+ return false;
790
+ const dispatchPhase = this.store.dispatchPhase();
791
+ if (this.dispatching
792
+ || !this.sessionsReady
793
+ || dispatchPhase === undefined
794
+ || (this.store.generationConsumed() && dispatchPhase === "crosscheck")
795
+ || !this.allResultsReady()
796
+ || !this.allAgentsIdle()
797
+ || [...this.agents.values()].some((runtime) => runtime.freshForPendingDispatch)) {
798
+ return false;
799
+ }
800
+ return ["agent-1", "agent-2"].some((agent) => this.store.dispatchAgentStatus(agent) === "pending"
801
+ && !this.reconciliationBlocked.has(agent));
802
+ }
803
+ retryDispatchPhase() {
804
+ if (!this.canRetryPendingDispatch())
805
+ return undefined;
806
+ return this.store.dispatchPhase();
807
+ }
808
+ beginCrosscheckProgress(generationId, dispatchToken, phase = "crosscheck") {
809
+ const attemptId = randomUUID();
810
+ this.crosscheckProgress = {
811
+ attemptId,
812
+ generationId,
813
+ dispatchToken,
814
+ phase,
815
+ slots: {
816
+ "agent-1": { submitted: false, settled: false },
817
+ "agent-2": { submitted: false, settled: false },
818
+ },
819
+ };
820
+ this.refreshStatusHint();
821
+ this.options.host.scheduleParentUiRender();
822
+ return attemptId;
823
+ }
824
+ markCrosscheckSlotSubmitted(agent, generationId, dispatchToken, attemptId) {
825
+ const progress = this.crosscheckProgress;
826
+ const generation = this.store.snapshot().generation;
827
+ if (!progress
828
+ || progress.attemptId !== attemptId
829
+ || progress.generationId !== generationId
830
+ || progress.dispatchToken !== dispatchToken
831
+ || generation.id !== generationId
832
+ || generation.dispatch?.token !== dispatchToken)
833
+ return;
834
+ progress.slots[agent].submitted = true;
835
+ this.crosscheckTurnIdentities.set(agent, {
836
+ generationId,
837
+ dispatchToken,
838
+ attemptId,
839
+ });
840
+ }
841
+ takeCrosscheckTurnIdentity(agent) {
842
+ const identity = this.crosscheckTurnIdentities.get(agent);
843
+ this.crosscheckTurnIdentities.delete(agent);
844
+ return identity;
845
+ }
846
+ settleCrosscheckSlot(agent, identity) {
847
+ const progress = this.crosscheckProgress;
848
+ if (!progress
849
+ || !identity
850
+ || progress.attemptId !== identity.attemptId
851
+ || progress.dispatchToken !== identity.dispatchToken
852
+ || progress.generationId !== identity.generationId
853
+ || progress.generationId !== this.store.snapshot().generation.id
854
+ || !progress.slots[agent].submitted
855
+ || progress.slots[agent].settled)
856
+ return;
857
+ progress.slots[agent].settled = true;
858
+ this.finishOrRenderCrosscheckProgress(progress);
859
+ }
860
+ settleUnsubmittedCrosscheckSlots(attemptId) {
861
+ const progress = this.crosscheckProgress;
862
+ if (!progress
863
+ || progress.attemptId !== attemptId
864
+ || progress.generationId !== this.store.snapshot().generation.id)
865
+ return;
866
+ for (const agent of ["agent-1", "agent-2"]) {
867
+ const slot = progress.slots[agent];
868
+ if (!slot.submitted && this.agents.get(agent)?.monitor.isIdle !== false) {
869
+ slot.settled = true;
870
+ }
871
+ }
872
+ this.finishOrRenderCrosscheckProgress(progress);
873
+ }
874
+ finishOrRenderCrosscheckProgress(progress) {
875
+ if (this.crosscheckProgress !== progress)
876
+ return;
877
+ if (Object.values(progress.slots).every((slot) => slot.settled)) {
878
+ this.crosscheckProgress = undefined;
879
+ }
880
+ this.refreshStatusHint();
881
+ this.options.host.scheduleParentUiRender();
882
+ }
883
+ async startCrosscheckDispatch() {
884
+ if (!this.isCrosscheckArmed())
885
+ return;
886
+ await this.startDispatch("crosscheck");
887
+ }
888
+ async startReconcileDispatch() {
889
+ if (!this.isReconcileArmed())
890
+ return;
891
+ const generation = this.store.snapshot().generation;
892
+ await this.startDispatch("reconcile", generation.reconcileRound + 1);
893
+ }
894
+ async startDispatch(phase, round) {
895
+ const generationId = this.store.generationId();
896
+ const token = randomUUID();
897
+ const attemptId = this.beginCrosscheckProgress(generationId, token, phase);
898
+ const agents = ["agent-1", "agent-2"];
899
+ try {
900
+ await this.withDispatchLifecycle(agents, async () => {
901
+ await this.store.update((state) => {
902
+ if (state.generation.id !== generationId)
903
+ return;
904
+ state.generation.reconcileAllowed = false;
905
+ state.generation.reconcileSettled = false;
906
+ state.generation.dispatch = {
907
+ token,
908
+ phase,
909
+ ...(round !== undefined && { round }),
910
+ agents: {
911
+ "agent-1": {
912
+ status: "pending",
913
+ baseline: this.agents.get("agent-1").monitor.lineBaseline,
914
+ },
915
+ "agent-2": {
916
+ status: "pending",
917
+ baseline: this.agents.get("agent-2").monitor.lineBaseline,
918
+ },
919
+ },
920
+ };
921
+ });
922
+ const results = await Promise.allSettled(agents.map((agent) => this.sendDispatchAgent(agent, generationId, attemptId)));
923
+ for (const [index, result] of results.entries()) {
924
+ if (result.status === "rejected") {
925
+ this.warnDispatchFailure(agents[index], "send", result.reason);
926
+ }
927
+ }
928
+ await this.finalizeDispatchIfComplete(generationId);
929
+ });
930
+ }
931
+ finally {
932
+ this.settleUnsubmittedCrosscheckSlots(attemptId);
933
+ }
934
+ }
935
+ async sendDispatchAgent(agent, generationId, progressAttemptId) {
936
+ if (this.reconciliationBlocked.has(agent))
937
+ return;
938
+ const state = this.store.snapshot();
939
+ if (state.generation.id !== generationId)
940
+ return;
941
+ const dispatch = state.generation.dispatch;
942
+ if (!dispatch || dispatch.agents[agent].status !== "pending")
943
+ return;
944
+ const runtime = this.agents.get(agent);
945
+ if (!runtime)
946
+ return;
947
+ let submitted = false;
948
+ const provider = resolveProvider(runtime.config.driver);
949
+ const releaseInputLock = this.lockAutomatedInput(agent);
950
+ try {
951
+ await (this.options.sendInquiry ?? sendInquiryQuestion)(runtime.session, this.buildDispatchPrompt(agent, generationId, dispatch.phase, dispatch.round), {
952
+ signal: this.dispatchAbort.signal,
953
+ composer: provider.composer,
954
+ onSubmitted: () => {
955
+ submitted = true;
956
+ if (progressAttemptId) {
957
+ this.markCrosscheckSlotSubmitted(agent, generationId, dispatch.token, progressAttemptId);
958
+ }
959
+ runtime.monitor.beginTurn();
960
+ },
961
+ });
962
+ }
963
+ finally {
964
+ releaseInputLock();
965
+ }
966
+ if (!submitted)
967
+ return;
968
+ await this.transitionDispatchStatus(agent, generationId, dispatch.token, "pending", "submitted");
969
+ const classification = await provider.live.classifyDispatchDelivery(runtime.session.identity.sessionFilePath, dispatch.agents[agent].baseline);
970
+ if (classification === "present") {
971
+ await this.markDispatchDelivered(agent, generationId, dispatch.token, "submitted");
972
+ }
973
+ await this.finalizeDispatchIfComplete(generationId);
974
+ }
975
+ buildDispatchPrompt(agent, generationId, phase, round) {
976
+ const other = otherAgent(agent);
977
+ if (phase === "crosscheck") {
978
+ return buildExecutorCrosscheckPrompt(this.currentResultFile(other), crosscheckJsonFilePath(this.options.persistentDir, agent, generationId));
979
+ }
980
+ if (round === undefined) {
981
+ throw new Error("Cannot build reconcile prompt without a round");
982
+ }
983
+ const otherJsonFile = round === 1
984
+ ? crosscheckJsonFilePath(this.options.persistentDir, other, generationId)
985
+ : reconcileJsonFilePath(this.options.persistentDir, other, generationId, round - 1);
986
+ const ownJsonFile = reconcileJsonFilePath(this.options.persistentDir, agent, generationId, round);
987
+ return buildExecutorReconcilePrompt(otherJsonFile, ownJsonFile);
988
+ }
989
+ async reconcilePersistedDispatch() {
990
+ const snapshot = this.store.snapshot();
991
+ const generationId = snapshot.generation.id;
992
+ const dispatch = snapshot.generation.dispatch;
993
+ if (!dispatch || (snapshot.generation.consumed && dispatch.phase === "crosscheck")) {
994
+ return;
995
+ }
996
+ for (const agent of ["agent-1", "agent-2"]) {
997
+ const expectedStatus = dispatch.agents[agent].status;
998
+ if (expectedStatus === "delivered")
999
+ continue;
1000
+ const current = await this.reconcileDispatchSide(agent, generationId, dispatch, expectedStatus);
1001
+ if (!current)
1002
+ return;
1003
+ }
1004
+ await this.finalizeDispatchIfComplete(generationId);
1005
+ }
1006
+ async retryPersistedDispatch() {
1007
+ if (this.dispatching
1008
+ || !this.sessionsReady
1009
+ || !this.allResultsReady()
1010
+ || !this.allAgentsIdle())
1011
+ return;
1012
+ const stateAtStart = this.store.snapshot();
1013
+ const generationId = stateAtStart.generation.id;
1014
+ const dispatchAtStart = stateAtStart.generation.dispatch;
1015
+ if (!dispatchAtStart
1016
+ || (stateAtStart.generation.consumed && dispatchAtStart.phase === "crosscheck"))
1017
+ return;
1018
+ const attemptId = this.beginCrosscheckProgress(generationId, dispatchAtStart.token, dispatchAtStart.phase);
1019
+ const lockedAgents = ["agent-1", "agent-2"].filter((agent) => stateAtStart.generation.dispatch?.agents[agent].status === "pending"
1020
+ && !this.reconciliationBlocked.has(agent));
1021
+ try {
1022
+ await this.withDispatchLifecycle(lockedAgents, async () => {
1023
+ const state = this.store.snapshot();
1024
+ if (state.generation.id !== generationId)
1025
+ return;
1026
+ const dispatch = state.generation.dispatch;
1027
+ if (!dispatch || (state.generation.consumed && dispatch.phase === "crosscheck")) {
1028
+ return;
1029
+ }
1030
+ for (const agent of ["agent-1", "agent-2"]) {
1031
+ const expectedStatus = dispatch.agents[agent].status;
1032
+ if (expectedStatus === "delivered")
1033
+ continue;
1034
+ if (this.reconciliationBlocked.has(agent))
1035
+ continue;
1036
+ const current = await this.reconcileDispatchSide(agent, generationId, dispatch, expectedStatus, async () => {
1037
+ try {
1038
+ await this.sendDispatchAgent(agent, generationId, attemptId);
1039
+ }
1040
+ catch (err) {
1041
+ this.warnDispatchFailure(agent, "retry", err);
1042
+ }
1043
+ });
1044
+ if (!current)
1045
+ return;
1046
+ }
1047
+ await this.finalizeDispatchIfComplete(generationId);
1048
+ });
1049
+ }
1050
+ finally {
1051
+ this.settleUnsubmittedCrosscheckSlots(attemptId);
1052
+ }
1053
+ }
1054
+ async reconcileDispatchSide(agent, generationId, dispatch, expectedStatus, onAbsent) {
1055
+ const target = onAbsent
1056
+ ? (() => {
1057
+ const runtime = this.agents.get(agent);
1058
+ return runtime
1059
+ ? {
1060
+ driver: runtime.config.driver,
1061
+ sessionFilePath: runtime.session.identity.sessionFilePath,
1062
+ }
1063
+ : undefined;
1064
+ })()
1065
+ : this.store.snapshot().sessions[agent];
1066
+ if (!target) {
1067
+ if (!onAbsent
1068
+ &&
1069
+ this.isCurrentDispatchSide(agent, generationId, dispatch.token, expectedStatus)) {
1070
+ this.resendAfterLaunch.add(agent);
1071
+ }
1072
+ return true;
1073
+ }
1074
+ if ("pendingDispatchBaselineReset" in target && target.pendingDispatchBaselineReset) {
1075
+ return true;
1076
+ }
1077
+ const classification = await resolveProvider(target.driver)
1078
+ .live.classifyDispatchDelivery(target.sessionFilePath, dispatch.agents[agent].baseline);
1079
+ if (!this.isCurrentDispatch(generationId, dispatch.token))
1080
+ return false;
1081
+ if (!this.isCurrentDispatchSide(agent, generationId, dispatch.token, expectedStatus))
1082
+ return true;
1083
+ if (classification === "present") {
1084
+ await this.markDispatchDelivered(agent, generationId, dispatch.token, expectedStatus);
1085
+ if (this.isCurrentDispatchSide(agent, generationId, dispatch.token, "delivered"))
1086
+ this.reconciliationBlocked.delete(agent);
1087
+ }
1088
+ else if (classification === "absent") {
1089
+ await this.markDispatchPending(agent, generationId, dispatch.token, expectedStatus);
1090
+ if (this.isCurrentDispatchSide(agent, generationId, dispatch.token, "pending")) {
1091
+ this.reconciliationBlocked.delete(agent);
1092
+ if (onAbsent) {
1093
+ await onAbsent();
1094
+ }
1095
+ else {
1096
+ this.resendAfterLaunch.add(agent);
1097
+ }
1098
+ }
1099
+ }
1100
+ else {
1101
+ this.reconciliationBlocked.add(agent);
1102
+ this.options.output.warn(onAbsent
1103
+ ? `Cannot reconcile ${agentLabel(agent)} crosscheck; the prompt was not resent`
1104
+ : `Cannot reconcile ${agentLabel(agent)} crosscheck; session log is unreadable and the prompt will not be resent`);
1105
+ }
1106
+ return true;
1107
+ }
1108
+ async withDispatchLifecycle(lockedAgents, dispatch) {
1109
+ this.dispatching = true;
1110
+ this.refreshStatusHint();
1111
+ this.options.host.scheduleParentUiRender();
1112
+ const releaseInputLocks = lockedAgents.map((agent) => this.lockAutomatedInput(agent));
1113
+ try {
1114
+ await dispatch();
1115
+ }
1116
+ finally {
1117
+ for (const release of releaseInputLocks)
1118
+ release();
1119
+ this.dispatching = false;
1120
+ this.refreshStatusHint();
1121
+ this.options.host.scheduleParentUiRender();
1122
+ }
1123
+ }
1124
+ async finalizeDispatchIfComplete(generationId) {
1125
+ const state = this.store.snapshot();
1126
+ const dispatch = state.generation.dispatch;
1127
+ if (state.generation.id !== generationId || !dispatch)
1128
+ return;
1129
+ if (dispatch.agents["agent-1"].status !== "delivered"
1130
+ || dispatch.agents["agent-2"].status !== "delivered")
1131
+ return;
1132
+ await this.store.update((next) => {
1133
+ if (next.generation.id !== generationId)
1134
+ return;
1135
+ const current = next.generation.dispatch;
1136
+ if (!current || current.token !== dispatch.token)
1137
+ return;
1138
+ if (current.phase === "crosscheck") {
1139
+ next.generation.consumed = true;
1140
+ }
1141
+ else {
1142
+ next.generation.reconcileRound = current.round ?? next.generation.reconcileRound;
1143
+ }
1144
+ next.generation.reconcilePendingSettlement = true;
1145
+ delete next.generation.dispatch;
1146
+ });
1147
+ this.refreshStatusHint();
1148
+ }
1149
+ async maybeSettleReconcileGate() {
1150
+ const snapshot = this.store.snapshot();
1151
+ if (snapshot.generation.reconcilePendingSettlement
1152
+ && !snapshot.generation.dispatch
1153
+ && this.allAgentsIdle()) {
1154
+ await this.settleReconcileGate(snapshot.generation.id);
1155
+ }
1156
+ }
1157
+ async settleReconcileGate(generationId) {
1158
+ const generation = this.store.snapshot().generation;
1159
+ if (generation.id !== generationId
1160
+ || !generation.reconcilePendingSettlement
1161
+ || generation.dispatch)
1162
+ return;
1163
+ const round = generation.reconcileRound;
1164
+ const paths = ["agent-1", "agent-2"].map((agent) => round === 0
1165
+ ? crosscheckJsonFilePath(this.options.persistentDir, agent, generationId)
1166
+ : reconcileJsonFilePath(this.options.persistentDir, agent, generationId, round));
1167
+ const [agent1, agent2] = await Promise.all(paths.map((path) => readReviewResult(path)));
1168
+ if (this.store.snapshot().generation.id !== generationId)
1169
+ return;
1170
+ if (!agent1 || !agent2) {
1171
+ await this.store.update((state) => {
1172
+ if (state.generation.id !== generationId
1173
+ || !state.generation.reconcilePendingSettlement
1174
+ || state.generation.dispatch)
1175
+ return;
1176
+ state.generation.reconcileJsonMissing = true;
1177
+ state.generation.reconcileAllowed = false;
1178
+ state.generation.reconcileSettled = false;
1179
+ state.generation.reconcilePendingSettlement = false;
1180
+ });
1181
+ this.options.output.warn("Crosscheck review JSON is missing or malformed; Ctrl+X Reconcile is unavailable.");
1182
+ this.refreshStatusHint();
1183
+ this.options.host.scheduleParentUiRender();
1184
+ return;
1185
+ }
1186
+ const reconcileAllowed = round === 0
1187
+ ? true
1188
+ : agent1.rejected.length > 0 || agent2.rejected.length > 0;
1189
+ await this.store.update((state) => {
1190
+ if (state.generation.id !== generationId
1191
+ || !state.generation.reconcilePendingSettlement
1192
+ || state.generation.dispatch)
1193
+ return;
1194
+ state.generation.reconcileJsonMissing = false;
1195
+ state.generation.reconcileAllowed = reconcileAllowed;
1196
+ state.generation.reconcileSettled = true;
1197
+ state.generation.reconcilePendingSettlement = false;
1198
+ });
1199
+ this.refreshStatusHint();
1200
+ this.options.host.scheduleParentUiRender();
1201
+ }
1202
+ markDispatchDelivered(agent, generationId, dispatchToken, expectedStatus) {
1203
+ return this.transitionDispatchStatus(agent, generationId, dispatchToken, expectedStatus, "delivered");
1204
+ }
1205
+ markDispatchPending(agent, generationId, dispatchToken, expectedStatus) {
1206
+ return this.transitionDispatchStatus(agent, generationId, dispatchToken, expectedStatus, "pending");
1207
+ }
1208
+ transitionDispatchStatus(agent, generationId, dispatchToken, expectedStatus, nextStatus) {
1209
+ return this.store.update((state) => {
1210
+ if (state.generation.id === generationId
1211
+ && state.generation.dispatch?.token === dispatchToken
1212
+ && state.generation.dispatch.agents[agent].status === expectedStatus) {
1213
+ state.generation.dispatch.agents[agent].status = nextStatus;
1214
+ }
1215
+ });
1216
+ }
1217
+ isCurrentDispatch(generationId, dispatchToken) {
1218
+ const generation = this.store.snapshot().generation;
1219
+ return generation.id === generationId
1220
+ && generation.dispatch?.token === dispatchToken;
1221
+ }
1222
+ isCurrentDispatchSide(agent, generationId, dispatchToken, status) {
1223
+ const generation = this.store.snapshot().generation;
1224
+ return generation.id === generationId
1225
+ && generation.dispatch?.token === dispatchToken
1226
+ && generation.dispatch.agents[agent].status === status;
1227
+ }
1228
+ /** Sends the committed Broadcast text and, for Steer/Follow-up, starts the next generation first. */
1229
+ async submitBroadcast() {
1230
+ const text = this.broadcast.submissionText();
1231
+ if (text === undefined)
1232
+ return;
1233
+ const mode = this.broadcast.mode;
1234
+ if ((mode === "hint" || mode === "chat")
1235
+ && ["agent-1", "agent-2"].some((agent) => this.hasPendingCrosscheck(agent)))
1236
+ return;
1237
+ const submission = this.broadcast.beginSubmission();
1238
+ const agents = ["agent-1", "agent-2"];
1239
+ let initiated = false;
1240
+ this.broadcastSendActive = true;
1241
+ this.refreshStatusHint();
1242
+ this.options.host.scheduleParentUiRender();
1243
+ try {
1244
+ await this.withDispatchLifecycle(agents, async () => {
1245
+ for (const agent of agents) {
1246
+ if (!this.agents.has(agent)) {
1247
+ throw new Error(`${agentLabel(agent)} is not ready for Broadcast`);
1248
+ }
1249
+ }
1250
+ if (mode === "steer" || mode === "follow-up") {
1251
+ const deliverySubmitCounts = await this.captureGenerationDeliveryCounts();
1252
+ await this.store.startGeneration(mode, text, deliverySubmitCounts);
1253
+ this.crosscheckProgress = undefined;
1254
+ for (const runtime of this.agents.values()) {
1255
+ runtime.resultReady = false;
1256
+ runtime.freshForPendingDispatch = false;
1257
+ }
1258
+ this.resendAfterLaunch.clear();
1259
+ this.reconciliationBlocked.clear();
1260
+ this.broadcast.resetModeForFreshGeneration();
1261
+ this.refreshStatusHint();
1262
+ }
1263
+ const generationId = this.store.snapshot().generation.id;
1264
+ const generationKind = mode === "steer" || mode === "follow-up"
1265
+ ? mode
1266
+ : undefined;
1267
+ const sends = agents.map((agent) => this.sendBroadcastAgent(agent, text, generationId, generationKind));
1268
+ initiated = true;
1269
+ this.broadcast.completeSubmission(submission);
1270
+ this.resizeSessions();
1271
+ this.options.host.scheduleParentUiRender();
1272
+ const results = await Promise.allSettled(sends);
1273
+ for (const [index, result] of results.entries()) {
1274
+ if (result.status === "rejected") {
1275
+ this.warnDispatchFailure(agents[index], "broadcast send", result.reason);
1276
+ }
1277
+ }
1278
+ });
1279
+ }
1280
+ finally {
1281
+ this.broadcastSendActive = false;
1282
+ this.refreshStatusHint();
1283
+ this.options.host.scheduleParentUiRender();
1284
+ if (!initiated)
1285
+ this.broadcast.cancelSubmission(submission);
1286
+ }
1287
+ }
1288
+ async captureGenerationDeliveryCounts() {
1289
+ const runtimes = ["agent-1", "agent-2"].map((agent) => {
1290
+ const runtime = this.agents.get(agent);
1291
+ if (!runtime)
1292
+ throw new Error(`${agentLabel(agent)} is not ready for Broadcast`);
1293
+ return [agent, runtime];
1294
+ });
1295
+ await Promise.all(runtimes.map(([, runtime]) => runtime.monitor.waitForPendingTurnAttempts()));
1296
+ const entries = await Promise.all(runtimes.map(async ([agent, runtime]) => {
1297
+ const lines = await resolveProvider(runtime.config.driver).live.readLines(runtime.session.identity.sessionFilePath);
1298
+ return [agent, runtime.monitor.expectedSubmitCount(lines)];
1299
+ }));
1300
+ return Object.fromEntries(entries);
1301
+ }
1302
+ async sendBroadcastAgent(agent, text, generationId, generationKind) {
1303
+ const runtime = this.agents.get(agent);
1304
+ if (!runtime)
1305
+ throw new Error(`${agentLabel(agent)} is not ready for Broadcast`);
1306
+ const generation = this.store.snapshot().generation;
1307
+ if (generation.id !== generationId)
1308
+ return;
1309
+ const prompt = generationKind === "steer"
1310
+ ? buildExecutorSteerPrompt(text, generation.resultFiles[agent])
1311
+ : generationKind === "follow-up"
1312
+ ? buildExecutorFollowUpPrompt(text, generation.resultFiles[agent])
1313
+ : text;
1314
+ const provider = resolveProvider(runtime.config.driver);
1315
+ const releaseInputLock = this.lockAutomatedInput(agent);
1316
+ try {
1317
+ if (generationKind && provider.cancelSequence) {
1318
+ await sendCancelSequence(runtime.session.pty, provider.cancelSequence, this.dispatchAbort.signal);
1319
+ }
1320
+ await (this.options.sendInquiry ?? sendInquiryQuestion)(runtime.session, prompt, {
1321
+ signal: this.dispatchAbort.signal,
1322
+ composer: provider.composer,
1323
+ onSubmitted: () => runtime.monitor.beginTurn(),
1324
+ });
1325
+ }
1326
+ finally {
1327
+ releaseInputLock();
1328
+ }
1329
+ }
1330
+ refreshResults() {
1331
+ if (!this.resultRefresh) {
1332
+ const refresh = this.performResultRefresh().finally(() => {
1333
+ if (this.resultRefresh === refresh)
1334
+ this.resultRefresh = undefined;
1335
+ });
1336
+ this.resultRefresh = refresh;
1337
+ }
1338
+ return this.resultRefresh;
1339
+ }
1340
+ async performResultRefresh() {
1341
+ let changed = false;
1342
+ const generation = this.store.snapshot().generation;
1343
+ const readiness = await Promise.all(["agent-1", "agent-2"].map(async (agent) => ({
1344
+ agent,
1345
+ ready: await fileHasContent(generation.resultFiles[agent]),
1346
+ })));
1347
+ // Guard against a late read from a generation the coordinator has since superseded.
1348
+ if (this.store.snapshot().generation.id !== generation.id)
1349
+ return;
1350
+ for (const { agent, ready } of readiness) {
1351
+ const runtime = this.agents.get(agent);
1352
+ if (runtime && ready !== runtime.resultReady) {
1353
+ runtime.resultReady = ready;
1354
+ changed = true;
1355
+ }
1356
+ }
1357
+ if (changed) {
1358
+ this.broadcast.reconcileMode(this.currentReadiness());
1359
+ this.refreshStatusHint();
1360
+ this.options.host.scheduleParentUiRender();
1361
+ this.maybeRetryPersistedDispatch();
1362
+ }
1363
+ }
1364
+ refreshStatusHint() {
1365
+ // The crosscheck focus hint now renders on the Broadcast mode row.
1366
+ }
1367
+ focusSwitchHint() {
1368
+ const nextTarget = this.nextFocusTarget();
1369
+ const nextLabel = nextTarget === "broadcast" ? "Broadcast" : agentLabel(nextTarget);
1370
+ return `Ctrl+] Focus ${nextLabel}`;
1371
+ }
1372
+ currentTransientFlash() {
1373
+ if (!this.transientFlash)
1374
+ return undefined;
1375
+ return {
1376
+ message: this.transientFlash.message,
1377
+ variant: this.transientFlash.variant,
1378
+ };
1379
+ }
1380
+ forwardEvents(agent, events) {
1381
+ const runtime = this.agents.get(agent);
1382
+ const session = runtime?.session ?? this.launchingSessions.get(agent);
1383
+ if (!session)
1384
+ return;
1385
+ const useMouse = session.vt.isAlternate || session.hasMouseReporting();
1386
+ const bytes = encodeKeysToBytes(events, {
1387
+ ...(useMouse && { wheelMode: "sgr" }),
1388
+ childKittyFlags: session.childKittyFlags,
1389
+ childBracketedPaste: session.vt.bracketedPaste,
1390
+ });
1391
+ if (bytes.length > 0)
1392
+ session.pty.write(bytes);
1393
+ }
1394
+ lockAutomatedInput(agent) {
1395
+ const depth = (this.automatedInputDepth.get(agent) ?? 0) + 1;
1396
+ this.automatedInputDepth.set(agent, depth);
1397
+ this.automatedInputOwners.add(agent);
1398
+ let released = false;
1399
+ return () => {
1400
+ if (released)
1401
+ return;
1402
+ released = true;
1403
+ const next = (this.automatedInputDepth.get(agent) ?? 1) - 1;
1404
+ if (next <= 0) {
1405
+ this.automatedInputDepth.delete(agent);
1406
+ this.automatedInputOwners.delete(agent);
1407
+ }
1408
+ else {
1409
+ this.automatedInputDepth.set(agent, next);
1410
+ }
1411
+ };
1412
+ }
1413
+ maybeRetryPersistedDispatch() {
1414
+ if (this.finishing
1415
+ || this.dispatching
1416
+ || !this.sessionsReady
1417
+ || this.resendAfterLaunch.size === 0
1418
+ || !this.allResultsReady()
1419
+ || [...this.agents.values()].some((runtime) => runtime.freshForPendingDispatch)
1420
+ || !this.allAgentsIdle())
1421
+ return;
1422
+ this.resendAfterLaunch.clear();
1423
+ this.runDispatchOperation(undefined, this.retryPersistedDispatch(), "resume crosscheck");
1424
+ }
1425
+ async handleAgentIdle(agent, progressIdentity = this.takeCrosscheckTurnIdentity(agent)) {
1426
+ this.settleCrosscheckSlot(agent, progressIdentity);
1427
+ const runtime = this.agents.get(agent);
1428
+ if (runtime?.freshForPendingDispatch
1429
+ && !this.freshDispatchPreparations.has(agent)) {
1430
+ this.freshDispatchPreparations.add(agent);
1431
+ const snapshot = this.store.snapshot();
1432
+ const generationId = snapshot.generation.id;
1433
+ const dispatch = snapshot.generation.dispatch;
1434
+ try {
1435
+ if (dispatch?.agents[agent].status === "pending") {
1436
+ await this.store.update((state) => {
1437
+ if (state.generation.id === generationId
1438
+ && state.generation.dispatch?.token === dispatch.token
1439
+ && state.generation.dispatch.agents[agent].status === "pending") {
1440
+ state.generation.dispatch.agents[agent].baseline = runtime.monitor.lineBaseline;
1441
+ if (state.sessions[agent]) {
1442
+ delete state.sessions[agent].pendingDispatchBaselineReset;
1443
+ }
1444
+ }
1445
+ });
1446
+ if (this.isCurrentDispatch(generationId, dispatch.token)) {
1447
+ this.reconciliationBlocked.delete(agent);
1448
+ this.resendAfterLaunch.add(agent);
1449
+ }
1450
+ }
1451
+ runtime.freshForPendingDispatch = false;
1452
+ }
1453
+ finally {
1454
+ this.freshDispatchPreparations.delete(agent);
1455
+ }
1456
+ }
1457
+ const snapshot = this.store.snapshot();
1458
+ const dispatch = snapshot.generation.dispatch;
1459
+ if (runtime
1460
+ && dispatch?.agents[agent].status === "submitted"
1461
+ && !runtime.freshForPendingDispatch) {
1462
+ const classification = await resolveProvider(runtime.config.driver)
1463
+ .live.classifyDispatchDelivery(runtime.session.identity.sessionFilePath, dispatch.agents[agent].baseline);
1464
+ if (!this.isCurrentDispatch(snapshot.generation.id, dispatch.token)) {
1465
+ this.maybeRetryPersistedDispatch();
1466
+ return;
1467
+ }
1468
+ if (classification === "present") {
1469
+ await this.markDispatchDelivered(agent, snapshot.generation.id, dispatch.token, "submitted");
1470
+ if (this.isCurrentDispatchSide(agent, snapshot.generation.id, dispatch.token, "delivered"))
1471
+ this.reconciliationBlocked.delete(agent);
1472
+ await this.finalizeDispatchIfComplete(snapshot.generation.id);
1473
+ }
1474
+ else if (classification === "absent") {
1475
+ await this.markDispatchPending(agent, snapshot.generation.id, dispatch.token, "submitted");
1476
+ if (this.isCurrentDispatchSide(agent, snapshot.generation.id, dispatch.token, "pending")) {
1477
+ this.reconciliationBlocked.delete(agent);
1478
+ this.resendAfterLaunch.add(agent);
1479
+ }
1480
+ }
1481
+ else {
1482
+ this.reconciliationBlocked.add(agent);
1483
+ this.options.output.warn(`Cannot reconcile ${agentLabel(agent)} crosscheck; the prompt was not resent`);
1484
+ }
1485
+ }
1486
+ this.maybeRetryPersistedDispatch();
1487
+ await this.maybeSettleReconcileGate();
1488
+ }
1489
+ broadcastRowCount(cols, totalRows) {
1490
+ const raw = this.broadcast.rowCount(cols);
1491
+ return Math.max(1, Math.min(raw, Math.max(1, totalRows - MIN_AGENT_ROWS)));
1492
+ }
1493
+ paneDimensions(cols, totalRows) {
1494
+ const agentAreaRows = Math.max(1, totalRows - this.broadcastRowCount(cols, totalRows));
1495
+ const metrics = computeSplitMetrics({
1496
+ cols,
1497
+ rows: agentAreaRows,
1498
+ collapseAt: SPLIT_COLLAPSE_AT_COLS,
1499
+ });
1500
+ const vtRows = Math.max(1, agentAreaRows - HEADER_ROWS);
1501
+ if (metrics.collapsed) {
1502
+ return {
1503
+ "agent-1": { cols: metrics.leftWidth, rows: vtRows },
1504
+ "agent-2": { cols: metrics.leftWidth, rows: vtRows },
1505
+ };
1506
+ }
1507
+ return {
1508
+ "agent-1": { cols: metrics.leftWidth, rows: vtRows },
1509
+ "agent-2": { cols: metrics.rightWidth, rows: vtRows },
1510
+ };
1511
+ }
1512
+ resizeSessions() {
1513
+ const layout = this.options.host.getLayout();
1514
+ const dimensions = this.paneDimensions(layout.columns, layout.childRows);
1515
+ for (const agent of ["agent-1", "agent-2"]) {
1516
+ const runtime = this.agents.get(agent);
1517
+ const session = runtime?.session ?? this.launchingSessions.get(agent);
1518
+ if (session)
1519
+ session.resize(dimensions[agent].cols, dimensions[agent].rows);
1520
+ }
1521
+ }
1522
+ refreshMouseModes() {
1523
+ const modes = new Set([1000, 1006]);
1524
+ for (const runtime of this.agents.values()) {
1525
+ for (const mode of runtime.session.mouseModes())
1526
+ modes.add(mode);
1527
+ }
1528
+ for (const session of this.launchingSessions.values()) {
1529
+ for (const mode of session.mouseModes())
1530
+ modes.add(mode);
1531
+ }
1532
+ this.options.host.setMouseModeSource(MOUSE_SOURCE, modes);
1533
+ }
1534
+ finish() {
1535
+ if (this.finishing)
1536
+ return;
1537
+ this.finishing = true;
1538
+ this.launchAbort?.abort(new Error("Crosscheck launch cancelled"));
1539
+ this.dispatchAbort.abort(new Error("Crosscheck coordinator stopped"));
1540
+ this.finishResolve?.({ kind: "crosscheck", interrupted: true });
1541
+ }
1542
+ fail(error) {
1543
+ if (this.finishing)
1544
+ return;
1545
+ this.finishing = true;
1546
+ this.launchAbort?.abort(error);
1547
+ this.dispatchAbort.abort(error);
1548
+ if (this.finishReject)
1549
+ this.finishReject(error);
1550
+ else
1551
+ this.pendingFailure = error;
1552
+ }
1553
+ async dispose() {
1554
+ this.finishing = true;
1555
+ if (!this.dispatchAbort.signal.aborted) {
1556
+ this.dispatchAbort.abort(new Error("Crosscheck coordinator disposed"));
1557
+ }
1558
+ if (this.resultTimer)
1559
+ clearInterval(this.resultTimer);
1560
+ this.resultTimer = undefined;
1561
+ if (this.activityTimer)
1562
+ clearInterval(this.activityTimer);
1563
+ this.activityTimer = undefined;
1564
+ if (this.transientFlash) {
1565
+ clearTimeout(this.transientFlash.timer);
1566
+ this.transientFlash = undefined;
1567
+ }
1568
+ this.detachUi?.();
1569
+ this.detachUi = undefined;
1570
+ this.options.host.clearMouseModeSource(MOUSE_SOURCE);
1571
+ await Promise.all([...this.agents.values()].map((runtime) => runtime.monitor.shutdown()));
1572
+ await Promise.allSettled([...this.dispatchOperations]);
1573
+ this.trackActivity();
1574
+ await Promise.allSettled([...this.activityOperations]);
1575
+ for (const runtime of this.agents.values()) {
1576
+ runtime.session.close();
1577
+ }
1578
+ for (const session of this.launchingSessions.values())
1579
+ session.close();
1580
+ this.agents.clear();
1581
+ this.launchingSessions.clear();
1582
+ }
1583
+ runDispatchOperation(agent, operation, action) {
1584
+ const tracked = operation
1585
+ .catch((err) => {
1586
+ if (!this.finishing || !this.dispatchAbort.signal.aborted) {
1587
+ this.warnDispatchFailure(agent, action, err);
1588
+ }
1589
+ })
1590
+ .finally(() => {
1591
+ this.dispatchOperations.delete(tracked);
1592
+ });
1593
+ this.dispatchOperations.add(tracked);
1594
+ }
1595
+ startActivityHeartbeat() {
1596
+ if (!this.options.touchActivity)
1597
+ return;
1598
+ this.activityTimer = setInterval(() => {
1599
+ this.trackActivity();
1600
+ }, this.options.activityHeartbeatMs ?? ACTIVITY_HEARTBEAT_MS);
1601
+ }
1602
+ trackActivity() {
1603
+ if (!this.options.touchActivity)
1604
+ return;
1605
+ const operation = this.options.touchActivity()
1606
+ .catch((err) => {
1607
+ this.options.output.warn(`Failed to refresh crosscheck session activity: ${err instanceof Error ? err.message : String(err)}`);
1608
+ })
1609
+ .finally(() => {
1610
+ this.activityOperations.delete(operation);
1611
+ });
1612
+ this.activityOperations.add(operation);
1613
+ }
1614
+ warnDispatchFailure(agent, action, error) {
1615
+ const target = agent ? ` for ${agentLabel(agent)}` : "";
1616
+ const reason = error instanceof Error ? error.message : String(error);
1617
+ this.options.output.warn(`Crosscheck ${action}${target} failed: ${reason}`);
1618
+ }
1619
+ async dumpRecords() {
1620
+ const generation = this.store.snapshot().generation;
1621
+ const committedTask = buildExecutorEffectiveTaskContext(generation.effectiveTask ?? this.options.task, generation.steerHistory);
1622
+ return await Promise.all(["agent-1", "agent-2"].map(async (agent, index) => {
1623
+ const config = this.options.configurations[agent];
1624
+ const runtime = this.agents.get(agent);
1625
+ const resultFile = generation.resultFiles[agent];
1626
+ const outputs = {};
1627
+ try {
1628
+ outputs.result = {
1629
+ artifactName: `${agent}-result`,
1630
+ file: resultFile,
1631
+ bytes: await readFile(resultFile),
1632
+ };
1633
+ }
1634
+ catch {
1635
+ // Result may not exist while the executor is still working.
1636
+ }
1637
+ return {
1638
+ seq: index,
1639
+ stepId: agent,
1640
+ role: "executor",
1641
+ driver: config.driver,
1642
+ phaseLabel: "Crosschecking...",
1643
+ inputs: {
1644
+ task: { artifactName: TASK, text: committedTask },
1645
+ },
1646
+ outputs,
1647
+ sessionFilePaths: runtime ? [runtime.session.identity.sessionFilePath] : [],
1648
+ inFlight: true,
1649
+ ...(runtime && { ptyTailSnapshot: runtime.session.ptyTail.snapshot() }),
1650
+ };
1651
+ }));
1652
+ }
1653
+ }
1654
+ async function fileHasContent(path) {
1655
+ try {
1656
+ const file = await open(path, "r");
1657
+ try {
1658
+ const info = await file.stat();
1659
+ if (!info.isFile() || info.size <= 0)
1660
+ return false;
1661
+ const probe = Buffer.allocUnsafe(1);
1662
+ const { bytesRead } = await file.read(probe, 0, probe.length, 0);
1663
+ return bytesRead > 0;
1664
+ }
1665
+ finally {
1666
+ await file.close();
1667
+ }
1668
+ }
1669
+ catch {
1670
+ return false;
1671
+ }
1672
+ }
1673
+ export function buildCrosscheckConfigurations(values) {
1674
+ return {
1675
+ "agent-1": {
1676
+ driver: values.agent1,
1677
+ model: values.agent1Model,
1678
+ effort: values.agent1Effort,
1679
+ },
1680
+ "agent-2": {
1681
+ driver: values.agent2,
1682
+ model: values.agent2Model,
1683
+ effort: values.agent2Effort,
1684
+ },
1685
+ };
1686
+ }
1687
+ function otherAgent(agent) {
1688
+ return agent === "agent-1" ? "agent-2" : "agent-1";
1689
+ }
1690
+ function agentLabel(agent) {
1691
+ return agent === "agent-1" ? "Agent 1" : "Agent 2";
1692
+ }
1693
+ // Re-exported for callers that only need mode availability without constructing a composer.
1694
+ export { availableBroadcastModes };
1695
+ //# sourceMappingURL=coordinator.js.map