clay-server 4.3.0-beta.4 → 4.3.0-beta.5

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.
@@ -0,0 +1,225 @@
1
+ var crypto = require("crypto");
2
+
3
+ function attachPairResultRecovery(ctx) {
4
+ var scheduled = Object.create(null);
5
+ var runtimeStatus = Object.create(null);
6
+
7
+ function opaqueId(key) {
8
+ return crypto.createHash("sha256").update(String(key)).digest("hex").slice(0, 24);
9
+ }
10
+ function ownerId(session) { return session && session.ownerId || null; }
11
+ function findOrigin(originId) {
12
+ var found = null, duplicate = false;
13
+ ctx.sm.sessions.forEach(function (session) {
14
+ if (!session || session.sessionOriginId !== originId) return;
15
+ if (found) duplicate = true;
16
+ else found = session;
17
+ });
18
+ return duplicate ? null : found;
19
+ }
20
+ function resolve(record) {
21
+ if (!record || record.projectSlug !== ctx.projectSlug) return { ok: false, code: "project", reason: "This completed result belongs to a different project." };
22
+ var driver = findOrigin(record.driverOriginId), worker = findOrigin(record.workerOriginId);
23
+ if (!driver || !worker) return { ok: false, code: "session", reason: "The original Driver or Split Worker session is unavailable." };
24
+ if (ownerId(driver) !== (record.ownerId || null) || ownerId(worker) !== (record.ownerId || null)) return { ok: false, code: "owner", reason: "The original session owner no longer matches." };
25
+ var group = ctx.store.groupForMember(driver.localId);
26
+ if (!group || ctx.store.groupForMember(worker.localId) !== group || !group.pair ||
27
+ group.pair.driverId !== driver.localId || group.pair.workerId !== worker.localId) {
28
+ return { ok: false, code: "pair", reason: "The original Driver and Split Worker pair is no longer active." };
29
+ }
30
+ var provenance = worker.sessionProvenance;
31
+ if (!provenance || provenance.kind !== "worker" || provenance.parentSessionOriginId !== record.driverOriginId || provenance.generation !== record.generation) {
32
+ return { ok: false, code: "generation", reason: "The Split Worker generation no longer matches this completed result." };
33
+ }
34
+ if (worker._pairDelegation && (worker._pairDelegation.outboxKey !== record.key || worker._pairDelegation.taskId !== record.taskId)) {
35
+ return { ok: false, code: "active_task", reason: "A newer Split Worker task is active, so this result cannot be restored automatically." };
36
+ }
37
+ return { ok: true, driver: driver, worker: worker, group: group };
38
+ }
39
+ function tokenFor(record, resolved) {
40
+ var token = resolved.worker._pairDelegation;
41
+ if (token && token.outboxKey === record.key) {
42
+ if (Number(record.manualRetryEpoch || 0) > Number(token._manualRetryEpochApplied || 0)) {
43
+ if (token._deliveryTimer && token._deliveryTimer.timer) clearTimeout(token._deliveryTimer.timer);
44
+ delete token._deliveryTimer; delete token._deliveryUncertain; delete token._deliveryAccepted;
45
+ delete token._deliveryStartupAttemptId;
46
+ token._manualRetryEpochApplied = Number(record.manualRetryEpoch || 0);
47
+ }
48
+ return token;
49
+ }
50
+ token = {
51
+ outboxKey: record.key, taskId: record.taskId, generation: record.generation,
52
+ from: resolved.driver.localId, groupId: resolved.group.id,
53
+ startIndex: Number.isInteger(record.historyStartIndex) ? record.historyStartIndex : resolved.worker.history.length,
54
+ message: record.message || "", wait: record.deliveryRoute === "blocking",
55
+ detached: record.deliveryRoute === "callback", detachRequested: record.deliveryRoute === "callback",
56
+ response: record.outcome && record.outcome.response || "",
57
+ failure: record.outcome && record.outcome.error || null,
58
+ interrupted: !!(record.outcome && record.outcome.status === "interrupted"),
59
+ _captureComplete: true,
60
+ _manualRetryEpochApplied: Number(record.manualRetryEpoch || 0),
61
+ };
62
+ resolved.worker._pairDelegation = token;
63
+ resolved.worker._delegatedBy = resolved.driver.localId;
64
+ return token;
65
+ }
66
+ function block(record, reason, code) {
67
+ if (record.deliveryState === "accepted" && record.finalized) return;
68
+ var saved = ctx.outbox.markBlocked(record.key, reason, code);
69
+ if (saved.ok) changed();
70
+ }
71
+ function process(key, allowDelivery) {
72
+ delete scheduled[key];
73
+ var record = ctx.outbox.get(key);
74
+ if (!record || !record.outcome) return { ok: false, deferred: true };
75
+ if (record.finalized) return { ok: true, finalized: true };
76
+ if (record.blockedCode === "human_stop") return { ok: false, blocked: true };
77
+ var resolved = resolve(record);
78
+ if (!resolved.ok) {
79
+ block(record, resolved.reason, resolved.code);
80
+ return { ok: false, blocked: true };
81
+ }
82
+ var stopReason = typeof ctx.blockedReason === "function" ? ctx.blockedReason(resolved.driver) : null;
83
+ if (stopReason) {
84
+ block(record, stopReason, "human_stop");
85
+ return { ok: false, blocked: true };
86
+ }
87
+ if (record.finalizationState === "failed") return { ok: false, blocked: true };
88
+ var token = tokenFor(record, resolved);
89
+ if (record.deliveryRoute === "blocking" || record.deliveryState === "accepted") {
90
+ var finished = ctx.resultCapture.finish(resolved.driver, resolved.worker, token, record.deliveryState === "accepted");
91
+ changed(resolved.driver);
92
+ return { ok: finished !== false, finalized: finished !== false };
93
+ }
94
+ if (record.deliveryState === "uncertain") return { ok: false, uncertain: true };
95
+ if (!allowDelivery) return { ok: false, deferred: true };
96
+ var result = ctx.wake(resolved.driver, resolved.worker, token);
97
+ changed(resolved.driver);
98
+ return result;
99
+ }
100
+ function schedule(key) {
101
+ if (scheduled[key]) return;
102
+ scheduled[key] = true;
103
+ setImmediate(function () { process(key, true); });
104
+ }
105
+ function healthyLiveDispatch(record) {
106
+ if (!record || record.outcome || record.state === "blocked") return false;
107
+ var resolved = resolve(record);
108
+ if (!resolved.ok) return false;
109
+ var token = resolved.worker._pairDelegation;
110
+ return !!(token && token.outboxKey === record.key && token.taskId === record.taskId && token.generation === record.generation &&
111
+ token.from === resolved.driver.localId && token.groupId === resolved.group.id && !token._capturePending && !token._captureHookError &&
112
+ (resolved.worker.isProcessing || resolved.worker._queryStarting));
113
+ }
114
+ function recordsFor(session) {
115
+ if (!session) return [];
116
+ var originId = session.sessionOriginId, currentOwner = ownerId(session);
117
+ return ctx.outbox.list().map(function (record) {
118
+ return runtimeStatus[record.key] ? Object.assign({}, record, runtimeStatus[record.key]) : record;
119
+ }).filter(function (record) {
120
+ if (record.projectSlug !== ctx.projectSlug || record.driverOriginId !== originId || (record.ownerId || null) !== currentOwner) return false;
121
+ if (healthyLiveDispatch(record)) return false;
122
+ if (record.finalized) return false;
123
+ if (record.deliveryState === "accepted") return record.state === "blocked" || record.finalizationState === "failed";
124
+ return true;
125
+ });
126
+ }
127
+ function publicItem(record) {
128
+ var completed = !!record.outcome;
129
+ var finalizationFailed = record.finalizationState === "failed";
130
+ var state = record.state === "blocked" || finalizationFailed ? "blocked" : record.deliveryState;
131
+ if (state === "attempting") state = "uncertain";
132
+ if (!completed && state !== "blocked") state = "uncertain";
133
+ var exhausted = state === "pending" && Number(record.deliveryAttemptCount || 0) >= ctx.outbox.maxDeliveryAttempts;
134
+ return {
135
+ id: opaqueId(record.key), state: exhausted ? "pending" : state,
136
+ message: record.blockedReason || record.finalizationError || (!completed ? "The Split Worker task was retained, but no durable completed result is available yet." :
137
+ (state === "uncertain" ? "Delivery could not be confirmed. The Driver may already have received this result." :
138
+ (exhausted ? "Automatic delivery could not complete. The finished result is retained." : "The finished Split Worker result is waiting to be delivered."))),
139
+ response: completed ? record.outcome.response || "" : "", error: completed ? record.outcome.error || null : null,
140
+ hasCompletedResult: completed,
141
+ canRetry: completed && !finalizationFailed && record.blockedCode !== "human_stop" && Number(record.manualRetryCount || 0) < 1 && (state === "uncertain" || exhausted),
142
+ confirmDuplicate: completed && state === "uncertain",
143
+ };
144
+ }
145
+ function canRead(ws, session) {
146
+ if (!ws || !session || ws._clayActiveSession !== session.localId) return false;
147
+ if (ownerId(session)) return !!(ws._clayUser && String(ws._clayUser.id) === String(ownerId(session)));
148
+ return !ws._clayUser || !ctx.isMultiUser();
149
+ }
150
+ function sendState(session, ws, sendFn) {
151
+ if (!canRead(ws, session)) return false;
152
+ var items = recordsFor(session).map(publicItem);
153
+ (sendFn || ctx.sendTo.bind(null, ws))({ type: "pair_result_status", sessionId: session.localId, items: items });
154
+ return true;
155
+ }
156
+ function changed(driver) {
157
+ var targets = typeof ctx.getClients === "function" ? ctx.getClients() : [];
158
+ for (var ws of targets) {
159
+ var session = driver || ctx.sm.sessions.get(ws._clayActiveSession);
160
+ if (session) sendState(session, ws);
161
+ }
162
+ }
163
+ function stateChanged(driver, record) {
164
+ if (record && record.key && record.persistenceError) runtimeStatus[record.key] = {
165
+ finalizationState: record.finalizationState, finalizationError: record.finalizationError,
166
+ };
167
+ if (record && record.key && record.finalized) delete runtimeStatus[record.key];
168
+ changed(driver);
169
+ if (record && !record.finalized && record.finalizationState !== "failed" && (record.deliveryState === "accepted" || record.deliveryRoute === "blocking")) schedule(record.key);
170
+ }
171
+ function available(session) {
172
+ if (!session) return;
173
+ var origin = session.sessionOriginId;
174
+ var records = ctx.outbox.list();
175
+ for (var i = 0; i < records.length; i++) {
176
+ if (records[i].driverOriginId === origin && records[i].outcome && records[i].deliveryState === "pending" && records[i].state !== "blocked") schedule(records[i].key);
177
+ }
178
+ }
179
+ function reconcile() {
180
+ var records = ctx.outbox.list();
181
+ for (var i = 0; i < records.length; i++) {
182
+ var record = records[i];
183
+ if (!record.outcome) continue;
184
+ if (record.deliveryState === "attempting") {
185
+ var uncertain = ctx.outbox.markRestartUncertain(record.key);
186
+ if (uncertain.ok && uncertain.record) record = uncertain.record;
187
+ }
188
+ process(record.key, false);
189
+ record = ctx.outbox.get(record.key);
190
+ if (record && record.deliveryState === "pending" && record.state !== "blocked" && record.finalizationState !== "failed") schedule(record.key);
191
+ }
192
+ }
193
+ function beginHumanTurn(session) {
194
+ var records = recordsFor(session);
195
+ for (var i = 0; i < records.length; i++) {
196
+ if (records[i].blockedCode !== "human_stop") continue;
197
+ var saved = ctx.outbox.authorizeHumanTurn(records[i].key);
198
+ if (saved.ok && saved.record.outcome && (saved.record.deliveryState === "pending" || saved.record.deliveryState === "accepted")) schedule(records[i].key);
199
+ }
200
+ changed(session);
201
+ }
202
+ function handleMessage(ws, msg) {
203
+ if (!msg || (msg.type !== "pair_result_state_request" && msg.type !== "pair_result_retry")) return false;
204
+ var session = ctx.sm.sessions.get(ws._clayActiveSession);
205
+ if (!canRead(ws, session)) return true;
206
+ if (msg.sessionId != null && msg.sessionId !== session.localId) return true;
207
+ if (msg.projectSlug && msg.projectSlug !== ctx.projectSlug) return true;
208
+ if (msg.type === "pair_result_state_request") { sendState(session, ws); return true; }
209
+ var records = recordsFor(session), record = null;
210
+ for (var i = 0; i < records.length; i++) if (opaqueId(records[i].key) === msg.id) record = records[i];
211
+ if (!record) { sendState(session, ws); return true; }
212
+ if (record.deliveryState === "uncertain" && msg.confirmDuplicate !== true) { sendState(session, ws); return true; }
213
+ var granted = ctx.outbox.grantManualRetry(record.key);
214
+ if (granted.ok) schedule(record.key);
215
+ changed(session);
216
+ return true;
217
+ }
218
+ if (ctx.sm && typeof ctx.sm.addOnSessionViewed === "function") {
219
+ ctx.sm.addOnSessionViewed(function (session, ws, sendFn) { available(session); sendState(session, ws, sendFn); });
220
+ }
221
+ return { available: available, beginHumanTurn: beginHumanTurn, changed: changed, stateChanged: stateChanged, handleMessage: handleMessage,
222
+ reconcile: reconcile, sendState: sendState, process: process };
223
+ }
224
+
225
+ module.exports = { attachPairResultRecovery: attachPairResultRecovery };
@@ -228,6 +228,12 @@ function attachPairTaskControl(ctx) {
228
228
  token.outcomeEnvelope = envelope;
229
229
  worker._lastPairOutcome = envelope;
230
230
  if (finalStatus === "interrupted" || finalStatus === "partial") worker._lastInterruptedPairTask = { taskId: token.taskId, message: token.message, generation: token.generation };
231
+ if (ctx.sm.sendAndRecord) {
232
+ try { ctx.sm.sendAndRecord(worker, { type: "pair_task_completed", _internal: true,
233
+ ownerId: caller.ownerId || null, driverOriginId: caller.sessionOriginId || null,
234
+ workerOriginId: worker.sessionOriginId || null, taskId: token.taskId, generation: token.generation,
235
+ outcome: envelope }); } catch (evidenceError) { token._completionEvidenceError = evidenceError.message || String(evidenceError); }
236
+ }
231
237
  if (ctx.sm.sendToSession) ctx.sm.sendToSession(caller, { type: "partner_task_completed", outcome: envelope });
232
238
  return envelope;
233
239
  }
