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.
- package/dist/agents/executors/dsl.d.ts +11 -0
- package/dist/agents/executors/dsl.d.ts.map +1 -0
- package/dist/agents/executors/dsl.js +11 -0
- package/dist/agents/executors/dsl.js.map +1 -0
- package/dist/agents/executors/index.d.ts +8 -0
- package/dist/agents/executors/index.d.ts.map +1 -0
- package/dist/agents/executors/index.js +17 -0
- package/dist/agents/executors/index.js.map +1 -0
- package/dist/agents/executors/prompts.d.ts +18 -0
- package/dist/agents/executors/prompts.d.ts.map +1 -0
- package/dist/agents/executors/prompts.js +62 -0
- package/dist/agents/executors/prompts.js.map +1 -0
- package/dist/agents/executors/types.d.ts +8 -0
- package/dist/agents/executors/types.d.ts.map +1 -0
- package/dist/agents/executors/types.js +2 -0
- package/dist/agents/executors/types.js.map +1 -0
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +4 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/registry.d.ts +1 -1
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +3 -0
- package/dist/agents/registry.js.map +1 -1
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +20 -45
- package/dist/agents/runner.js.map +1 -1
- package/dist/ansi.d.ts +4 -0
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +4 -0
- package/dist/ansi.js.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/crosscheck/broadcast-composer.d.ts +89 -0
- package/dist/crosscheck/broadcast-composer.d.ts.map +1 -0
- package/dist/crosscheck/broadcast-composer.js +431 -0
- package/dist/crosscheck/broadcast-composer.js.map +1 -0
- package/dist/crosscheck/coordinator.d.ts +154 -0
- package/dist/crosscheck/coordinator.d.ts.map +1 -0
- package/dist/crosscheck/coordinator.js +1695 -0
- package/dist/crosscheck/coordinator.js.map +1 -0
- package/dist/crosscheck/key-helpers.d.ts +16 -0
- package/dist/crosscheck/key-helpers.d.ts.map +1 -0
- package/dist/crosscheck/key-helpers.js +42 -0
- package/dist/crosscheck/key-helpers.js.map +1 -0
- package/dist/crosscheck/live-agent-session.d.ts +38 -0
- package/dist/crosscheck/live-agent-session.d.ts.map +1 -0
- package/dist/crosscheck/live-agent-session.js +126 -0
- package/dist/crosscheck/live-agent-session.js.map +1 -0
- package/dist/crosscheck/reconcile.d.ts +10 -0
- package/dist/crosscheck/reconcile.d.ts.map +1 -0
- package/dist/crosscheck/reconcile.js +29 -0
- package/dist/crosscheck/reconcile.js.map +1 -0
- package/dist/crosscheck/state.d.ts +102 -0
- package/dist/crosscheck/state.d.ts.map +1 -0
- package/dist/crosscheck/state.js +221 -0
- package/dist/crosscheck/state.js.map +1 -0
- package/dist/dsl.d.ts +2 -0
- package/dist/dsl.d.ts.map +1 -1
- package/dist/dsl.js +2 -1
- package/dist/dsl.js.map +1 -1
- package/dist/dump/service.d.ts +6 -3
- package/dist/dump/service.d.ts.map +1 -1
- package/dist/dump/service.js +32 -12
- package/dist/dump/service.js.map +1 -1
- package/dist/flow/built-in/crosscheck-factory.d.ts +15 -0
- package/dist/flow/built-in/crosscheck-factory.d.ts.map +1 -0
- package/dist/flow/built-in/crosscheck-factory.js +60 -0
- package/dist/flow/built-in/crosscheck-factory.js.map +1 -0
- package/dist/flow/built-in/crosscheck.d.ts +6 -0
- package/dist/flow/built-in/crosscheck.d.ts.map +1 -0
- package/dist/flow/built-in/crosscheck.js +6 -0
- package/dist/flow/built-in/crosscheck.js.map +1 -0
- package/dist/flow/built-in/default-factory.d.ts.map +1 -1
- package/dist/flow/built-in/default-factory.js +1 -0
- package/dist/flow/built-in/default-factory.js.map +1 -1
- package/dist/flow/built-in/simplify-factory.d.ts.map +1 -1
- package/dist/flow/built-in/simplify-factory.js +1 -0
- package/dist/flow/built-in/simplify-factory.js.map +1 -1
- package/dist/flow/custom/load.d.ts.map +1 -1
- package/dist/flow/custom/load.js +4 -0
- package/dist/flow/custom/load.js.map +1 -1
- package/dist/flow/dsl.d.ts +1 -0
- package/dist/flow/dsl.d.ts.map +1 -1
- package/dist/flow/dsl.js +1 -0
- package/dist/flow/dsl.js.map +1 -1
- package/dist/flow/index.d.ts +2 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +2 -1
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/params.d.ts +7 -1
- package/dist/flow/params.d.ts.map +1 -1
- package/dist/flow/params.js +28 -13
- package/dist/flow/params.js.map +1 -1
- package/dist/flow/types.d.ts +8 -1
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/flow/types.js.map +1 -1
- package/dist/offscreen/controller.d.ts +9 -3
- package/dist/offscreen/controller.d.ts.map +1 -1
- package/dist/offscreen/controller.js +149 -75
- package/dist/offscreen/controller.js.map +1 -1
- package/dist/offscreen/fork-busy.d.ts +2 -1
- package/dist/offscreen/fork-busy.d.ts.map +1 -1
- package/dist/offscreen/fork-busy.js +5 -2
- package/dist/offscreen/fork-busy.js.map +1 -1
- package/dist/offscreen/fork.d.ts +1 -1
- package/dist/offscreen/fork.d.ts.map +1 -1
- package/dist/offscreen/fork.js +4 -2
- package/dist/offscreen/fork.js.map +1 -1
- package/dist/offscreen/inquiry-sender.d.ts +4 -9
- package/dist/offscreen/inquiry-sender.d.ts.map +1 -1
- package/dist/offscreen/inquiry-sender.js +5 -54
- package/dist/offscreen/inquiry-sender.js.map +1 -1
- package/dist/offscreen/session-turn-monitor.d.ts +66 -0
- package/dist/offscreen/session-turn-monitor.d.ts.map +1 -0
- package/dist/offscreen/session-turn-monitor.js +257 -0
- package/dist/offscreen/session-turn-monitor.js.map +1 -0
- package/dist/orchestrator.d.ts +7 -6
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +122 -61
- package/dist/orchestrator.js.map +1 -1
- package/dist/preview-panel.d.ts +1 -0
- package/dist/preview-panel.d.ts.map +1 -1
- package/dist/preview-panel.js +24 -20
- package/dist/preview-panel.js.map +1 -1
- package/dist/providers/claude/completion.d.ts +1 -0
- package/dist/providers/claude/completion.d.ts.map +1 -1
- package/dist/providers/claude/completion.js +5 -0
- package/dist/providers/claude/completion.js.map +1 -1
- package/dist/providers/claude/composer.d.ts +6 -0
- package/dist/providers/claude/composer.d.ts.map +1 -0
- package/dist/providers/claude/composer.js +7 -0
- package/dist/providers/claude/composer.js.map +1 -0
- package/dist/providers/claude/session.d.ts +1 -0
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +21 -1
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/claude/strategy.d.ts +6 -1
- package/dist/providers/claude/strategy.d.ts.map +1 -1
- package/dist/providers/claude/strategy.js +40 -11
- package/dist/providers/claude/strategy.js.map +1 -1
- package/dist/providers/codex/composer.d.ts +6 -0
- package/dist/providers/codex/composer.d.ts.map +1 -0
- package/dist/providers/codex/composer.js +143 -0
- package/dist/providers/codex/composer.js.map +1 -0
- package/dist/providers/codex/plan-mode-activator.d.ts +4 -6
- package/dist/providers/codex/plan-mode-activator.d.ts.map +1 -1
- package/dist/providers/codex/plan-mode-activator.js +16 -21
- package/dist/providers/codex/plan-mode-activator.js.map +1 -1
- package/dist/providers/codex/strategy.d.ts +5 -1
- package/dist/providers/codex/strategy.d.ts.map +1 -1
- package/dist/providers/codex/strategy.js +8 -3
- package/dist/providers/codex/strategy.js.map +1 -1
- package/dist/providers/composer-utils.d.ts +9 -0
- package/dist/providers/composer-utils.d.ts.map +1 -0
- package/dist/providers/composer-utils.js +218 -0
- package/dist/providers/composer-utils.js.map +1 -0
- package/dist/providers/copilot/completion.d.ts +6 -2
- package/dist/providers/copilot/completion.d.ts.map +1 -1
- package/dist/providers/copilot/completion.js +56 -11
- package/dist/providers/copilot/completion.js.map +1 -1
- package/dist/providers/copilot/composer.d.ts +6 -0
- package/dist/providers/copilot/composer.d.ts.map +1 -0
- package/dist/providers/copilot/composer.js +10 -0
- package/dist/providers/copilot/composer.js.map +1 -0
- package/dist/providers/copilot/strategy.d.ts +6 -1
- package/dist/providers/copilot/strategy.d.ts.map +1 -1
- package/dist/providers/copilot/strategy.js +69 -12
- package/dist/providers/copilot/strategy.js.map +1 -1
- package/dist/providers/fork.d.ts +3 -1
- package/dist/providers/fork.d.ts.map +1 -1
- package/dist/providers/fork.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/live.d.ts +17 -0
- package/dist/providers/live.d.ts.map +1 -0
- package/dist/providers/live.js +201 -0
- package/dist/providers/live.js.map +1 -0
- package/dist/providers/session-launch.d.ts +25 -0
- package/dist/providers/session-launch.d.ts.map +1 -0
- package/dist/providers/session-launch.js +54 -0
- package/dist/providers/session-launch.js.map +1 -0
- package/dist/providers/types.d.ts +75 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/resume-tui.d.ts.map +1 -1
- package/dist/resume-tui.js +3 -1
- package/dist/resume-tui.js.map +1 -1
- package/dist/session/list.d.ts.map +1 -1
- package/dist/session/list.js +5 -3
- package/dist/session/list.js.map +1 -1
- package/dist/session/locator.d.ts.map +1 -1
- package/dist/session/locator.js +6 -22
- package/dist/session/locator.js.map +1 -1
- package/dist/session/reader.d.ts +8 -0
- package/dist/session/reader.d.ts.map +1 -1
- package/dist/session/reader.js +12 -2
- package/dist/session/reader.js.map +1 -1
- package/dist/session/resume.d.ts.map +1 -1
- package/dist/session/resume.js +1 -0
- package/dist/session/resume.js.map +1 -1
- package/dist/session/session-store.d.ts +3 -0
- package/dist/session/session-store.d.ts.map +1 -1
- package/dist/session/session-store.js +18 -6
- package/dist/session/session-store.js.map +1 -1
- package/dist/startup-tui.js +2 -2
- package/dist/startup-tui.js.map +1 -1
- package/dist/status-bar.d.ts +3 -0
- package/dist/status-bar.d.ts.map +1 -1
- package/dist/status-bar.js.map +1 -1
- package/dist/terminal/host.d.ts +19 -3
- package/dist/terminal/host.d.ts.map +1 -1
- package/dist/terminal/host.js +45 -7
- package/dist/terminal/host.js.map +1 -1
- package/dist/terminal/kitty-negotiator.js +1 -1
- package/dist/terminal/kitty-negotiator.js.map +1 -1
- package/dist/terminal/paste-submit.d.ts +18 -27
- package/dist/terminal/paste-submit.d.ts.map +1 -1
- package/dist/terminal/paste-submit.js +81 -128
- package/dist/terminal/paste-submit.js.map +1 -1
- package/dist/terminal-input.d.ts.map +1 -1
- package/dist/terminal-input.js +3 -4
- package/dist/terminal-input.js.map +1 -1
- package/dist/util/abortable-sleep.d.ts +6 -0
- package/dist/util/abortable-sleep.d.ts.map +1 -0
- package/dist/util/abortable-sleep.js +40 -0
- package/dist/util/abortable-sleep.js.map +1 -0
- 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
|