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.
- package/lib/daemon-project-access.js +118 -0
- package/lib/daemon-projects.js +19 -5
- package/lib/daemon.js +19 -18
- package/lib/project-logs-access-scope.js +94 -0
- package/lib/project-logs-query.js +3 -0
- package/lib/project-logs-service.js +86 -84
- package/lib/project-logs-store.js +5 -3
- package/lib/project-pair-lifecycle.js +140 -102
- package/lib/project-pair-message.js +50 -0
- package/lib/project-pair-replacement-state.js +72 -0
- package/lib/project-pair-task-control.js +264 -0
- package/lib/project-pair-usage.js +76 -0
- package/lib/project-session-pair.js +115 -116
- package/lib/project-sessions.js +25 -1
- package/lib/project-worker-proposal.js +69 -58
- package/lib/project.js +21 -4
- package/lib/public/css/icon-strip.css +40 -0
- package/lib/public/modules/app-messages.js +9 -0
- package/lib/public/modules/project-access.js +141 -0
- package/lib/public/modules/project-grouping.js +22 -0
- package/lib/public/modules/sidebar-projects.js +14 -171
- package/lib/public/modules/worker-proposal.js +4 -0
- package/lib/sdk-bridge.js +20 -0
- package/lib/server-admin.js +3 -3
- package/lib/server-global-ws.js +10 -0
- package/lib/server-mates.js +4 -3
- package/lib/server-palette.js +17 -14
- package/lib/server.js +184 -102
- package/lib/session-pair-factory.js +3 -0
- package/lib/session-pair-history.js +40 -0
- package/lib/session-pair-mcp-server.js +19 -2
- package/lib/session-pair-operation.js +11 -0
- package/lib/session-pair-prompts.js +24 -2
- package/lib/session-pair-turn-control.js +8 -3
- package/lib/session-provenance.js +1 -0
- package/lib/session-spawn-mcp-server.js +2 -1
- package/lib/sessions.js +2 -0
- package/lib/worker-proposal-control.js +69 -0
- package/lib/worker-proposal-decision.js +7 -0
- package/lib/worker-runtime-catalog.js +52 -0
- package/lib/workspace-query-access.js +3 -2
- package/lib/workspace-query-service.js +1 -1
- package/lib/worktree.js +8 -4
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/adapters/codex.js +25 -7
- 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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
161
|
-
var
|
|
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].
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
|
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 };
|