@@ -1,19 +1,12 @@
1
- var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts"); var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"),
2
- { attachPairTurnControl } = require("./session-pair-turn-control");
1
+ var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts"); var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"), { attachPairTurnControl } = require("./session-pair-turn-control");
3
2
  var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration"), { attachWorkerPermission } = require("./project-worker-permission"), { attachWorkerProposal } = require("./project-worker-proposal");
4
3
  var { attachPairMessage } = require("./project-pair-message"), { attachPairTaskControl } = require("./project-pair-task-control"), stopAutonomous = require("./project-pair-autonomous-stop").stopAutonomousWork, stopOwned = require("./project-pair-owned-stop").stopOwnedPair;
5
4
  var pairHistory = require("./session-pair-history"), responseText = pairHistory.responseText, errorSince = pairHistory.errorSince, recentTurns = pairHistory.recentTurns, operationFingerprint = require("./session-pair-operation").fingerprint;
6
- var beginPairQuery = require("./session-pair-query-start").beginPairQuery;
7
- function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
8
- function toolError(err) { return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true }); }
5
+ var beginPairQuery = require("./session-pair-query-start").beginPairQuery, attachPairResultPipeline = require("./project-pair-result-pipeline").attachPairResultPipeline;
6
+ function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); } function toolError(err) { return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true }); }
9
7
  function attachSessionPair(ctx) {
10
- var sm = ctx.sm;
11
- var store = ctx.splitStore;
12
- var workerProposal;
13
- var workerPermission;
14
- var lifecycle;
15
- var taskControl;
16
- var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
8
+ var sm = ctx.sm, store = ctx.splitStore, workerProposal, workerPermission, lifecycle, taskControl;
9
+ var turnControl = attachPairTurnControl({ sm: sm, splitStore: store }), resultCapture;
17
10
  function groupAndPartner(caller, missingPairMessage) {
18
11
  if (!caller) throw new Error("partner tools require a session-bound tool server");
19
12
  if (sm.sessions.get(caller.localId) !== caller) {
@@ -119,12 +112,14 @@ function attachSessionPair(ctx) {
119
112
  function requestDetach(worker) {
120
113
  var token = worker && worker._pairDelegation;
121
114
  if (!token || token.detached || token.detachRequested) return false;
122
- token.detachRequested = true;
115
+ if (token.outboxKey && ctx.resultOutbox && !ctx.resultOutbox.setRoute(token.outboxKey, "callback").ok) return false;
116
+ token.wait = false; token.detachRequested = true;
123
117
  return true;
124
118
  }
125
119
  function handleTurnDone(partner) {
126
120
  var token = partner && partner._pairDelegation;
127
121
  if (!token) return false;
122
+ if (token._capturePending || token._captureComplete) return false;
128
123
  var group = store.groupForMember(partner.localId);
129
124
  if (!group || group.id !== token.groupId || group.members.indexOf(token.from) === -1) return false;
130
125
  var caller = sm.sessions.get(token.from);
@@ -137,12 +132,8 @@ function attachSessionPair(ctx) {
137
132
  token.failure = errorSince(partner.history || [], token.startIndex);
138
133
  token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
139
134
  if (token.interrupted) taskControl.ensureSystemInterruption(partner, token);
140
- taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
141
- finishDelegation(group, caller, partner, token);
142
- if (typeof ctx.onPartnerResult === "function") ctx.onPartnerResult(caller, partner, token);
143
- var resumed = resumeDriverWithResult(caller, partner, token);
144
- taskControl.drain(caller, partner);
145
- return resumed;
135
+ var outcome = taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
136
+ return resultCapture.capture(caller, partner, token, outcome);
146
137
  }
147
138
  function waitForPartner(group, caller, partner, token, timeoutSeconds) {
148
139
  return new Promise(function (resolve, reject) {
@@ -157,28 +148,33 @@ function attachSessionPair(ctx) {
157
148
  }
158
149
  if (!partner.isProcessing && !partner._queryStarting) {
159
150
  clearInterval(timer);
160
- finishDelegation(group, caller, partner, token);
161
151
  var failure = errorSince(partner.history || [], token.startIndex);
162
152
  var interrupted = !failure && !!partner._lastTurnInterrupted;
163
153
  var response = responseText(partner.history || [], token.startIndex);
164
154
  if (interrupted) taskControl.ensureSystemInterruption(partner, token);
165
155
  var outcome = taskControl.complete(partner, caller, token, failure ? "failed" : (interrupted ? "interrupted" : "completed"), response, failure);
156
+ resultCapture.capture(caller, partner, token, outcome);
166
157
  resolve({
167
- status: failure ? "error" : (outcome.status === "completed" ? "complete" : outcome.status),
158
+ status: token._capturePending ? "capture_pending" : (failure ? "error" : (outcome.status === "completed" ? "complete" : outcome.status)),
168
159
  taskId: token.taskId,
169
160
  generation: token.generation,
170
161
  response: response,
171
162
  error: failure || undefined,
172
163
  outcome: outcome,
173
164
  });
174
- taskControl.drain(caller, partner);
175
165
  return;
176
166
  }
177
167
  if (Date.now() >= deadline || token.detachRequested) {
178
168
  clearInterval(timer);
179
- token.detached = true;
169
+ var routed = token.outboxKey && ctx.resultOutbox ? ctx.resultOutbox.setRoute(token.outboxKey, "callback") : { ok: true };
170
+ if (!routed.ok) {
171
+ monitorPartner(group, caller, partner, token);
172
+ resolve({ status: "capture_pending", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), error: routed.error });
173
+ return;
174
+ }
175
+ token.wait = false; token.detached = true;
180
176
  monitorPartner(group, caller, partner, token);
181
- resolve({ status: "running", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
177
+ resolve({ status: "running", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for a requested status, concrete failure or stall diagnosis, or capacity/queue decision; do not poll routinely." });
182
178
  }
183
179
  }, 500);
184
180
  });
@@ -201,14 +197,20 @@ function attachSessionPair(ctx) {
201
197
  var message = typeof args.message === "string" ? args.message.trim() : "";
202
198
  if (!message) throw new Error("message is required");
203
199
  turnControl.assertWorkerAction(caller);
204
- var wait = args.wait !== false;
200
+ var wait = args.wait === true;
205
201
  var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
206
202
  timeout = Math.max(1, Math.min(900, timeout));
207
203
  var partner = resolved.partner;
208
204
  if (partner._pairDelegation) throw new Error("the partner is already handling a delegated task");
209
205
  delete partner._pairInterruption; delete partner._lastInterruptedPairTask;
210
206
  var task = taskControl.begin(partner, caller, message, args.taskId);
211
- var token = Object.assign(task, { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length });
207
+ var token = Object.assign(task, { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length, wait: wait });
208
+ var started = resultCapture.begin(caller, partner, token, message, ctx.projectSlug);
209
+ if (!started.ok) {
210
+ token._retainOnError = true; partner._pairDelegation = token; partner._delegatedBy = caller.localId;
211
+ sm.sendAndRecord(caller, { type: "error", text: "The Split Worker task was not dispatched because its durable completion record could not be created: " + started.error });
212
+ return toolError(new Error(started.error));
213
+ }
212
214
  partner._pairDelegation = token;
213
215
  partner._delegatedBy = caller.localId;
214
216
  sm.sendAndRecord(partner, {
@@ -243,14 +245,14 @@ function attachSessionPair(ctx) {
243
245
  if (!wait) {
244
246
  token.detached = true;
245
247
  monitorPartner(resolved.group, caller, partner, token);
246
- return toolResult({ status: "running", partnerId: partner.localId, taskId: token.taskId, generation: token.generation, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
248
+ return toolResult({ status: "running", partnerId: partner.localId, taskId: token.taskId, generation: token.generation, hint: "The completed result will be pushed back automatically. Use read_partner only for a requested status, concrete failure or stall diagnosis, or capacity/queue decision; do not poll routinely." });
247
249
  }
248
250
  if (!partner.isProcessing && !partner._queryStarting) {
249
- finishDelegation(resolved.group, caller, partner, token);
250
251
  var immediateFailure = errorSince(partner.history || [], token.startIndex);
251
252
  var immediateResponse = responseText(partner.history || [], token.startIndex);
252
253
  var immediateOutcome = taskControl.complete(partner, caller, token, immediateFailure ? "failed" : "completed", immediateResponse, immediateFailure);
253
- taskControl.drain(caller, partner);
254
+ var immediateCapture = resultCapture.capture(caller, partner, token, immediateOutcome);
255
+ if (!immediateCapture && token._capturePending) return toolError(new Error("The completed Split Worker result could not be durably captured yet."));
254
256
  return toolResult({
255
257
  status: immediateFailure ? "error" : (immediateOutcome.status === "completed" ? "complete" : immediateOutcome.status),
256
258
  taskId: token.taskId,
@@ -268,7 +270,7 @@ function attachSessionPair(ctx) {
268
270
  if (group) {
269
271
  var partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
270
272
  var partner = sm.sessions.get(partnerId);
271
- if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing) {
273
+ if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing && !partner._pairDelegation._retainOnError) {
272
274
  finishDelegation(group, caller, partner, partner._pairDelegation);
273
275
  }
274
276
  }
@@ -334,6 +336,7 @@ function attachSessionPair(ctx) {
334
336
  function handleHumanStop(session) {
335
337
  var roles = turnControl.markHumanStop(session);
336
338
  if (!roles) return false;
339
+ if (roles.worker._pairDelegation && roles.worker._pairDelegation.outboxKey && ctx.resultOutbox) ctx.resultOutbox.markBlocked(roles.worker._pairDelegation.outboxKey, "The human stopped this Split Worker turn.", "human_stop");
337
340
  taskControl.markInterruption(roles.worker, "user", "The human stopped this Split Worker turn.");
338
341
  workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
339
342
  return true;
@@ -487,8 +490,9 @@ function attachSessionPair(ctx) {
487
490
  }
488
491
  return pairPrompt;
489
492
  }
490
- return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop, stopOwnedPair: stopOwnedPair, stopAutonomousWork: stopAutonomousWork, stopBarrier: stopBarrier,
491
- handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt,
493
+ var resultPipeline = attachPairResultPipeline({ sm: sm, store: store, resultOutbox: ctx.resultOutbox, getSdk: ctx.getSdk, getLinuxUserForSession: ctx.getLinuxUserForSession, acceptanceTimeoutMs: ctx.acceptanceTimeoutMs, finishDelegation: finishDelegation, resumeDriverWithResult: resumeDriverWithResult, onPartnerResult: ctx.onPartnerResult, onStateChange: ctx.onResultStateChange, blockedReason: turnControl.blockedReason, drain: function (caller, partner) { return taskControl.drain(caller, partner); } });
494
+ resultCapture = resultPipeline.capture; var resultDelivery = resultPipeline.delivery;
495
+ return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop, stopOwnedPair: stopOwnedPair, stopAutonomousWork: stopAutonomousWork, stopBarrier: stopBarrier, handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt,
492
496
  respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission, createOwnedPair: createOwnedPair, removeOwnedPair: removeOwnedPair, preflightRuntime: preflightRuntime,
493
- sendToPartner: sendToPartner, rolesFor: rolesFor };
497
+ sendToPartner: sendToPartner, rolesFor: rolesFor, resultCapture: resultCapture, resultOutbox: ctx.resultOutbox, wakePartnerResult: function (caller, partner, token) { return resultDelivery.wake(caller, partner, token); }, getResultOutboxStatus: function () { return ctx.resultOutbox ? ctx.resultOutbox.status() : { records: [], persistenceError: null }; } };
494
498
  } module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
package/lib/project.js CHANGED
@@ -55,6 +55,8 @@ var { attachDelegatedFollowUp } = require("./project-delegated-follow-up");
55
55
  var { attachCapsuleTurn } = require("./project-capsule-turn");
56
56
  var workspaceSessionRef = require("./workspace-query-service").sessionRef;
57
57
  var { attachSessionPair } = require("./project-session-pair");
58
+ var { attachPairResultOutbox } = require("./project-pair-result-outbox");
59
+ var { attachPairResultRecovery } = require("./project-pair-result-recovery");
58
60
  var { attachSessionHandoff } = require("./project-session-handoff");
59
61
  var { attachDriverContinuation } = require("./project-driver-continuation");
60
62
  var driverContinuationAccess = require("./driver-continuation-access");
@@ -819,6 +821,9 @@ function createProjectContext(opts) {
819
821
 
820
822
  // The SDK bridge is created after local MCP servers. Session spawning uses
821
823
  // a getter so tool handlers see the initialized bridge when they run.
824
+ var _pairResultOutbox = attachPairResultOutbox({ storageDir: sm.sessionsDir, projectSlug: slug });
825
+ _pairResultOutbox.recoverPending(sm.sessions);
826
+ var _pairResultRecovery = null;
822
827
  var _sessionPair = attachSessionPair({
823
828
  sm: sm,
824
829
  isMate: isMate,
@@ -837,12 +842,21 @@ function createProjectContext(opts) {
837
842
  usersModule: usersModule,
838
843
  getLinuxUserForSession: getLinuxUserForSession,
839
844
  onProcessingChanged: onProcessingChanged,
845
+ projectSlug: slug,
846
+ resultOutbox: _pairResultOutbox,
847
+ onResultStateChange: function (driver, record) { if (_pairResultRecovery) _pairResultRecovery.stateChanged(driver, record); },
840
848
  adapters: adapters,
841
849
  onPartnerResult: function (driver, worker, token) {
842
850
  if (_scheduledTaskExecution) _scheduledTaskExecution.onPartnerResult(driver, worker, token);
843
851
  if (_autonomousRun) _autonomousRun.onPartnerResult(driver, worker, token);
844
852
  },
845
853
  });
854
+ _pairResultRecovery = attachPairResultRecovery({
855
+ sm: sm, store: _splitGroups.store, outbox: _pairResultOutbox, resultCapture: _sessionPair.resultCapture,
856
+ wake: _sessionPair.wakePartnerResult, projectSlug: slug, sendTo: sendTo,
857
+ blockedReason: _sessionPair.stopBarrier,
858
+ getClients: function () { return clients; }, isMultiUser: usersModule.isMultiUser,
859
+ });
846
860
  var _sessionSpawn = attachSessionSpawn({
847
861
  cwd: cwd,
848
862
  sm: sm,
@@ -1277,6 +1291,7 @@ function createProjectContext(opts) {
1277
1291
  },
1278
1292
  onProcessingChanged: onProcessingChanged,
1279
1293
  onSessionAvailable: function (session) {
1294
+ if (_pairResultRecovery) _pairResultRecovery.available(session);
1280
1295
  if (_scheduledMessages) _scheduledMessages.notify();
1281
1296
  if (_userMessage && session) setImmediate(function () { _userMessage.consumePendingMessage(session); });
1282
1297
  },
@@ -1741,6 +1756,7 @@ function createProjectContext(opts) {
1741
1756
  if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
1742
1757
 
1743
1758
  // --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
1759
+ if (_pairResultRecovery.handleMessage(ws, msg)) return;
1744
1760
  if (_sessionPair.handleMessage(ws, msg)) return;
1745
1761
  if (_sessionHandoff.handleMessage(ws, msg)) return;
1746
1762
  if (_driverContinuation.handleMessage(ws, msg)) return;
@@ -2016,6 +2032,7 @@ function createProjectContext(opts) {
2016
2032
  serverAuthToken: serverAuthToken,
2017
2033
  resolveDefaultAi: opts.resolveDefaultAi,
2018
2034
  });
2035
+ _pairResultRecovery.reconcile();
2019
2036
  var _toolLlmRuntime = attachProjectToolLlmRuntime({
2020
2037
  users: usersModule,
2021
2038
  osUsers: osUsers,
@@ -2066,7 +2083,11 @@ function createProjectContext(opts) {
2066
2083
  if (isMate || !_sessionPair.workerPermission) return false;
2067
2084
  return _sessionPair.workerPermission.isDriverOperated(session);
2068
2085
  },
2069
- beginHumanPairTurn: function (session) { return _sessionPair.beginHumanTurn(session); },
2086
+ beginHumanPairTurn: function (session) {
2087
+ var resumed = _sessionPair.beginHumanTurn(session);
2088
+ if (resumed) _pairResultRecovery.beginHumanTurn(session);
2089
+ return resumed;
2090
+ },
2070
2091
  authorizePendingDispatch: function (session, actor, item) {
2071
2092
  if (!session || sm.sessions.get(session.localId) !== session || !item || item.projectSlug !== slug) return false;
2072
2093
  if (_scheduledMessages && !_scheduledMessages.authorizePending(item, session)) return false;
@@ -2499,6 +2520,7 @@ function createProjectContext(opts) {
2499
2520
  _email: _email,
2500
2521
  restoreDebateState: restoreDebateState,
2501
2522
  scheduledMessages: _scheduledMessages,
2523
+ pairResultRecovery: _pairResultRecovery,
2502
2524
  sendCursorSharingState: opts.sendCursorSharingState,
2503
2525
  stopFileWatch: stopFileWatch,
2504
2526
  stopAllDirWatches: stopAllDirWatches,
package/lib/public/app.js CHANGED
@@ -276,6 +276,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
276
276
  splitPanes: null,
277
277
  splitGroups: [],
278
278
  splitDelegations: {},
279
+ pairResultStatus: [],
279
280
  notesTemporarilyHidden: false,
280
281
  dmMode: false,
281
282
  historyFrom: 0,
@@ -1387,6 +1387,18 @@ pre.mermaid-error {
1387
1387
  .plan-banner.plan-exit { color: var(--success); }
1388
1388
  .plan-banner.plan-exit .plan-banner-icon { color: var(--success); }
1389
1389
 
1390
+ .pair-result-status { display: grid; gap: 8px; padding: 10px 16px 0; }
1391
+ .pair-result-status-card { border: 1px solid var(--border); border-radius: 10px; background: var(--bg-alt); padding: 10px 12px; color: var(--text-secondary); font-size: 12px; }
1392
+ .pair-result-status-card > strong, .pair-result-status-card > span { display: block; }
1393
+ .pair-result-status-card > strong { color: var(--text); margin-bottom: 3px; }
1394
+ .pair-result-status-card details { margin-top: 7px; }
1395
+ .pair-result-status-card summary { cursor: pointer; color: var(--accent); }
1396
+ .pair-result-status-card pre { max-height: 180px; overflow: auto; white-space: pre-wrap; font: inherit; }
1397
+ .pair-result-status-card button { margin-top: 8px; }
1398
+ .pair-result-status-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1399
+ .pair-result-status-retry:disabled { cursor: not-allowed; opacity: 0.55; }
1400
+ .pair-result-status-uncertain, .pair-result-status-blocked { border-color: color-mix(in srgb, var(--warning) 45%, var(--border)); }
1401
+
1390
1402
  /* --- Plan card --- */
1391
1403
  .plan-card {
1392
1404
  max-width: var(--content-width);
@@ -62,6 +62,7 @@ import { handleShellCommandResult } from './shell-command.js';
62
62
  import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
63
63
  import { handleScheduledTaskMessage, handleScheduledTaskSessionSwitched } from './scheduled-tasks.js';
64
64
  import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerationProgress, clearAllImageGenerationProgress } from './generated-images.js';
65
+ import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
65
66
 
66
67
  // --- App module imports ---
67
68
  import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
@@ -243,6 +244,7 @@ export function processMessage(msg) {
243
244
  // Restore cached rich context usage BEFORE updateContextPanel runs
244
245
  if (msg.contextUsage && !msg.contextUsage.unavailable) store.set({ richContextUsage: msg.contextUsage });
245
246
  else markContextUnavailable();
247
+
246
248
  // Restore accurate context data from the last result in full history
247
249
  if (msg.lastUsage || msg.lastModelUsage) {
248
250
  accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
@@ -804,6 +806,10 @@ export function processMessage(msg) {
804
806
  handleRemoteSelection(msg);
805
807
  break;
806
808
 
809
+ case "pair_result_status":
810
+ handlePairResultStatus(msg);
811
+ break;
812
+
807
813
  case "session_io":
808
814
  blinkSessionDot(msg.id);
809
815
  break;
@@ -823,6 +829,7 @@ export function processMessage(msg) {
823
829
  break;
824
830
 
825
831
  case "session_switched":
832
+ clearPairResultStatus();
826
833
  syncAutonomousRunForSession(msg.autonomousRun || null);
827
834
  closeWhatsNewArticle();
828
835
  // Save draft from outgoing session
@@ -0,0 +1,87 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { showConfirm } from './app-misc.js';
4
+
5
+ var statusGeneration = 0;
6
+
7
+ function retrySource(item) {
8
+ return { item: item, generation: statusGeneration, projectSlug: store.get("currentSlug") || store.get("activeProjectSlug") || null,
9
+ sessionId: store.get("activeSessionId"), userId: store.get("myUserId") || null, ws: getWs() };
10
+ }
11
+
12
+ function sendRetry(source, confirmed) {
13
+ var ws = getWs();
14
+ var projectSlug = store.get("currentSlug") || store.get("activeProjectSlug") || null;
15
+ var items = store.get("pairResultStatus") || [];
16
+ var stillVisible = items.some(function (item) { return item.id === source.item.id; });
17
+ if (!ws || ws !== source.ws || ws.readyState !== 1 || statusGeneration !== source.generation ||
18
+ store.get("activeSessionId") !== source.sessionId || projectSlug !== source.projectSlug ||
19
+ (store.get("myUserId") || null) !== source.userId || !stillVisible) return false;
20
+ ws.send(JSON.stringify({ type: "pair_result_retry", id: source.item.id, confirmDuplicate: confirmed === true,
21
+ sessionId: source.sessionId, projectSlug: source.projectSlug }));
22
+ return true;
23
+ }
24
+
25
+ function retry(item) {
26
+ var source = retrySource(item);
27
+ if (item.confirmDuplicate) {
28
+ showConfirm("The Driver may already have received this completed result. Retry delivery anyway? This can cause a duplicate result receipt.", function () {
29
+ sendRetry(source, true);
30
+ }, "Retry delivery", false);
31
+ return;
32
+ }
33
+ sendRetry(source, false);
34
+ }
35
+
36
+ function renderPairResultStatus() {
37
+ var existing = document.getElementById("pair-result-status");
38
+ if (existing) existing.remove();
39
+ var items = store.get("pairResultStatus") || [];
40
+ if (!items.length) return;
41
+ var messages = document.getElementById("messages");
42
+ if (!messages || !messages.parentElement) return;
43
+ var section = document.createElement("section");
44
+ section.id = "pair-result-status";
45
+ section.className = "pair-result-status";
46
+ section.setAttribute("role", "status");
47
+ for (var i = 0; i < items.length; i++) {
48
+ var item = items[i], card = document.createElement("div");
49
+ card.className = "pair-result-status-card pair-result-status-" + item.state;
50
+ var title = document.createElement("strong");
51
+ title.textContent = item.state === "blocked" ? "Split Worker result blocked" :
52
+ (item.state === "uncertain" ? (item.hasCompletedResult === false ? "Split Worker recovery uncertain" : "Split Worker result delivery uncertain") : "Split Worker result pending");
53
+ var message = document.createElement("span");
54
+ message.textContent = item.message || "The completed result is retained.";
55
+ card.appendChild(title); card.appendChild(message);
56
+ if (item.hasCompletedResult !== false) {
57
+ var details = document.createElement("details"), summary = document.createElement("summary"), body = document.createElement("pre");
58
+ summary.textContent = "View completed result";
59
+ body.textContent = item.error ? "Error: " + item.error : (item.response || "No text response was recorded.");
60
+ details.appendChild(summary); details.appendChild(body); card.appendChild(details);
61
+ }
62
+ if (item.canRetry) {
63
+ var button = document.createElement("button");
64
+ button.type = "button"; button.className = "confirm-btn confirm-ok pair-result-status-retry"; button.textContent = "Retry delivery";
65
+ button.addEventListener("click", retry.bind(null, item));
66
+ card.appendChild(button);
67
+ }
68
+ section.appendChild(card);
69
+ }
70
+ messages.parentElement.insertBefore(section, messages);
71
+ }
72
+
73
+ function handlePairResultStatus(msg) {
74
+ if (!msg || msg.sessionId !== store.get("activeSessionId")) return false;
75
+ statusGeneration++;
76
+ store.set({ pairResultStatus: Array.isArray(msg.items) ? msg.items : [] });
77
+ renderPairResultStatus();
78
+ return true;
79
+ }
80
+
81
+ function clearPairResultStatus() {
82
+ statusGeneration++;
83
+ store.set({ pairResultStatus: [] });
84
+ renderPairResultStatus();
85
+ }
86
+
87
+ export { clearPairResultStatus, handlePairResultStatus, renderPairResultStatus };