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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/lib/driver-continuation-pair.js +5 -1
  2. package/lib/multi-worker-feature.js +11 -0
  3. package/lib/project-connection.js +2 -0
  4. package/lib/project-pair-autonomous-stop.js +14 -8
  5. package/lib/project-pair-close-transaction.js +59 -0
  6. package/lib/project-pair-global-stop.js +18 -0
  7. package/lib/project-pair-lifecycle-status.js +34 -0
  8. package/lib/project-pair-lifecycle.js +58 -76
  9. package/lib/project-pair-message.js +1 -1
  10. package/lib/project-pair-owned-stop.js +7 -4
  11. package/lib/project-pair-result-capture.js +116 -0
  12. package/lib/project-pair-result-delivery.js +140 -0
  13. package/lib/project-pair-result-outbox.js +400 -0
  14. package/lib/project-pair-result-pipeline.js +16 -0
  15. package/lib/project-pair-result-recovery.js +229 -0
  16. package/lib/project-pair-structural-close.js +20 -0
  17. package/lib/project-pair-task-control.js +45 -18
  18. package/lib/project-pair-worker-close.js +68 -0
  19. package/lib/project-session-pair-prompt.js +26 -0
  20. package/lib/project-session-pair.js +87 -106
  21. package/lib/project-worker-creation-proposal.js +176 -0
  22. package/lib/project-worker-permission.js +7 -8
  23. package/lib/project-worker-proposal-runner.js +45 -0
  24. package/lib/project-worker-proposal.js +79 -115
  25. package/lib/project.js +38 -1
  26. package/lib/public/app.js +1 -0
  27. package/lib/public/css/messages.css +12 -0
  28. package/lib/public/css/pane.css +48 -2
  29. package/lib/public/modules/app-connection.js +49 -76
  30. package/lib/public/modules/app-messages.js +11 -0
  31. package/lib/public/modules/pair-result-status.js +87 -0
  32. package/lib/public/modules/project-activation.js +10 -7
  33. package/lib/public/modules/sidebar-sessions.js +31 -11
  34. package/lib/public/modules/split-group-helpers.js +74 -2
  35. package/lib/public/modules/split-pair-ui.js +28 -6
  36. package/lib/public/modules/split-pane-reconciler.js +28 -0
  37. package/lib/public/modules/split-pane-renderer.js +107 -0
  38. package/lib/public/modules/split-view.js +58 -188
  39. package/lib/public/modules/websocket-watchdog.js +126 -0
  40. package/lib/public/modules/worker-pane-lock.js +13 -3
  41. package/lib/server.js +1 -0
  42. package/lib/session-pair-factory.js +66 -0
  43. package/lib/session-pair-mcp-server.js +10 -5
  44. package/lib/session-pair-prompts.js +41 -25
  45. package/lib/session-pair-target.js +42 -0
  46. package/lib/session-pair-turn-control.js +11 -6
  47. package/lib/session-split-group-anchors.js +89 -7
  48. package/lib/session-split-group-persistence.js +136 -0
  49. package/lib/session-split-group-roles.js +164 -0
  50. package/lib/session-split-group-v2-store.js +146 -0
  51. package/lib/session-split-groups.js +109 -99
  52. package/lib/sessions.js +33 -2
  53. package/lib/ws-schema.js +2 -0
  54. package/package.json +1 -1
