clay-server 4.0.1-beta.1 → 4.1.0-beta.2

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 (46) hide show
  1. package/lib/daemon-project-access.js +118 -0
  2. package/lib/daemon-projects.js +19 -5
  3. package/lib/daemon.js +19 -18
  4. package/lib/project-logs-access-scope.js +94 -0
  5. package/lib/project-logs-query.js +3 -0
  6. package/lib/project-logs-service.js +86 -84
  7. package/lib/project-logs-store.js +5 -3
  8. package/lib/project-pair-lifecycle.js +140 -102
  9. package/lib/project-pair-message.js +50 -0
  10. package/lib/project-pair-replacement-state.js +72 -0
  11. package/lib/project-pair-task-control.js +264 -0
  12. package/lib/project-pair-usage.js +76 -0
  13. package/lib/project-session-pair.js +115 -116
  14. package/lib/project-sessions.js +25 -1
  15. package/lib/project-worker-proposal.js +69 -58
  16. package/lib/project.js +21 -4
  17. package/lib/public/css/icon-strip.css +40 -0
  18. package/lib/public/modules/app-messages.js +9 -0
  19. package/lib/public/modules/project-access.js +141 -0
  20. package/lib/public/modules/project-grouping.js +22 -0
  21. package/lib/public/modules/sidebar-projects.js +14 -171
  22. package/lib/public/modules/worker-proposal.js +4 -0
  23. package/lib/sdk-bridge.js +20 -0
  24. package/lib/server-admin.js +3 -3
  25. package/lib/server-global-ws.js +10 -0
  26. package/lib/server-mates.js +4 -3
  27. package/lib/server-palette.js +17 -14
  28. package/lib/server.js +184 -102
  29. package/lib/session-pair-factory.js +3 -0
  30. package/lib/session-pair-history.js +40 -0
  31. package/lib/session-pair-mcp-server.js +19 -2
  32. package/lib/session-pair-operation.js +11 -0
  33. package/lib/session-pair-prompts.js +24 -2
  34. package/lib/session-pair-turn-control.js +8 -3
  35. package/lib/session-provenance.js +1 -0
  36. package/lib/session-spawn-mcp-server.js +2 -1
  37. package/lib/sessions.js +2 -0
  38. package/lib/worker-proposal-control.js +69 -0
  39. package/lib/worker-proposal-decision.js +7 -0
  40. package/lib/worker-runtime-catalog.js +52 -0
  41. package/lib/workspace-query-access.js +3 -2
  42. package/lib/workspace-query-service.js +1 -1
  43. package/lib/worktree.js +8 -4
  44. package/lib/ws-schema.js +1 -0
  45. package/lib/yoke/adapters/codex.js +25 -7
  46. package/package.json +1 -1
@@ -341,11 +341,12 @@ function createProjectLogsStore(opts) {
341
341
  // revision that changes category updates the vocabulary without rewriting a
342
342
  // single historical record. A shared project shares this list because it
343
343
  // shares this store.
344
- function categories() {
344
+ function categories(options) {
345
345
  var seen = {};
346
346
  var out = [];
347
347
  var live = entries(false);
348
348
  for (var i = 0; i < live.length; i++) {
349
+ if (!logsQuery.matchesContext(live[i], options || {})) continue;
349
350
  var value = live[i].category;
350
351
  if (!value || seen[value]) continue;
351
352
  seen[value] = true;
@@ -382,6 +383,7 @@ function createProjectLogsStore(opts) {
382
383
  var out = [];
383
384
  var total = 0;
384
385
  for (var i = 0; i < live.length; i++) {
386
+ if (!logsQuery.matchesContext(live[i], options2)) continue;
385
387
  var awaiting = logsComments.pendingReviewComments(live[i].comments);
386
388
  for (var j = 0; j < awaiting.length; j++) {
387
389
  total++;
@@ -408,13 +410,13 @@ function createProjectLogsStore(opts) {
408
410
  function list(args) {
409
411
  var options2 = args || {};
410
412
  var result = logsQuery.listEntries(entries(options2.includeDeleted === true), options2, cleanLine, validateKind);
411
- result.categories = categories();
413
+ result.categories = categories(options2);
412
414
  return result;
413
415
  }
414
416
 
415
417
  function search(args) {
416
418
  var result = logsQuery.searchEntries(entries(false), args || {}, cleanLine, validateKind);
417
- result.categories = categories();
419
+ result.categories = categories(args || {});
418
420
  return result;
419
421
  }
420
422
 
@@ -14,6 +14,10 @@
14
14
  // into, and inventing a destructive one would lose work.
15
15
 
16
16
  var eligibility = require("./session-driver-eligibility");
17
+ var replacementState = require("./project-pair-replacement-state");
18
+ var pairUsage = require("./project-pair-usage");
19
+ var contextStatus = pairUsage.contextStatus;
20
+ var firstNumber = pairUsage.firstNumber;
17
21
 
18
22
  var MAX_GENERATIONS = 5;
19
23
  var MAX_NOTE_CHARS = 400;
@@ -38,58 +42,10 @@ function clampText(value, max) {
38
42
  return text;
39
43
  }
40
44
 
41
- function firstNumber() {
42
- for (var i = 0; i < arguments.length; i++) {
43
- var value = arguments[i];
44
- if (typeof value === "number" && isFinite(value) && value >= 0) return value;
45
- }
46
- return null;
47
- }
48
-
49
- // The Worker's context position, from authoritative session accounting only:
50
- // the SDK's own getContextUsage snapshot when the adapter provides one, else
51
- // the last result message's usage totals. Field names differ across adapters,
52
- // so each candidate is probed and `source` states which reading was used.
53
- // Never estimated from history text.
54
- function contextStatus(session) {
55
- var snapshot = session.lastContextUsage || null;
56
- var used = null;
57
- var window = null;
58
- var source = "unavailable";
59
-
60
- if (snapshot && typeof snapshot === "object") {
61
- used = firstNumber(snapshot.totalTokens, snapshot.usedTokens, snapshot.tokens, snapshot.inputTokens);
62
- window = firstNumber(snapshot.contextWindow, snapshot.maxTokens, snapshot.windowSize, snapshot.limit);
63
- if (used !== null || window !== null) source = "sdk_context_usage";
64
- }
65
-
66
- if (used === null) {
67
- var history = session.history || [];
68
- for (var i = history.length - 1; i >= 0; i--) {
69
- if (!history[i] || history[i].type !== "result") continue;
70
- var usage = history[i].usage || null;
71
- if (!usage) break;
72
- var input = firstNumber(usage.input_tokens, usage.inputTokens) || 0;
73
- var cacheRead = firstNumber(usage.cache_read_input_tokens, usage.cacheReadInputTokens) || 0;
74
- var cacheWrite = firstNumber(usage.cache_creation_input_tokens, usage.cacheCreationInputTokens) || 0;
75
- var output = firstNumber(usage.output_tokens, usage.outputTokens) || 0;
76
- used = input + cacheRead + cacheWrite + output;
77
- source = "last_result_usage";
78
- break;
79
- }
80
- }
81
-
82
- var ratio = null;
83
- if (used !== null && window !== null && window > 0) {
84
- ratio = Math.round((used / window) * 1000) / 1000;
85
- if (ratio > 1) ratio = 1;
86
- }
87
- return {
88
- source: source,
89
- usedTokens: used,
90
- contextWindow: window,
91
- usedRatio: ratio,
92
- };
45
+ function replacementFingerprint(args, worker) {
46
+ return JSON.stringify({ sourceWorkerId: worker.localId, interrupt: args.interrupt === true,
47
+ vendor: args.workerVendor || "", model: args.workerModel || "", effort: args.workerEffort || "",
48
+ message: args.message || "", evaluation: args.evaluation || null });
93
49
  }
94
50
 
95
51
  function continuityStatus(session) {
@@ -119,6 +75,8 @@ function activityStatus(session) {
119
75
  isProcessing: !!(session.isProcessing || session._queryStarting),
120
76
  delegated: !!token,
121
77
  currentTask: token ? clampText(token.message, MAX_TASK_PREVIEW_CHARS) : "",
78
+ currentTaskId: token && token.taskId || null,
79
+ interruption: session._pairInterruption || null,
122
80
  lastTurnInterrupted: !!session._lastTurnInterrupted,
123
81
  };
124
82
  }
@@ -128,22 +86,47 @@ function attachPairLifecycle(ctx) {
128
86
  var store = ctx.splitStore;
129
87
  var turnControl = ctx.turnControl;
130
88
 
131
- // Per-Driver ledger of the Worker generations it has run, newest last.
132
- // Lives on the live Driver session object: it informs the Driver's next
133
- // choice within this session and is deliberately not persisted, because a
134
- // bounded observation of a session that no longer exists would only be
135
- // misleading after a restart.
136
89
  function ledgerFor(driver) {
137
90
  if (!Array.isArray(driver._workerGenerations)) driver._workerGenerations = [];
91
+ var group = store.groupForMember(driver.localId);
92
+ var worker = group && group.pair && sm.sessions.get(group.pair.workerId);
93
+ var generation = worker && (worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation);
94
+ if (worker && Number.isInteger(generation) && generation > 0 && !findGeneration(driver, worker)) {
95
+ driver._workerGenerations.push({
96
+ generation: generation,
97
+ workerSessionId: worker.localId,
98
+ workerOriginId: worker.sessionOriginId || null,
99
+ vendor: worker.vendor || null,
100
+ model: worker.model || null,
101
+ effort: worker.effort || null,
102
+ startedAt: worker.sessionProvenance && worker.sessionProvenance.createdAt || Date.now(),
103
+ endedAt: null,
104
+ observed: null,
105
+ evaluation: null,
106
+ });
107
+ while (driver._workerGenerations.length > MAX_GENERATIONS) driver._workerGenerations.shift();
108
+ }
138
109
  return driver._workerGenerations;
139
110
  }
140
-
141
111
  function recordGenerationStart(driver, worker) {
142
112
  var ledger = ledgerFor(driver);
143
- var generation = ledger.length + 1;
113
+ var existing = findGeneration(driver, worker);
114
+ if (existing && !existing.endedAt) {
115
+ worker._pairGeneration = existing.generation;
116
+ return existing.generation;
117
+ }
118
+ var generation = 1;
119
+ for (var i = 0; i < ledger.length; i++) {
120
+ if (Number.isInteger(ledger[i].generation) && ledger[i].generation >= generation) generation = ledger[i].generation + 1;
121
+ }
122
+ if (worker.sessionProvenance && Number.isInteger(worker.sessionProvenance.generation) &&
123
+ worker.sessionProvenance.generation >= generation) {
124
+ generation = worker.sessionProvenance.generation;
125
+ }
144
126
  ledger.push({
145
127
  generation: generation,
146
128
  workerSessionId: worker.localId,
129
+ workerOriginId: worker.sessionOriginId || null,
147
130
  vendor: worker.vendor || null,
148
131
  model: worker.model || null,
149
132
  effort: worker.effort || null,
@@ -154,21 +137,38 @@ function attachPairLifecycle(ctx) {
154
137
  });
155
138
  while (ledger.length > MAX_GENERATIONS) ledger.shift();
156
139
  worker._pairGeneration = generation;
140
+ if (worker.sessionProvenance) worker.sessionProvenance.generation = generation;
141
+ if (typeof sm.saveSessionFile === "function") {
142
+ sm.saveSessionFile(driver);
143
+ sm.saveSessionFile(worker);
144
+ }
157
145
  return generation;
158
146
  }
159
-
160
- function findGeneration(driver, workerSessionId) {
161
- var ledger = ledgerFor(driver);
147
+ function findGeneration(driver, worker) {
148
+ var ledger = Array.isArray(driver._workerGenerations) ? driver._workerGenerations : [];
149
+ var originId = worker && worker.sessionOriginId || null;
162
150
  for (var i = ledger.length - 1; i >= 0; i--) {
163
- if (ledger[i].workerSessionId === workerSessionId) return ledger[i];
151
+ if (originId && ledger[i].workerOriginId === originId) {
152
+ ledger[i].workerSessionId = worker.localId;
153
+ return ledger[i];
154
+ }
164
155
  }
165
- return null;
156
+ if (!worker || !originId) return null;
157
+ var generation = worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation;
158
+ if (!Number.isInteger(generation) || generation < 1) return null;
159
+ var candidates = [];
160
+ for (var j = 0; j < ledger.length; j++) {
161
+ if (!ledger[j].workerOriginId && ledger[j].generation === generation) candidates.push(ledger[j]);
162
+ }
163
+ if (candidates.length !== 1) return null;
164
+ candidates[0].workerOriginId = originId;
165
+ candidates[0].workerSessionId = worker.localId;
166
+ if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(driver);
167
+ return candidates[0];
166
168
  }
167
-
168
- // Objective signals the server already has. Recorded when a generation ends
169
- // so the Driver's next decision can use them without a transcript.
170
169
  function closeGeneration(driver, worker) {
171
- var record = findGeneration(driver, worker.localId);
170
+ ledgerFor(driver);
171
+ var record = findGeneration(driver, worker);
172
172
  if (!record || record.endedAt) return record;
173
173
  var continuity = continuityStatus(worker);
174
174
  var context = contextStatus(worker);
@@ -177,13 +177,12 @@ function attachPairLifecycle(ctx) {
177
177
  userTurns: continuity.userTurns,
178
178
  errorEntries: continuity.errorEntries,
179
179
  interrupted: !!worker._lastTurnInterrupted,
180
- usedTokens: context.usedTokens,
181
- usedRatio: context.usedRatio,
180
+ usedTokens: context.current.usedTokens,
181
+ usedRatio: context.current.usedRatio,
182
182
  };
183
+ if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(driver);
183
184
  return record;
184
185
  }
185
-
186
- // The exact pair, with the Driver's structural capability re-checked on every call.
187
186
  function resolveDriverPair(caller) {
188
187
  if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
189
188
  // Exact live object identity, before anything is read off the caller. A
@@ -211,12 +210,11 @@ function attachPairLifecycle(ctx) {
211
210
  return null;
212
211
  }
213
212
 
214
- // Bounded status for the reuse-vs-replace decision. Exact pair only; no
215
- // other user's data and no transcript.
216
213
  function partnerStatus(caller) {
217
214
  var resolved = resolveDriverPair(caller);
218
215
  var worker = resolved.worker;
219
216
  var blocked = turnControl.blockedReason(caller) || replaceBlockedReason(worker);
217
+ var resumeBlocked = turnControl.blockedReason(caller) || (worker.isProcessing || worker._queryStarting ? "the Split Worker is mid-turn" : (!worker._pairInterruption ? "the Split Worker has no interrupted task" : null));
220
218
  var ledger = ledgerFor(caller).map(function (record) {
221
219
  return {
222
220
  generation: record.generation,
@@ -228,6 +226,23 @@ function attachPairLifecycle(ctx) {
228
226
  };
229
227
  });
230
228
  return {
229
+ identity: {
230
+ sessionId: worker.localId,
231
+ originId: worker.sessionOriginId || null,
232
+ kind: worker.sessionProvenance && worker.sessionProvenance.kind || "worker",
233
+ generation: worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation || null,
234
+ },
235
+ configuration: {
236
+ vendor: worker.vendor || null,
237
+ model: worker.model || null,
238
+ effort: worker.effort || null,
239
+ permissionMode: worker.permissionMode || null,
240
+ },
241
+ time: {
242
+ observedAt: Date.now(),
243
+ createdAt: worker.sessionProvenance && worker.sessionProvenance.createdAt || null,
244
+ lastActivityAt: firstNumber(worker.lastActivityAt, worker.lastActivity),
245
+ },
231
246
  worker: {
232
247
  sessionId: worker.localId,
233
248
  title: worker.title || "New Session",
@@ -242,59 +257,84 @@ function attachPairLifecycle(ctx) {
242
257
  orchestration: turnControl.status(caller),
243
258
  replaceSafe: !blocked,
244
259
  replaceBlockedReason: blocked,
260
+ canReplace: !blocked,
261
+ canReplaceBlockedReason: blocked,
262
+ canResume: !resumeBlocked,
263
+ resumeBlockedReason: resumeBlocked,
264
+ tasks: ctx.taskStatus ? ctx.taskStatus(worker) : null,
265
+ replacement: caller._lastPairReplacement || null,
266
+ proposal: ctx.proposalStatus ? ctx.proposalStatus(caller) : null,
245
267
  generations: ledger,
246
268
  };
247
269
  }
248
270
 
249
- // Transactional replacement. Rejects an active Worker unless interrupt is
250
- // explicitly true, cancels anything the old Worker was waiting on, dissolves
251
- // the exact pair without deleting history, and creates a fresh Worker.
252
- // Idempotent in the sense that it either completes or leaves the existing
253
- // pair untouched; it never half-dissolves.
254
271
  function replacePartner(args, caller) {
255
272
  var resolved = resolveDriverPair(caller);
256
273
  var group = resolved.group;
257
274
  var oldWorker = resolved.worker;
275
+ ledgerFor(caller);
258
276
  turnControl.assertWorkerAction(caller);
277
+ var replacementEntry = replacementState.begin(caller, args.transactionId, oldWorker, replacementFingerprint(args, oldWorker));
278
+ var replay = replacementState.replayValue(replacementEntry);
279
+ if (replay) return Promise.resolve(replay);
259
280
 
260
281
  // Validate the replacement while the old pair is still fully intact. An
261
282
  // uninstalled vendor, an unavailable model or an unsupported effort must
262
283
  // never cost the user their running Worker, so this precedes the interrupt,
263
284
  // the permission cancellation and the dissolve.
264
- ctx.preflightWorkerForDriver(caller, {
265
- workerVendor: args.workerVendor,
266
- workerModel: args.workerModel,
267
- workerEffort: args.workerEffort,
268
- });
285
+ try {
286
+ ctx.preflightWorkerForDriver(caller, {
287
+ workerVendor: args.workerVendor,
288
+ workerModel: args.workerModel,
289
+ workerEffort: args.workerEffort,
290
+ });
291
+ } catch (preflightError) {
292
+ replacementState.fail(replacementEntry.transaction, "preflight_failed", preflightError, "not_required");
293
+ throw preflightError;
294
+ }
269
295
 
270
296
  var blocked = replaceBlockedReason(oldWorker);
271
297
  if (blocked && args.interrupt !== true) {
298
+ replacementState.fail(replacementEntry.transaction, "preflight_failed", new Error(blocked), "not_required");
272
299
  throw new Error("cannot replace the Split Worker because " + blocked +
273
300
  "; call again with interrupt set to true to stop it first");
274
301
  }
275
302
 
276
- if (args.evaluation) validateEvaluation(args.evaluation);
277
- var creationTicket = turnControl.reserveCreation(caller, "replace");
303
+ var creationTicket;
304
+ try {
305
+ if (args.evaluation) validateEvaluation(args.evaluation);
306
+ creationTicket = turnControl.reserveCreation(caller, "replace");
307
+ } catch (validationError) {
308
+ replacementState.fail(replacementEntry.transaction, "preflight_failed", validationError, "not_required");
309
+ throw validationError;
310
+ }
278
311
 
279
312
  if (blocked) {
313
+ replacementState.stage(replacementEntry.transaction, "stopping_source");
314
+ replacementEntry.transaction.sourceStopped = true;
315
+ if (ctx.markInterruption) ctx.markInterruption(oldWorker, "driver", "The Driver accepted replacement of this Worker generation.");
280
316
  oldWorker.taskStopRequested = true;
281
317
  if (oldWorker.abortController) {
282
318
  try { oldWorker.abortController.abort(); } catch (e) {}
283
319
  }
284
320
  if (oldWorker._pairDelegation && typeof ctx.finishDelegation === "function") {
321
+ if (typeof ctx.completeInterruptedTask === "function") ctx.completeInterruptedTask(oldWorker, caller, oldWorker._pairDelegation);
285
322
  ctx.finishDelegation(group, caller, oldWorker, oldWorker._pairDelegation);
286
323
  }
287
324
  }
288
325
 
289
326
  // Any permission decision the old Worker was waiting on dies with the pair.
327
+ replacementState.stage(replacementEntry.transaction, "cancelling_source_waits");
290
328
  if (typeof ctx.cancelWorkerPermissions === "function") {
291
329
  ctx.cancelWorkerPermissions(oldWorker, "The Driver replaced this Split Worker.");
292
330
  }
293
331
 
294
332
  var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
333
+ replacementState.stage(replacementEntry.transaction, "dissolving_source_pair");
295
334
  var dissolved = store.dissolve(ws, { id: group.id });
296
335
  if (!dissolved.ok) {
297
336
  turnControl.releaseCreation(creationTicket);
337
+ replacementState.fail(replacementEntry.transaction, "dissolve_failed", new Error(dissolved.error || "could not dissolve"), "not_required");
298
338
  throw new Error(dissolved.error || "could not dissolve the existing pair");
299
339
  }
300
340
 
@@ -316,6 +356,7 @@ function attachPairLifecycle(ctx) {
316
356
  // come back. The error says so rather than implying a clean restore.
317
357
  var created;
318
358
  try {
359
+ replacementState.stage(replacementEntry.transaction, "creating_target");
319
360
  created = ctx.createWorkerForDriver(caller, {
320
361
  workerVendor: typeof args.workerVendor === "string" ? args.workerVendor : "",
321
362
  workerModel: typeof args.workerModel === "string" ? args.workerModel : "",
@@ -333,17 +374,22 @@ function attachPairLifecycle(ctx) {
333
374
  ? " Its interrupted turn cannot be resumed, because stopping it was explicitly requested."
334
375
  : "";
335
376
  turnControl.releaseCreation(creationTicket);
377
+ replacementState.fail(replacementEntry.transaction, "creation_failed", e, restored && restored.ok ? "source_pair_restored" : "source_pair_restore_failed");
336
378
  throw new Error("could not create the replacement Split Worker: " + (e.message || String(e)) +
337
379
  ". " + restoreNote + interruptNote);
338
380
  }
339
381
  var closed = closeGeneration(caller, oldWorker);
340
- if (closed && args.evaluation) applyEvaluation(closed, args.evaluation);
382
+ if (closed && args.evaluation) {
383
+ applyEvaluation(closed, args.evaluation);
384
+ if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(caller);
385
+ }
341
386
  var generation = recordGenerationStart(caller, created.worker);
342
387
 
343
388
  var result = {
344
389
  status: "replaced",
345
390
  previousWorkerSessionId: oldWorker.localId,
346
391
  previousWorkerHistoryPreserved: true,
392
+ previousWorkerFilesPreserved: true,
347
393
  previousGeneration: closed ? closed.generation : null,
348
394
  interrupted: !!blocked,
349
395
  workerSessionId: created.worker.localId,
@@ -351,7 +397,9 @@ function attachPairLifecycle(ctx) {
351
397
  vendor: created.worker.vendor || null,
352
398
  model: created.worker.model || null,
353
399
  effort: created.worker.effort || null,
400
+ transactionId: replacementEntry.transaction.transactionId,
354
401
  };
402
+ replacementState.complete(replacementEntry.transaction, result);
355
403
 
356
404
  var message = typeof args.message === "string" ? args.message.trim() : "";
357
405
  if (!message) return Promise.resolve(result);
@@ -362,17 +410,15 @@ function attachPairLifecycle(ctx) {
362
410
  });
363
411
  }
364
412
 
365
- // Shape check only; writes nothing. Lets a caller that is about to destroy
366
- // something reject a malformed assessment first.
413
+ // No global or cross-user ranking is formed from Worker evaluations.
367
414
  function validateEvaluation(raw) {
368
- var input = raw && typeof raw === "object" ? raw : {};
415
+ var input = typeof raw === "string" ? { outcome: raw } : (raw && typeof raw === "object" ? raw : {});
369
416
  var outcome = typeof input.outcome === "string" ? input.outcome.trim().toLowerCase() : "";
370
417
  if (EVALUATION_OUTCOMES.indexOf(outcome) === -1) {
371
418
  throw new Error('evaluation outcome must be one of: ' + EVALUATION_OUTCOMES.join(", "));
372
419
  }
373
420
  return { outcome: outcome, note: clampText(input.note, MAX_NOTE_CHARS) };
374
421
  }
375
-
376
422
  function applyEvaluation(record, raw) {
377
423
  var clean = validateEvaluation(raw);
378
424
  record.evaluation = {
@@ -383,23 +429,20 @@ function attachPairLifecycle(ctx) {
383
429
  return record.evaluation;
384
430
  }
385
431
 
386
- // Attach a bounded assessment to one exact Worker generation. The Driver
387
- // supplies the judgement; the server supplies the objective observations and
388
- // refuses anything outside the enum. No global or cross-user ranking is
389
- // formed from this.
390
432
  function recordEvaluation(args, caller) {
391
433
  var resolved = resolveDriverPair(caller);
434
+ var ledger = ledgerFor(caller);
392
435
  var target = Number.isInteger(args.generation)
393
436
  ? (function () {
394
- var ledger = ledgerFor(caller);
395
437
  for (var i = 0; i < ledger.length; i++) {
396
438
  if (ledger[i].generation === args.generation) return ledger[i];
397
439
  }
398
440
  return null;
399
441
  })()
400
- : findGeneration(caller, resolved.worker.localId);
442
+ : findGeneration(caller, resolved.worker);
401
443
  if (!target) throw new Error("no such Split Worker generation for this Driver");
402
444
  var evaluation = applyEvaluation(target, args);
445
+ if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(caller);
403
446
  return {
404
447
  status: "recorded",
405
448
  generation: target.generation,
@@ -410,11 +453,6 @@ function attachPairLifecycle(ctx) {
410
453
  };
411
454
  }
412
455
 
413
- // Tool handlers for the three lifecycle tools, with this module's own error
414
- // shaping. Kept here so the pair coordinator only wires names to handlers.
415
- // partnerStatus, or null when this session cannot legitimately ask for it.
416
- // Lets read_partner fold the capacity report in without duplicating the
417
- // guard chain or swallowing errors inline at the call site.
418
456
  function optionalStatus(boundSession) {
419
457
  try { return partnerStatus(boundSession); } catch (e) { return null; }
420
458
  }
@@ -0,0 +1,50 @@
1
+ var crypto = require("crypto");
2
+
3
+ function toolResult(value) {
4
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
5
+ }
6
+
7
+ function requestId(value) {
8
+ if (typeof value !== "string") return "pmsg_" + crypto.randomUUID();
9
+ var clean = value.trim();
10
+ if (!clean || clean.length > 120 || !/^[A-Za-z0-9._:-]+$/.test(clean)) return null;
11
+ return clean;
12
+ }
13
+
14
+ function attachPairMessage(ctx) {
15
+ function send(args, caller) {
16
+ var id = requestId(args.requestId);
17
+ if (!id) return toolResult({ status: "rejected", requestId: null, reason: "requestId is invalid" });
18
+ try {
19
+ ctx.turnControl.assertWorkerAction(caller);
20
+ var resolved = ctx.resolvePair(caller);
21
+ var worker = resolved.partner;
22
+ var message = typeof args.message === "string" ? args.message.trim() : "";
23
+ if (!message) return toolResult({ status: "rejected", requestId: id, partnerId: worker.localId, reason: "message is required" });
24
+ if (!worker.isProcessing && !worker._queryStarting) {
25
+ return toolResult({ status: "rejected", requestId: id, partnerId: worker.localId, reason: "the Split Worker has no active turn" });
26
+ }
27
+ var sdk = ctx.getSdk();
28
+ if (!sdk || !sdk.pushMessage(worker, message)) {
29
+ return toolResult({ status: "rejected", requestId: id, partnerId: worker.localId, reason: "the active Worker runtime did not accept the message" });
30
+ }
31
+ ctx.sm.sendAndRecord(worker, {
32
+ type: "partner_message",
33
+ text: message,
34
+ requestId: id,
35
+ delegatedBy: caller.localId,
36
+ });
37
+ worker.lastActivity = Date.now();
38
+ return toolResult({ status: "queued", requestId: id, partnerId: worker.localId, acknowledgement: "input_queue", runtimeState: worker._queryStarting ? "starting" : "active" });
39
+ } catch (err) {
40
+ return toolResult({ status: "rejected", requestId: id, reason: err.message || String(err) });
41
+ }
42
+ }
43
+
44
+ function fingerprint(args) {
45
+ return typeof args.message === "string" ? args.message.trim() : "";
46
+ }
47
+ return { fingerprint: fingerprint, send: send };
48
+ }
49
+
50
+ module.exports = { attachPairMessage: attachPairMessage };
@@ -0,0 +1,72 @@
1
+ var crypto = require("crypto");
2
+
3
+ function storeFor(driver) {
4
+ if (!driver._pairReplacementTransactions) driver._pairReplacementTransactions = Object.create(null);
5
+ return driver._pairReplacementTransactions;
6
+ }
7
+
8
+ function begin(driver, requestedId, worker, fingerprint) {
9
+ var id = typeof requestedId === "string" && requestedId.trim() ? requestedId.trim() : "replace_" + crypto.randomUUID();
10
+ var existing = storeFor(driver)[id];
11
+ if (existing) {
12
+ if (existing.fingerprint !== (fingerprint || "")) throw new Error("replacement transaction id was already used with different input");
13
+ return { transaction: existing, replay: true };
14
+ }
15
+ var transaction = {
16
+ transactionId: id,
17
+ status: "active",
18
+ stage: "preflight",
19
+ startedAt: Date.now(),
20
+ updatedAt: Date.now(),
21
+ sourceWorkerId: worker.localId,
22
+ sourceGeneration: worker._pairGeneration || worker.sessionProvenance && worker.sessionProvenance.generation || null,
23
+ sourceTurnState: worker.isProcessing || worker._queryStarting ? "running" : "idle",
24
+ sourceStopped: false,
25
+ sourcePairRestored: null,
26
+ filesPreserved: true,
27
+ fingerprint: fingerprint || "",
28
+ targetWorkerId: null,
29
+ targetGeneration: null,
30
+ failure: null,
31
+ result: null,
32
+ };
33
+ storeFor(driver)[id] = transaction;
34
+ driver._lastPairReplacement = transaction;
35
+ return { transaction: transaction, replay: false };
36
+ }
37
+
38
+ function stage(transaction, value) {
39
+ transaction.stage = value;
40
+ transaction.updatedAt = Date.now();
41
+ return transaction;
42
+ }
43
+
44
+ function fail(transaction, stageName, error, rollback) {
45
+ transaction.status = "failed";
46
+ transaction.stage = stageName;
47
+ transaction.updatedAt = Date.now();
48
+ transaction.completedAt = transaction.updatedAt;
49
+ transaction.failure = { message: error && (error.message || String(error)) || "Unknown replacement failure", rollback: rollback || "not_required" };
50
+ transaction.sourcePairRestored = rollback === "source_pair_restored" ? true : (rollback === "source_pair_restore_failed" ? false : null);
51
+ return transaction;
52
+ }
53
+
54
+ function complete(transaction, result) {
55
+ transaction.status = "completed";
56
+ transaction.stage = "committed";
57
+ transaction.updatedAt = Date.now();
58
+ transaction.completedAt = transaction.updatedAt;
59
+ transaction.targetWorkerId = result.workerSessionId;
60
+ transaction.targetGeneration = result.generation;
61
+ transaction.result = result;
62
+ return transaction;
63
+ }
64
+
65
+ function replayValue(entry) {
66
+ if (!entry.replay) return null;
67
+ if (entry.transaction.status === "completed") return entry.transaction.result;
68
+ if (entry.transaction.status === "failed") throw new Error(entry.transaction.failure.message);
69
+ throw new Error("replacement transaction is already active at stage " + entry.transaction.stage);
70
+ }
71
+
72
+ module.exports = { begin: begin, complete: complete, fail: fail, replayValue: replayValue, stage: stage };