@@ -0,0 +1,140 @@
1
+ var RETRY_DELAY_MS = 50;
2
+ var DEFAULT_ACCEPTANCE_TIMEOUT_MS = 15000;
3
+ var splitRoles = require("./session-split-group-roles");
4
+
5
+ function attachPairResultDelivery(ctx) {
6
+ function recordMatches(caller, partner, token, record) {
7
+ return !!(record && record.key === token.outboxKey && record.ownerId === (caller.ownerId || null) &&
8
+ record.driverOriginId === ctx.outbox.stableOrigin(caller) && record.workerOriginId === ctx.outbox.stableOrigin(partner) &&
9
+ record.taskId === token.taskId && record.generation === token.generation && record.deliveryRoute === "callback");
10
+ }
11
+ function valid(caller, partner, token) {
12
+ var group = ctx.store.groupForMember(partner.localId), record = ctx.outbox.get(token.outboxKey);
13
+ return !!(group && group.id === token.groupId && splitRoles.matchesLiveTarget(ctx.sm, group, caller, partner) &&
14
+ splitRoles.tokenMatchesWorker(partner, token) &&
15
+ ctx.sm.sessions.get(caller.localId) === caller &&
16
+ ctx.sm.sessions.get(partner.localId) === partner && caller.ownerId === partner.ownerId &&
17
+ !caller.destroying && !partner.destroying && !ctx.blockedReason(caller) && partner._pairDelegation === token &&
18
+ recordMatches(caller, partner, token, record));
19
+ }
20
+ function currentAttempt(caller, partner, token, attemptId) {
21
+ var record = ctx.outbox.get(token.outboxKey);
22
+ return valid(caller, partner, token) && record && record.deliveryState === "attempting" && record.attemptId === attemptId;
23
+ }
24
+ function textFor(partner, token) {
25
+ var failure = token.failure || null, response = token.response || "";
26
+ if (token.interrupted) return "[Split Worker execution interrupted] The user interrupted the Split Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
27
+ return "A Split Worker task delegated through send_to_partner has finished.\n\nOriginal task:\n" + token.message + "\n\n" + (failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) + "\n\nReview the result, verify it as needed, and continue the task.";
28
+ }
29
+ function finish(caller, partner, token) { return ctx.finish(caller, partner, token, true); }
30
+ function clearAttemptTimer(token, attemptId) {
31
+ if (!token._deliveryTimer || token._deliveryTimer.attemptId !== attemptId) return;
32
+ clearTimeout(token._deliveryTimer.timer);
33
+ delete token._deliveryTimer;
34
+ }
35
+ function retry(caller, partner, token, attemptId, reason) {
36
+ clearAttemptTimer(token, attemptId);
37
+ if (!currentAttempt(caller, partner, token, attemptId)) return { ok: false, stale: true };
38
+ var pending = ctx.outbox.markDelivery(token.outboxKey, attemptId, "pending", { deliveryError: reason || null });
39
+ if (!pending.ok) {
40
+ if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, ctx.outbox.get(token.outboxKey));
41
+ return { ok: false, error: pending.error, stale: pending.stale };
42
+ }
43
+ if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, pending.record);
44
+ if (pending.record.deliveryAttemptCount < ctx.outbox.maxDeliveryAttempts) {
45
+ setTimeout(function () { wake(caller, partner, token); }, RETRY_DELAY_MS * pending.record.deliveryAttemptCount);
46
+ }
47
+ return { ok: false, pending: true };
48
+ }
49
+ function uncertain(caller, partner, token, attemptId, reason) {
50
+ clearAttemptTimer(token, attemptId);
51
+ if (!currentAttempt(caller, partner, token, attemptId)) return false;
52
+ token._deliveryUncertain = true;
53
+ var saved = ctx.outbox.markDelivery(token.outboxKey, attemptId, "uncertain", { deliveryError: reason || "delivery acceptance is uncertain" });
54
+ if (saved.ok && typeof ctx.onStateChange === "function") ctx.onStateChange(caller, saved.record);
55
+ return false;
56
+ }
57
+ function accepted(caller, partner, token, attemptId) {
58
+ clearAttemptTimer(token, attemptId);
59
+ if (!currentAttempt(caller, partner, token, attemptId)) return { ok: false, stale: true };
60
+ var saved = ctx.outbox.markDelivery(token.outboxKey, attemptId, "accepted", { acceptedAt: Date.now() });
61
+ if (!saved.ok) {
62
+ token._deliveryUncertain = true;
63
+ if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, ctx.outbox.get(token.outboxKey));
64
+ return { ok: false, uncertain: true, error: saved.error };
65
+ }
66
+ token._deliveryAccepted = true;
67
+ if (typeof ctx.onStateChange === "function") ctx.onStateChange(caller, saved.record);
68
+ return { ok: true, accepted: true, finished: finish(caller, partner, token) !== false };
69
+ }
70
+ function recordTranscript(caller, token, message, attemptId) {
71
+ var outboxRecord = ctx.outbox.get(token.outboxKey);
72
+ if (outboxRecord && outboxRecord.transcriptPersisted) return { ok: true };
73
+ var matches = function (entry) { return !!(entry && entry.pairResultOutboxKey === token.outboxKey); };
74
+ var existing = caller.history && caller.history.some(matches);
75
+ var durable = existing && typeof ctx.sm.hasDurableSessionRecord === "function" && ctx.sm.hasDurableSessionRecord(caller, matches);
76
+ if (!durable) {
77
+ var record = { type: "user_message", text: message, _internal: true, partnerResult: true,
78
+ pairResultOutboxKey: token.outboxKey, pairResultAttemptId: attemptId };
79
+ var saved;
80
+ try {
81
+ if (typeof ctx.sm.sendAndRecordDurably !== "function") return { ok: false, error: "Durable Driver transcript recording is unavailable" };
82
+ saved = ctx.sm.sendAndRecordDurably(caller, record);
83
+ } catch (error) { return { ok: false, error: error.message || String(error) }; }
84
+ if (saved !== true) {
85
+ if (caller.history && caller.history[caller.history.length - 1] === record) caller.history.pop();
86
+ return { ok: false, error: "Driver transcript persistence failed" };
87
+ }
88
+ }
89
+ var marked = ctx.outbox.markTranscript(token.outboxKey, attemptId);
90
+ return marked.ok ? { ok: true } : { ok: false, error: marked.error, stale: marked.stale };
91
+ }
92
+ function wake(caller, partner, token) {
93
+ if (!token || token._deliveryUncertain || token._deliveryAccepted) return { ok: false, deferred: true };
94
+ if (ctx.blockedReason(caller)) {
95
+ var blocked = ctx.outbox.markBlocked(token.outboxKey, ctx.blockedReason(caller), "human_stop");
96
+ if (blocked.ok && typeof ctx.onStateChange === "function") ctx.onStateChange(caller, blocked.record);
97
+ return { ok: false, blocked: true };
98
+ }
99
+ if (!valid(caller, partner, token)) return { ok: false, deferred: true };
100
+ if (caller._queryStarting) return { ok: false, deferred: true, reason: "query_starting" };
101
+ var started = ctx.outbox.beginDelivery(token.outboxKey);
102
+ if (!started.ok) return { ok: false, pending: true, error: started.error };
103
+ if (started.terminal || started.inFlight || started.blocked || started.exhausted) return { ok: true, terminal: !!started.terminal, exhausted: !!started.exhausted };
104
+ var attemptId = started.attemptId, message = textFor(partner, token);
105
+ var transcript = recordTranscript(caller, token, message, attemptId);
106
+ if (!transcript.ok) return retry(caller, partner, token, attemptId, transcript.error);
107
+ var sdk = ctx.getSdk();
108
+ if (!sdk || typeof sdk.pushMessage !== "function" || typeof sdk.startQuery !== "function") {
109
+ return retry(caller, partner, token, attemptId, "SDK bridge is unavailable");
110
+ }
111
+ try {
112
+ if (caller.queryInstance) {
113
+ if (sdk.pushMessage(caller, message) === true) {
114
+ return accepted(caller, partner, token, attemptId);
115
+ }
116
+ return retry(caller, partner, token, attemptId, "Driver query rejected the result");
117
+ }
118
+ token._deliveryStartupAttemptId = attemptId;
119
+ var beforePush = function () { return token._deliveryStartupAttemptId === attemptId && currentAttempt(caller, partner, token, attemptId); };
120
+ var onAccepted = function () { accepted(caller, partner, token, attemptId); };
121
+ var timeoutMs = Number(ctx.acceptanceTimeoutMs || DEFAULT_ACCEPTANCE_TIMEOUT_MS);
122
+ token._deliveryTimer = { attemptId: attemptId, timer: setTimeout(function () {
123
+ uncertain(caller, partner, token, attemptId, "delivery acceptance timed out");
124
+ }, timeoutMs) };
125
+ var startup = sdk.startQuery(caller, message, undefined, ctx.getLinuxUserForSession(caller), beforePush, onAccepted);
126
+ Promise.resolve(startup).then(function (result) {
127
+ if (!currentAttempt(caller, partner, token, attemptId)) return;
128
+ if (result === false) retry(caller, partner, token, attemptId, "Driver query rejected the result");
129
+ }, function (error) {
130
+ if (currentAttempt(caller, partner, token, attemptId)) uncertain(caller, partner, token, attemptId, error.message || String(error));
131
+ });
132
+ return { ok: false, pending: true };
133
+ } catch (error) {
134
+ return uncertain(caller, partner, token, attemptId, error.message || String(error));
135
+ }
136
+ }
137
+ return { wake: wake };
138
+ }
139
+
140
+ module.exports = { attachPairResultDelivery: attachPairResultDelivery };
@@ -0,0 +1,400 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+
4
+ var MAX_CAPTURE_ATTEMPTS = 3;
5
+ var MAX_DELIVERY_ATTEMPTS = 3;
6
+ var RETRY_DELAY_MS = 25;
7
+
8
+ function stableOrigin(session) {
9
+ if (session && typeof session.sessionOriginId === "string" && session.sessionOriginId) return session.sessionOriginId;
10
+ return null;
11
+ }
12
+
13
+ function resultKey(args) {
14
+ return [args.ownerId || "_default", args.driverOriginId, args.workerOriginId, args.taskId, args.generation == null ? "unknown" : args.generation].join("/");
15
+ }
16
+
17
+ function copy(value) {
18
+ return value == null ? value : JSON.parse(JSON.stringify(value));
19
+ }
20
+
21
+ function attachPairResultOutbox(options) {
22
+ options = options || {};
23
+ var filePath = options.filePath;
24
+ if (!filePath && options.storageDir) filePath = path.join(options.storageDir, "pair-result-outbox.json");
25
+ if (!filePath) throw new Error("pair result outbox requires a file path");
26
+ var records = Object.create(null);
27
+ var captureOperations = Object.create(null);
28
+ var lastPersistenceError = null;
29
+ var storageCorrupt = false;
30
+
31
+ function read() {
32
+ try {
33
+ var parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
34
+ if (!parsed || typeof parsed !== "object" || !parsed.records || typeof parsed.records !== "object") throw new Error("pair result outbox has an invalid format");
35
+ records = parsed.records;
36
+ } catch (error) {
37
+ if (error.code !== "ENOENT") { storageCorrupt = true; lastPersistenceError = error.message || String(error); }
38
+ }
39
+ }
40
+ function persist(nextRecords) {
41
+ if (storageCorrupt) return { ok: false, error: lastPersistenceError || "pair result outbox storage is unreadable" };
42
+ var serialized = JSON.stringify({ version: 1, records: nextRecords || records }, null, 2) + "\n";
43
+ try {
44
+ if (typeof options.persist === "function") {
45
+ if (options.persist(filePath, serialized) === false) throw new Error("pair result outbox persistence returned false");
46
+ lastPersistenceError = null;
47
+ return { ok: true };
48
+ }
49
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
50
+ var tempPath = filePath + ".tmp." + process.pid;
51
+ fs.writeFileSync(tempPath, serialized, "utf8");
52
+ if (process.platform !== "win32") {
53
+ try { fs.chmodSync(tempPath, 0o600); } catch (chmodError) {}
54
+ }
55
+ fs.renameSync(tempPath, filePath);
56
+ lastPersistenceError = null;
57
+ return { ok: true };
58
+ } catch (error) {
59
+ lastPersistenceError = error.message || String(error);
60
+ return { ok: false, error: lastPersistenceError };
61
+ }
62
+ }
63
+ function sameOutcome(left, right) { return JSON.stringify(left) === JSON.stringify(right); }
64
+ function captureRecord(key, outcome) {
65
+ var record = records[key];
66
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
67
+ if (record.outcome) {
68
+ if (!sameOutcome(record.outcome, outcome)) return { ok: false, conflict: true, error: "pair result outcome is immutable", record: copy(record) };
69
+ return { ok: true, record: copy(record), duplicate: true };
70
+ }
71
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
72
+ outcome: copy(outcome), state: record.state === "blocked" ? "blocked" : "captured", capturedAt: Date.now(),
73
+ preparedCloseOutcome: null,
74
+ closePreparationState: record.preparedCloseOutcome ? "superseded_by_terminal_evidence" : record.closePreparationState || null,
75
+ });
76
+ nextRecords[record.key] = next;
77
+ var saved = persist(nextRecords);
78
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
79
+ records = nextRecords;
80
+ return { ok: true, record: copy(next), duplicate: false };
81
+ }
82
+ function addCaptureCallback(operation, callback) {
83
+ if (typeof callback === "function" && operation.callbacks.indexOf(callback) === -1) operation.callbacks.push(callback);
84
+ }
85
+ function notifyCapture(operation, result) {
86
+ var callbacks = operation.callbacks.slice(); operation.callbacks = [];
87
+ for (var i = 0; i < callbacks.length; i++) callbacks[i](result);
88
+ }
89
+ function retryCapture(key, operation, attempt) {
90
+ attempt = attempt || 1;
91
+ if (captureOperations[key] !== operation || !operation.active) return { ok: false, stale: true };
92
+ var isCurrent = true;
93
+ try { if (operation.isCurrent) isCurrent = operation.isCurrent() === true; } catch (error) { isCurrent = false; }
94
+ if (!isCurrent) {
95
+ operation.active = false;
96
+ var stale = { ok: false, stale: true, error: "pair result capture no longer matches the live Worker task" };
97
+ operation.lastResult = stale;
98
+ notifyCapture(operation, stale);
99
+ return stale;
100
+ }
101
+ var result = captureRecord(key, operation.outcome);
102
+ operation.lastResult = result;
103
+ if (result.ok) {
104
+ operation.active = false; operation.complete = true;
105
+ if (attempt > 1) notifyCapture(operation, result);
106
+ else operation.callbacks = [];
107
+ return result;
108
+ }
109
+ if (attempt >= MAX_CAPTURE_ATTEMPTS) {
110
+ operation.active = false;
111
+ notifyCapture(operation, result);
112
+ return result;
113
+ }
114
+ operation.timer = (options.setTimeout || setTimeout)(function () { retryCapture(key, operation, attempt + 1); }, RETRY_DELAY_MS * attempt);
115
+ return result;
116
+ }
117
+ function begin(args) {
118
+ if (!args.driverOriginId || !args.workerOriginId) return { ok: false, error: "durable session origins are required before dispatch" };
119
+ var key = resultKey(args);
120
+ var existing = records[key];
121
+ if (existing) return { ok: true, key: key, record: copy(existing), duplicate: true };
122
+ var record = {
123
+ key: key,
124
+ ownerId: args.ownerId || null,
125
+ projectSlug: args.projectSlug || null,
126
+ driverOriginId: args.driverOriginId,
127
+ workerOriginId: args.workerOriginId,
128
+ taskId: args.taskId,
129
+ generation: args.generation == null ? null : args.generation,
130
+ driverSessionId: args.driverSessionId == null ? null : args.driverSessionId,
131
+ workerSessionId: args.workerSessionId == null ? null : args.workerSessionId,
132
+ groupId: typeof args.groupId === "string" ? args.groupId : null,
133
+ workerSessionOriginId: args.workerOriginId,
134
+ historyStartIndex: args.historyStartIndex == null ? null : args.historyStartIndex,
135
+ message: args.message || "",
136
+ state: "dispatching",
137
+ deliveryState: "pending",
138
+ deliveryRoute: args.deliveryRoute === "blocking" ? "blocking" : "callback",
139
+ deliveryAttemptCount: 0,
140
+ deliveryAttemptSequence: 0,
141
+ transcriptPersisted: false,
142
+ blockedReason: null,
143
+ blockedCode: null,
144
+ finalized: false,
145
+ manualRetryCount: 0,
146
+ manualRetryEpoch: 0,
147
+ createdAt: Date.now(),
148
+ capturedAt: null,
149
+ outcome: null,
150
+ };
151
+ var nextRecords = Object.assign({}, records);
152
+ nextRecords[key] = record;
153
+ var saved = persist(nextRecords);
154
+ if (!saved.ok) return { ok: false, key: key, error: saved.error, record: copy(record) };
155
+ records = nextRecords;
156
+ return { ok: true, key: key, record: copy(record), duplicate: false };
157
+ }
158
+ function capture(args, outcome, callback, isCurrent) {
159
+ var key = typeof args === "string" ? args : resultKey(args);
160
+ var record = records[key];
161
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
162
+ if (record.outcome) return captureRecord(key, outcome);
163
+ var operation = captureOperations[key];
164
+ if (operation) {
165
+ if (!sameOutcome(operation.outcome, outcome)) return { ok: false, conflict: true, error: "pair result outcome is immutable" };
166
+ addCaptureCallback(operation, callback);
167
+ if (operation.active) return Object.assign({ inFlight: true }, operation.lastResult || { ok: false });
168
+ if (operation.complete) return captureRecord(key, outcome);
169
+ operation.active = true; operation.callbacks = [];
170
+ addCaptureCallback(operation, callback);
171
+ return retryCapture(key, operation, 1);
172
+ }
173
+ operation = { outcome: copy(outcome), callbacks: [], active: true, complete: false, lastResult: null, timer: null,
174
+ isCurrent: typeof isCurrent === "function" ? isCurrent : null };
175
+ captureOperations[key] = operation;
176
+ addCaptureCallback(operation, callback);
177
+ return retryCapture(key, operation, 1);
178
+ }
179
+ function prepareCapture(key, outcome) {
180
+ var record = records[key];
181
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
182
+ if (record.outcome) {
183
+ if (!sameOutcome(record.outcome, outcome)) return { ok: false, conflict: true, error: "pair result outcome is immutable", record: copy(record) };
184
+ return { ok: true, record: copy(record), duplicate: true, committed: true };
185
+ }
186
+ if (record.preparedCloseOutcome) {
187
+ if (!sameOutcome(record.preparedCloseOutcome, outcome)) return { ok: false, conflict: true, error: "prepared close outcome changed", record: copy(record) };
188
+ return { ok: true, record: copy(record), duplicate: true, prepared: true };
189
+ }
190
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
191
+ state: "close_prepared", preparedCloseOutcome: copy(outcome),
192
+ closePreparationState: "prepared", closePreparedAt: Date.now(),
193
+ });
194
+ nextRecords[key] = next;
195
+ var saved = persist(nextRecords);
196
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
197
+ records = nextRecords;
198
+ return { ok: true, record: copy(next), duplicate: false, prepared: true };
199
+ }
200
+ function finalizePreparedCapture(key, outcome) {
201
+ var record = records[key];
202
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
203
+ if (record.outcome) return captureRecord(key, outcome);
204
+ if (!record.preparedCloseOutcome || !sameOutcome(record.preparedCloseOutcome, outcome)) {
205
+ return { ok: false, error: "prepared close outcome is unavailable or changed", record: copy(record) };
206
+ }
207
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
208
+ outcome: copy(outcome), state: "captured", capturedAt: Date.now(),
209
+ preparedCloseOutcome: null, closePreparationState: "committed", closeCommittedAt: Date.now(),
210
+ });
211
+ nextRecords[key] = next;
212
+ var saved = persist(nextRecords);
213
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
214
+ records = nextRecords;
215
+ return { ok: true, record: copy(next), duplicate: false };
216
+ }
217
+ function rollbackPreparedCapture(key, outcome) {
218
+ var record = records[key];
219
+ if (!record || !sameOutcome(record.preparedCloseOutcome, outcome) || record.outcome || record.finalized || record.transcriptPersisted ||
220
+ record.deliveryAttemptCount !== 0 || record.deliveryState !== "pending") {
221
+ return { ok: false, error: "prepared pair result can no longer be rolled back" };
222
+ }
223
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, {
224
+ state: "dispatching", preparedCloseOutcome: null, closePreparationState: null, closePreparedAt: null,
225
+ });
226
+ nextRecords[key] = next;
227
+ var saved = persist(nextRecords);
228
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
229
+ records = nextRecords;
230
+ return { ok: true, record: copy(next) };
231
+ }
232
+ function transition(key, changes) {
233
+ var record = records[key];
234
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
235
+ var nextRecords = Object.assign({}, records), next = Object.assign({}, record, changes);
236
+ nextRecords[key] = next;
237
+ var saved = persist(nextRecords);
238
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
239
+ records = nextRecords;
240
+ return { ok: true, record: copy(next) };
241
+ }
242
+ function lifetimeAttemptSequence(record) {
243
+ var sequence = Math.max(Number(record.deliveryAttemptSequence || 0), Number(record.deliveryAttemptCount || 0));
244
+ var match = typeof record.attemptId === "string" && record.attemptId.match(/:(\d+)$/);
245
+ if (match) sequence = Math.max(sequence, Number(match[1]));
246
+ return sequence;
247
+ }
248
+ function beginDelivery(key) {
249
+ var record = records[key];
250
+ if (!record || !record.outcome) return { ok: false, error: "pair result is not captured" };
251
+ if (record.deliveryRoute !== "callback") return { ok: true, record: copy(record), blocked: true };
252
+ if (record.deliveryState === "accepted" || record.deliveryState === "uncertain") return { ok: true, record: copy(record), terminal: true };
253
+ if (record.deliveryState === "attempting") return { ok: true, record: copy(record), inFlight: true };
254
+ var count = Number(record.deliveryAttemptCount || 0);
255
+ if (count >= MAX_DELIVERY_ATTEMPTS) return { ok: true, record: copy(record), exhausted: true };
256
+ var sequence = lifetimeAttemptSequence(record) + 1;
257
+ var attemptId = key + ":" + sequence;
258
+ var started = transition(key, { deliveryState: "attempting", deliveryAttemptCount: count + 1,
259
+ deliveryAttemptSequence: sequence, attemptId: attemptId, attemptStartedAt: Date.now(), deliveryError: null });
260
+ if (started.ok) started.attemptId = attemptId;
261
+ return started;
262
+ }
263
+ function transitionAttempt(key, attemptId, allowedStates, changes) {
264
+ var record = records[key];
265
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
266
+ if (record.attemptId !== attemptId || allowedStates.indexOf(record.deliveryState) === -1) {
267
+ return { ok: false, stale: true, record: copy(record), error: "delivery attempt is no longer current" };
268
+ }
269
+ return transition(key, changes);
270
+ }
271
+ function markTranscript(key, attemptId) {
272
+ return transitionAttempt(key, attemptId, ["attempting"], { transcriptPersisted: true, transcriptPersistedAt: Date.now() });
273
+ }
274
+ function markDelivery(key, attemptId, state, changes) {
275
+ return transitionAttempt(key, attemptId, ["attempting"], Object.assign({ deliveryState: state }, changes || {}));
276
+ }
277
+ function setRoute(key, route) {
278
+ var record = records[key];
279
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
280
+ if (route !== "blocking" && route !== "callback") return { ok: false, error: "delivery route is invalid" };
281
+ if (record.deliveryState === "accepted" || record.deliveryState === "uncertain") return { ok: false, error: "terminal delivery route cannot change" };
282
+ return transition(key, { deliveryRoute: route });
283
+ }
284
+ function recover(key, session, callback) {
285
+ var record = records[key];
286
+ var history = session && Array.isArray(session.history) ? session.history : [];
287
+ var origin = stableOrigin(session);
288
+ var boundary = record && Number.isInteger(record.historyStartIndex) ? record.historyStartIndex : 0;
289
+ var terminal = record && origin === record.workerOriginId && session.ownerId === record.ownerId && history.slice(boundary).filter(function (entry) {
290
+ return entry && entry.type === "pair_task_completed" && entry.ownerId === record.ownerId && entry.driverOriginId === record.driverOriginId && entry.workerOriginId === record.workerOriginId && entry.taskId === record.taskId && entry.generation === record.generation;
291
+ }).pop();
292
+ if (!terminal || !terminal.outcome || terminal.outcome.taskId !== record.taskId || terminal.outcome.generation !== record.generation) return { ok: false, uncertain: true, error: "stored Worker history does not prove this terminal completion" };
293
+ return capture(key, terminal.outcome, callback);
294
+ }
295
+ function recoverPending(sessionMap) {
296
+ var results = [], sessionsList = sessionMap && typeof sessionMap.values === "function" ? Array.from(sessionMap.values()) : [], keys = Object.keys(records);
297
+ for (var i = 0; i < keys.length; i++) {
298
+ if (records[keys[i]].state !== "dispatching" && records[keys[i]].state !== "close_prepared") continue;
299
+ var session = sessionsList.find(function (candidate) { return stableOrigin(candidate) === records[keys[i]].workerOriginId && candidate.ownerId === records[keys[i]].ownerId; });
300
+ if (session) results.push({ key: keys[i], result: recover(keys[i], session) });
301
+ }
302
+ return results;
303
+ }
304
+ function markBlocked(key, reason, code) {
305
+ var record = records[key];
306
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
307
+ var nextRecords = Object.assign({}, records), next = copy(record);
308
+ next.state = "blocked";
309
+ next.blockedReason = String(reason || "Delivery is blocked");
310
+ next.blockedCode = code || null;
311
+ if (code === "human_stop" && record.blockedCode !== "human_stop") {
312
+ next.blockedDeliveryState = record.deliveryState;
313
+ next.blockedAttemptId = record.attemptId || null;
314
+ }
315
+ nextRecords[key] = next;
316
+ var saved = persist(nextRecords);
317
+ if (!saved.ok) return { ok: false, error: saved.error, record: copy(record) };
318
+ records = nextRecords;
319
+ return { ok: true, record: copy(next) };
320
+ }
321
+ function markFinalized(key) {
322
+ var record = records[key];
323
+ if (!record) return { ok: false, error: "pair result outbox record was not found" };
324
+ if (record.finalized) return { ok: true, duplicate: true, record: copy(record) };
325
+ return transition(key, { finalized: true, finalizedAt: Date.now(), finalizationState: "complete", finalizationError: null });
326
+ }
327
+ function markFinalizationFailure(key, reason) {
328
+ return transition(key, { finalizationState: "failed", finalizationError: String(reason || "Result finalization failed") });
329
+ }
330
+ function markRestartUncertain(key) {
331
+ var record = records[key];
332
+ if (!record || record.deliveryState !== "attempting") return { ok: true, stale: true, record: copy(record) };
333
+ return transitionAttempt(key, record.attemptId, ["attempting"], {
334
+ deliveryState: "uncertain", deliveryError: "Delivery was interrupted before acceptance could be confirmed",
335
+ });
336
+ }
337
+ function authorizeHumanTurn(key) {
338
+ var record = records[key];
339
+ if (!record || record.blockedCode !== "human_stop") return { ok: false, stale: true, record: copy(record) };
340
+ var deliveryState = record.blockedDeliveryState || record.deliveryState;
341
+ if (deliveryState === "attempting") deliveryState = "uncertain";
342
+ return transition(key, { state: record.outcome ? "captured" : "dispatching", blockedReason: null, blockedCode: null,
343
+ blockedDeliveryState: null, blockedAttemptId: null, deliveryState: deliveryState });
344
+ }
345
+ function grantManualRetry(key) {
346
+ var record = records[key];
347
+ if (!record || !record.outcome || record.deliveryRoute !== "callback") return { ok: false, error: "completed result is unavailable" };
348
+ if (record.blockedCode === "human_stop") return { ok: false, blocked: true, error: record.blockedReason };
349
+ var exhausted = record.deliveryState === "pending" && Number(record.deliveryAttemptCount || 0) >= MAX_DELIVERY_ATTEMPTS;
350
+ if (record.deliveryState !== "uncertain" && !exhausted) return { ok: false, error: "result is not eligible for manual retry" };
351
+ if (Number(record.manualRetryCount || 0) >= 1) return { ok: false, exhausted: true, error: "manual retry was already used" };
352
+ return transition(key, { state: "captured", blockedReason: null, blockedCode: null, deliveryState: "pending",
353
+ deliveryAttemptCount: 0, attemptId: null, deliveryError: null, manualRetryCount: Number(record.manualRetryCount || 0) + 1,
354
+ manualRetryEpoch: Number(record.manualRetryEpoch || 0) + 1 });
355
+ }
356
+ function persistedRecords() {
357
+ var parsed;
358
+ try { parsed = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { return []; }
359
+ var source = parsed && parsed.records && typeof parsed.records === "object" ? parsed.records : {};
360
+ return Object.keys(source).map(function (key) {
361
+ var record = copy(source[key]);
362
+ record.deliveryState = record.deliveryState || "pending";
363
+ record.recoveryState = record.state === "dispatching" || record.state === "close_prepared" || record.deliveryState === "attempting" || record.deliveryState === "uncertain" ? "uncertain" : "known";
364
+ record.blockedReason = record.blockedReason || null;
365
+ return record;
366
+ });
367
+ }
368
+ function status() {
369
+ return { records: persistedRecords(), persistenceError: lastPersistenceError };
370
+ }
371
+ read();
372
+ return {
373
+ begin: begin,
374
+ capture: capture,
375
+ prepareCapture: prepareCapture,
376
+ finalizePreparedCapture: finalizePreparedCapture,
377
+ rollbackPreparedCapture: rollbackPreparedCapture,
378
+ beginDelivery: beginDelivery,
379
+ markTranscript: markTranscript,
380
+ markDelivery: markDelivery,
381
+ setRoute: setRoute,
382
+ recover: recover,
383
+ recoverPending: recoverPending,
384
+ markBlocked: markBlocked,
385
+ markFinalized: markFinalized,
386
+ markFinalizationFailure: markFinalizationFailure,
387
+ markRestartUncertain: markRestartUncertain,
388
+ authorizeHumanTurn: authorizeHumanTurn,
389
+ grantManualRetry: grantManualRetry,
390
+ status: status,
391
+ list: function () { return Object.keys(records).map(function (key) { return copy(records[key]); }); },
392
+ get: function (key) { return records[key] ? copy(records[key]) : null; },
393
+ key: resultKey,
394
+ stableOrigin: stableOrigin,
395
+ maxCaptureAttempts: MAX_CAPTURE_ATTEMPTS,
396
+ maxDeliveryAttempts: MAX_DELIVERY_ATTEMPTS,
397
+ };
398
+ }
399
+
400
+ module.exports = { attachPairResultOutbox: attachPairResultOutbox, resultKey: resultKey, stableOrigin: stableOrigin };
@@ -0,0 +1,16 @@
1
+ var attachPairResultCapture = require("./project-pair-result-capture").attachPairResultCapture;
2
+ var attachPairResultDelivery = require("./project-pair-result-delivery").attachPairResultDelivery;
3
+
4
+ function attachPairResultPipeline(ctx) {
5
+ var capture;
6
+ var delivery = attachPairResultDelivery({ sm: ctx.sm, store: ctx.store, outbox: ctx.resultOutbox, getSdk: ctx.getSdk,
7
+ getLinuxUserForSession: ctx.getLinuxUserForSession, acceptanceTimeoutMs: ctx.acceptanceTimeoutMs,
8
+ blockedReason: ctx.blockedReason,
9
+ onStateChange: ctx.onStateChange,
10
+ finish: function (caller, partner, token, delivered) { return capture.finish(caller, partner, token, delivered); } });
11
+ capture = attachPairResultCapture({ sm: ctx.sm, store: ctx.store, resultOutbox: ctx.resultOutbox, delivery: delivery, finishDelegation: ctx.finishDelegation, onStateChange: ctx.onStateChange,
12
+ resumeDriverWithResult: ctx.resumeDriverWithResult, onPartnerResult: ctx.onPartnerResult, drain: ctx.drain });
13
+ return { capture: capture, delivery: delivery };
14
+ }
15
+
16
+ module.exports = { attachPairResultPipeline: attachPairResultPipeline };