clay-server 4.1.0-beta.1 → 4.1.0-beta.3
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/ask-user-request-identity.js +23 -0
- package/lib/project-ask-user.js +4 -1
- package/lib/project-pair-lifecycle.js +69 -41
- package/lib/project-pair-usage.js +7 -4
- package/lib/project-worker-proposal.js +15 -1
- package/lib/public/app.js +1 -0
- package/lib/public/modules/ask-user-state.js +66 -0
- package/lib/public/modules/tools.js +16 -2
- package/lib/sdk-message-processor.js +19 -1
- package/lib/session-pair-mcp-server.js +7 -1
- package/lib/session-provenance.js +1 -0
- package/lib/session-spawn-mcp-server.js +2 -1
- package/lib/sessions.js +2 -0
- package/lib/yoke/adapters/codex.js +24 -10
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function canonicalize(value) {
|
|
2
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
3
|
+
if (!value || typeof value !== "object") return value;
|
|
4
|
+
var keys = Object.keys(value).sort();
|
|
5
|
+
var result = {};
|
|
6
|
+
for (var i = 0; i < keys.length; i++) result[keys[i]] = canonicalize(value[keys[i]]);
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sameAskUserRequest(event, id, input) {
|
|
11
|
+
return !!(event && event.type === "tool_executing" && event.name === "AskUserQuestion"
|
|
12
|
+
&& event.id === id && JSON.stringify(canonicalize(event.input || {})) === JSON.stringify(canonicalize(input || {})));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasRecordedAskUserRequest(history, id, input) {
|
|
16
|
+
if (!Array.isArray(history) || !id) return false;
|
|
17
|
+
for (var i = history.length - 1; i >= 0; i--) {
|
|
18
|
+
if (sameAskUserRequest(history[i], id, input)) return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { canonicalize: canonicalize, sameAskUserRequest: sameAskUserRequest, hasRecordedAskUserRequest: hasRecordedAskUserRequest };
|
package/lib/project-ask-user.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var userInput = require("./yoke/user-input");
|
|
2
|
+
var askUserIdentity = require("./ask-user-request-identity");
|
|
2
3
|
|
|
3
4
|
function attachAskUser(ctx) {
|
|
4
5
|
function isFirstDebateQuestion(session) {
|
|
@@ -41,7 +42,9 @@ function attachAskUser(ctx) {
|
|
|
41
42
|
if (boundSession.pendingAskUser && boundSession.pendingAskUser[request.id] === pending) delete boundSession.pendingAskUser[request.id];
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
if (!askUserIdentity.hasRecordedAskUserRequest(boundSession.history, request.id, input)) {
|
|
46
|
+
ctx.record(boundSession, { type: "tool_executing", id: request.id, name: "AskUserQuestion", input: input });
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function createHandler(boundSession) {
|
|
@@ -86,22 +86,47 @@ function attachPairLifecycle(ctx) {
|
|
|
86
86
|
var store = ctx.splitStore;
|
|
87
87
|
var turnControl = ctx.turnControl;
|
|
88
88
|
|
|
89
|
-
// Per-Driver ledger of the Worker generations it has run, newest last.
|
|
90
|
-
// Lives on the live Driver session object: it informs the Driver's next
|
|
91
|
-
// choice within this session and is deliberately not persisted, because a
|
|
92
|
-
// bounded observation of a session that no longer exists would only be
|
|
93
|
-
// misleading after a restart.
|
|
94
89
|
function ledgerFor(driver) {
|
|
95
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
|
+
}
|
|
96
109
|
return driver._workerGenerations;
|
|
97
110
|
}
|
|
98
|
-
|
|
99
111
|
function recordGenerationStart(driver, worker) {
|
|
100
112
|
var ledger = ledgerFor(driver);
|
|
101
|
-
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
|
+
}
|
|
102
126
|
ledger.push({
|
|
103
127
|
generation: generation,
|
|
104
128
|
workerSessionId: worker.localId,
|
|
129
|
+
workerOriginId: worker.sessionOriginId || null,
|
|
105
130
|
vendor: worker.vendor || null,
|
|
106
131
|
model: worker.model || null,
|
|
107
132
|
effort: worker.effort || null,
|
|
@@ -112,21 +137,38 @@ function attachPairLifecycle(ctx) {
|
|
|
112
137
|
});
|
|
113
138
|
while (ledger.length > MAX_GENERATIONS) ledger.shift();
|
|
114
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
|
+
}
|
|
115
145
|
return generation;
|
|
116
146
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
var
|
|
147
|
+
function findGeneration(driver, worker) {
|
|
148
|
+
var ledger = Array.isArray(driver._workerGenerations) ? driver._workerGenerations : [];
|
|
149
|
+
var originId = worker && worker.sessionOriginId || null;
|
|
120
150
|
for (var i = ledger.length - 1; i >= 0; i--) {
|
|
121
|
-
if (ledger[i].
|
|
151
|
+
if (originId && ledger[i].workerOriginId === originId) {
|
|
152
|
+
ledger[i].workerSessionId = worker.localId;
|
|
153
|
+
return ledger[i];
|
|
154
|
+
}
|
|
122
155
|
}
|
|
123
|
-
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];
|
|
124
168
|
}
|
|
125
|
-
|
|
126
|
-
// Objective signals the server already has. Recorded when a generation ends
|
|
127
|
-
// so the Driver's next decision can use them without a transcript.
|
|
128
169
|
function closeGeneration(driver, worker) {
|
|
129
|
-
|
|
170
|
+
ledgerFor(driver);
|
|
171
|
+
var record = findGeneration(driver, worker);
|
|
130
172
|
if (!record || record.endedAt) return record;
|
|
131
173
|
var continuity = continuityStatus(worker);
|
|
132
174
|
var context = contextStatus(worker);
|
|
@@ -138,10 +180,9 @@ function attachPairLifecycle(ctx) {
|
|
|
138
180
|
usedTokens: context.current.usedTokens,
|
|
139
181
|
usedRatio: context.current.usedRatio,
|
|
140
182
|
};
|
|
183
|
+
if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(driver);
|
|
141
184
|
return record;
|
|
142
185
|
}
|
|
143
|
-
|
|
144
|
-
// The exact pair, with the Driver's structural capability re-checked on every call.
|
|
145
186
|
function resolveDriverPair(caller) {
|
|
146
187
|
if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
|
|
147
188
|
// Exact live object identity, before anything is read off the caller. A
|
|
@@ -169,8 +210,6 @@ function attachPairLifecycle(ctx) {
|
|
|
169
210
|
return null;
|
|
170
211
|
}
|
|
171
212
|
|
|
172
|
-
// Bounded status for the reuse-vs-replace decision. Exact pair only; no
|
|
173
|
-
// other user's data and no transcript.
|
|
174
213
|
function partnerStatus(caller) {
|
|
175
214
|
var resolved = resolveDriverPair(caller);
|
|
176
215
|
var worker = resolved.worker;
|
|
@@ -229,15 +268,11 @@ function attachPairLifecycle(ctx) {
|
|
|
229
268
|
};
|
|
230
269
|
}
|
|
231
270
|
|
|
232
|
-
// Transactional replacement. Rejects an active Worker unless interrupt is
|
|
233
|
-
// explicitly true, cancels anything the old Worker was waiting on, dissolves
|
|
234
|
-
// the exact pair without deleting history, and creates a fresh Worker.
|
|
235
|
-
// Idempotent in the sense that it either completes or leaves the existing
|
|
236
|
-
// pair untouched; it never half-dissolves.
|
|
237
271
|
function replacePartner(args, caller) {
|
|
238
272
|
var resolved = resolveDriverPair(caller);
|
|
239
273
|
var group = resolved.group;
|
|
240
274
|
var oldWorker = resolved.worker;
|
|
275
|
+
ledgerFor(caller);
|
|
241
276
|
turnControl.assertWorkerAction(caller);
|
|
242
277
|
var replacementEntry = replacementState.begin(caller, args.transactionId, oldWorker, replacementFingerprint(args, oldWorker));
|
|
243
278
|
var replay = replacementState.replayValue(replacementEntry);
|
|
@@ -344,7 +379,10 @@ function attachPairLifecycle(ctx) {
|
|
|
344
379
|
". " + restoreNote + interruptNote);
|
|
345
380
|
}
|
|
346
381
|
var closed = closeGeneration(caller, oldWorker);
|
|
347
|
-
if (closed && args.evaluation)
|
|
382
|
+
if (closed && args.evaluation) {
|
|
383
|
+
applyEvaluation(closed, args.evaluation);
|
|
384
|
+
if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(caller);
|
|
385
|
+
}
|
|
348
386
|
var generation = recordGenerationStart(caller, created.worker);
|
|
349
387
|
|
|
350
388
|
var result = {
|
|
@@ -372,17 +410,15 @@ function attachPairLifecycle(ctx) {
|
|
|
372
410
|
});
|
|
373
411
|
}
|
|
374
412
|
|
|
375
|
-
//
|
|
376
|
-
// something reject a malformed assessment first.
|
|
413
|
+
// No global or cross-user ranking is formed from Worker evaluations.
|
|
377
414
|
function validateEvaluation(raw) {
|
|
378
|
-
var input = raw && typeof raw === "object" ? raw : {};
|
|
415
|
+
var input = typeof raw === "string" ? { outcome: raw } : (raw && typeof raw === "object" ? raw : {});
|
|
379
416
|
var outcome = typeof input.outcome === "string" ? input.outcome.trim().toLowerCase() : "";
|
|
380
417
|
if (EVALUATION_OUTCOMES.indexOf(outcome) === -1) {
|
|
381
418
|
throw new Error('evaluation outcome must be one of: ' + EVALUATION_OUTCOMES.join(", "));
|
|
382
419
|
}
|
|
383
420
|
return { outcome: outcome, note: clampText(input.note, MAX_NOTE_CHARS) };
|
|
384
421
|
}
|
|
385
|
-
|
|
386
422
|
function applyEvaluation(record, raw) {
|
|
387
423
|
var clean = validateEvaluation(raw);
|
|
388
424
|
record.evaluation = {
|
|
@@ -393,23 +429,20 @@ function attachPairLifecycle(ctx) {
|
|
|
393
429
|
return record.evaluation;
|
|
394
430
|
}
|
|
395
431
|
|
|
396
|
-
// Attach a bounded assessment to one exact Worker generation. The Driver
|
|
397
|
-
// supplies the judgement; the server supplies the objective observations and
|
|
398
|
-
// refuses anything outside the enum. No global or cross-user ranking is
|
|
399
|
-
// formed from this.
|
|
400
432
|
function recordEvaluation(args, caller) {
|
|
401
433
|
var resolved = resolveDriverPair(caller);
|
|
434
|
+
var ledger = ledgerFor(caller);
|
|
402
435
|
var target = Number.isInteger(args.generation)
|
|
403
436
|
? (function () {
|
|
404
|
-
var ledger = ledgerFor(caller);
|
|
405
437
|
for (var i = 0; i < ledger.length; i++) {
|
|
406
438
|
if (ledger[i].generation === args.generation) return ledger[i];
|
|
407
439
|
}
|
|
408
440
|
return null;
|
|
409
441
|
})()
|
|
410
|
-
: findGeneration(caller, resolved.worker
|
|
442
|
+
: findGeneration(caller, resolved.worker);
|
|
411
443
|
if (!target) throw new Error("no such Split Worker generation for this Driver");
|
|
412
444
|
var evaluation = applyEvaluation(target, args);
|
|
445
|
+
if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(caller);
|
|
413
446
|
return {
|
|
414
447
|
status: "recorded",
|
|
415
448
|
generation: target.generation,
|
|
@@ -420,11 +453,6 @@ function attachPairLifecycle(ctx) {
|
|
|
420
453
|
};
|
|
421
454
|
}
|
|
422
455
|
|
|
423
|
-
// Tool handlers for the three lifecycle tools, with this module's own error
|
|
424
|
-
// shaping. Kept here so the pair coordinator only wires names to handlers.
|
|
425
|
-
// partnerStatus, or null when this session cannot legitimately ask for it.
|
|
426
|
-
// Lets read_partner fold the capacity report in without duplicating the
|
|
427
|
-
// guard chain or swallowing errors inline at the call site.
|
|
428
456
|
function optionalStatus(boundSession) {
|
|
429
457
|
try { return partnerStatus(boundSession); } catch (e) { return null; }
|
|
430
458
|
}
|
|
@@ -52,14 +52,17 @@ function contextStatus(session) {
|
|
|
52
52
|
if (input !== null) { cumulativeInput += input; observedInput = true; }
|
|
53
53
|
if (output !== null) { cumulativeOutput += output; observedOutput = true; }
|
|
54
54
|
results++;
|
|
55
|
-
lastTask = { inputTokens: input, outputTokens: output, currentInputTokens:
|
|
55
|
+
lastTask = { inputTokens: input, outputTokens: output, currentInputTokens: null };
|
|
56
56
|
var observedWindow = matchingWindow(item.modelUsage, session.model);
|
|
57
57
|
if (observedWindow !== null) window = observedWindow;
|
|
58
58
|
}
|
|
59
|
-
if (used
|
|
60
|
-
used =
|
|
61
|
-
source = "
|
|
59
|
+
if (used !== null && window !== null && used > window) {
|
|
60
|
+
used = null;
|
|
61
|
+
source = "unavailable";
|
|
62
62
|
}
|
|
63
|
+
// Older result records contain lastStreamInputTokens, but that field was
|
|
64
|
+
// populated from adapter snapshots that could be cumulative or inflated by
|
|
65
|
+
// cache accounting. It is not safe evidence of current context occupancy.
|
|
63
66
|
var ratio = used !== null && window !== null && window > 0 ? Math.min(1, Math.round((used / window) * 1000) / 1000) : null;
|
|
64
67
|
return {
|
|
65
68
|
current: { source: source, usedTokens: used, windowTokens: window, usedRatio: ratio,
|
|
@@ -6,6 +6,18 @@ var driverOrchestration = require("./session-driver-orchestration");
|
|
|
6
6
|
var proposalControl = require("./worker-proposal-control");
|
|
7
7
|
var runtimeCatalog = require("./worker-runtime-catalog");
|
|
8
8
|
var MAX_SUMMARY_CHARS = 600, MAX_PLAN_CHARS = 6000, MAX_TASK_CHARS = 30000, MAX_RATIONALE_CHARS = 1000;
|
|
9
|
+
var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
|
|
10
|
+
function normalizeEvaluation(raw) {
|
|
11
|
+
if (raw == null) return null;
|
|
12
|
+
var input = typeof raw === "string" ? { outcome: raw } : raw;
|
|
13
|
+
if (!input || typeof input !== "object") return { error: "evaluation must be an outcome string or object" };
|
|
14
|
+
var outcome = typeof input.outcome === "string" ? input.outcome.trim().toLowerCase() : "";
|
|
15
|
+
if (EVALUATION_OUTCOMES.indexOf(outcome) === -1) {
|
|
16
|
+
return { error: "evaluation outcome must be one of: " + EVALUATION_OUTCOMES.join(", ") };
|
|
17
|
+
}
|
|
18
|
+
var note = typeof input.note === "string" ? input.note.replace(/\s+/g, " ").trim().slice(0, 400) : "";
|
|
19
|
+
return { value: { outcome: outcome, note: note } };
|
|
20
|
+
}
|
|
9
21
|
function modelValue(entry) {
|
|
10
22
|
if (typeof entry === "string") return entry;
|
|
11
23
|
return entry && (entry.value || entry.id) || "";
|
|
@@ -227,6 +239,8 @@ function attachWorkerProposal(ctx) {
|
|
|
227
239
|
if (!task) return toolResult({ error: "message is required so an accepted replacement delegates exactly once." });
|
|
228
240
|
if (!rationale) return toolResult({ error: "recommendationRationale is required for the replacement audit trail." });
|
|
229
241
|
if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Split Worker task is too long." });
|
|
242
|
+
var normalizedEvaluation = normalizeEvaluation(args.evaluation);
|
|
243
|
+
if (normalizedEvaluation && normalizedEvaluation.error) return toolResult({ error: normalizedEvaluation.error });
|
|
230
244
|
await ensureModelCatalogs();
|
|
231
245
|
var liveGroup = store.groupForMember(session.localId);
|
|
232
246
|
if (!isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) ||
|
|
@@ -261,7 +275,7 @@ function attachWorkerProposal(ctx) {
|
|
|
261
275
|
sourceGroupId: sourceGroupId,
|
|
262
276
|
sourceWorkerId: sourceWorkerId,
|
|
263
277
|
interrupt: args.interrupt === true,
|
|
264
|
-
evaluation:
|
|
278
|
+
evaluation: normalizedEvaluation ? normalizedEvaluation.value : null,
|
|
265
279
|
transactionId: "replace_" + crypto.randomUUID(),
|
|
266
280
|
};
|
|
267
281
|
if (superseded) updateProposal(session, superseded, { status: "superseded", supersededBy: proposal.proposalId });
|
package/lib/public/app.js
CHANGED
|
@@ -287,6 +287,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
287
287
|
slashCommands: [],
|
|
288
288
|
processing: false,
|
|
289
289
|
activeSessionId: null,
|
|
290
|
+
askUserRequests: {},
|
|
290
291
|
activeBackgroundTasks: [],
|
|
291
292
|
sessionVendorBound: false,
|
|
292
293
|
cliSessionId: null,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
function scopeKey() {
|
|
4
|
+
var sessionId = store.get('paneSessionId') || store.get('activeSessionId') || "unknown";
|
|
5
|
+
var project = store.get('currentSlug') || "unknown-project";
|
|
6
|
+
var pane = store.get('paneMode') ? "pane" : "main";
|
|
7
|
+
return project + "\u0000" + pane + "\u0000" + String(sessionId);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function requestKey(toolId) {
|
|
11
|
+
return scopeKey() + "\u0000" + String(toolId);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requests() {
|
|
15
|
+
return store.get('askUserRequests') || {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getAskUserRequestKey(toolId) {
|
|
19
|
+
return requestKey(toolId);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function rememberAskUserRequest(toolId) {
|
|
23
|
+
var key = requestKey(toolId);
|
|
24
|
+
var current = requests();
|
|
25
|
+
if (!current[key]) {
|
|
26
|
+
var next = Object.assign({}, current);
|
|
27
|
+
next[key] = { toolId: String(toolId), scope: scopeKey(), answered: false };
|
|
28
|
+
store.set({ askUserRequests: next });
|
|
29
|
+
}
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hasAskUserRequest(toolId) {
|
|
34
|
+
return !!requests()[requestKey(toolId)];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function markAskUserRequestAnswered(toolId) {
|
|
38
|
+
var key = requestKey(toolId);
|
|
39
|
+
var current = requests();
|
|
40
|
+
if (!current[key]) return;
|
|
41
|
+
var next = Object.assign({}, current);
|
|
42
|
+
next[key] = Object.assign({}, next[key], { answered: true });
|
|
43
|
+
store.set({ askUserRequests: next });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function findAskUserCard(root, toolId) {
|
|
47
|
+
if (!root || !root.querySelectorAll) return null;
|
|
48
|
+
var key = requestKey(toolId);
|
|
49
|
+
var cards = root.querySelectorAll(".ask-user-container");
|
|
50
|
+
for (var i = 0; i < cards.length; i++) {
|
|
51
|
+
if (cards[i].dataset && cards[i].dataset.askUserRequestKey === key) return cards[i];
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function saveAskUserState() {
|
|
57
|
+
return Object.assign({}, requests());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function restoreAskUserState(saved) {
|
|
61
|
+
store.set({ askUserRequests: Object.assign({}, saved || {}) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resetAskUserState() {
|
|
65
|
+
store.set({ askUserRequests: {} });
|
|
66
|
+
}
|
|
@@ -8,6 +8,7 @@ import { getChatLayout } from './theme.js';
|
|
|
8
8
|
import { store } from './store.js';
|
|
9
9
|
import { VENDOR_NAMES } from './app-rendering.js';
|
|
10
10
|
import { startThinkingSegment, appendThinkingText, stopThinkingSegment, resetThinkingTurn, clearThinkingState, saveThinkingState, restoreThinkingState, updateThinkingTokenEstimate } from './thinking-lifecycle.js';
|
|
11
|
+
import { findAskUserCard, getAskUserRequestKey, hasAskUserRequest, rememberAskUserRequest, markAskUserRequestAnswered, saveAskUserState, restoreAskUserState, resetAskUserState } from './ask-user-state.js';
|
|
11
12
|
|
|
12
13
|
var ctx;
|
|
13
14
|
|
|
@@ -227,6 +228,10 @@ function shortPath(p) {
|
|
|
227
228
|
|
|
228
229
|
// --- AskUserQuestion ---
|
|
229
230
|
export function renderAskUserQuestion(toolId, input) {
|
|
231
|
+
var existingCard = findAskUserCard(ctx.messagesEl, toolId);
|
|
232
|
+
if (existingCard) return existingCard;
|
|
233
|
+
if (hasAskUserRequest(toolId)) return null;
|
|
234
|
+
|
|
230
235
|
ctx.finalizeAssistantBlock();
|
|
231
236
|
stopThinking();
|
|
232
237
|
closeToolGroup();
|
|
@@ -237,6 +242,9 @@ export function renderAskUserQuestion(toolId, input) {
|
|
|
237
242
|
var container = document.createElement("div");
|
|
238
243
|
container.className = "ask-user-container";
|
|
239
244
|
container.dataset.toolId = toolId;
|
|
245
|
+
var requestKey = getAskUserRequestKey(toolId);
|
|
246
|
+
container.dataset.askUserRequestKey = requestKey;
|
|
247
|
+
rememberAskUserRequest(toolId);
|
|
240
248
|
|
|
241
249
|
// Mate DM: wrap in avatar + content layout (same as msg-assistant)
|
|
242
250
|
var mateContentWrap = null;
|
|
@@ -372,6 +380,7 @@ export function renderAskUserQuestion(toolId, input) {
|
|
|
372
380
|
skipBtn.addEventListener("click", function () {
|
|
373
381
|
if (container.classList.contains("answered")) return;
|
|
374
382
|
container.classList.add("answered");
|
|
383
|
+
markAskUserRequestAnswered(toolId);
|
|
375
384
|
enableMainInput();
|
|
376
385
|
if (ctx.ws && ctx.connected) {
|
|
377
386
|
ctx.ws.send(JSON.stringify({ type: "stop" }));
|
|
@@ -415,7 +424,7 @@ export function enableMainInput() {
|
|
|
415
424
|
}
|
|
416
425
|
|
|
417
426
|
function submitAskUserAnswer(container, toolId, questions, answers, multiSelections) {
|
|
418
|
-
if (container.classList.contains("answered")) return;
|
|
427
|
+
if (container.classList.contains("answered") || container.dataset.askUserSubmitted === "true") return;
|
|
419
428
|
|
|
420
429
|
var result = {};
|
|
421
430
|
for (var i = 0; i < questions.length; i++) {
|
|
@@ -430,6 +439,7 @@ function submitAskUserAnswer(container, toolId, questions, answers, multiSelecti
|
|
|
430
439
|
if (Object.keys(result).length === 0) return;
|
|
431
440
|
|
|
432
441
|
container.classList.add("answered");
|
|
442
|
+
container.dataset.askUserSubmitted = "true";
|
|
433
443
|
enableMainInput();
|
|
434
444
|
if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
|
|
435
445
|
|
|
@@ -471,9 +481,10 @@ function showAnswerSummary(container, questions, answers) {
|
|
|
471
481
|
}
|
|
472
482
|
|
|
473
483
|
export function markAskUserAnswered(toolId, answers) {
|
|
474
|
-
var container =
|
|
484
|
+
var container = findAskUserCard(ctx.messagesEl, toolId);
|
|
475
485
|
if (container && !container.classList.contains("answered")) {
|
|
476
486
|
container.classList.add("answered");
|
|
487
|
+
markAskUserRequestAnswered(toolId);
|
|
477
488
|
enableMainInput();
|
|
478
489
|
// Restore answers from history replay
|
|
479
490
|
if (answers && Object.keys(answers).length > 0) {
|
|
@@ -2420,6 +2431,7 @@ export function saveToolState() {
|
|
|
2420
2431
|
toolGroupCounter: toolGroupCounter,
|
|
2421
2432
|
toolGroups: toolGroups,
|
|
2422
2433
|
lastCumulativeCost: _lastCumulativeCost,
|
|
2434
|
+
askUserState: saveAskUserState(),
|
|
2423
2435
|
};
|
|
2424
2436
|
}
|
|
2425
2437
|
|
|
@@ -2435,6 +2447,7 @@ export function restoreToolState(saved) {
|
|
|
2435
2447
|
toolGroupCounter = saved.toolGroupCounter;
|
|
2436
2448
|
toolGroups = saved.toolGroups;
|
|
2437
2449
|
_lastCumulativeCost = saved.lastCumulativeCost || 0;
|
|
2450
|
+
restoreAskUserState(saved.askUserState);
|
|
2438
2451
|
if (todoWidgetEl) {
|
|
2439
2452
|
setupTodoObserver();
|
|
2440
2453
|
}
|
|
@@ -2442,6 +2455,7 @@ export function restoreToolState(saved) {
|
|
|
2442
2455
|
|
|
2443
2456
|
export function resetToolState() {
|
|
2444
2457
|
tools = {};
|
|
2458
|
+
resetAskUserState();
|
|
2445
2459
|
clearThinkingState();
|
|
2446
2460
|
inPlanMode = false;
|
|
2447
2461
|
planContent = null;
|
|
@@ -2,6 +2,7 @@ var usersModule = require("./users");
|
|
|
2
2
|
var yoke = require("./yoke");
|
|
3
3
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
4
4
|
var sessionTitlePolicy = require("./session-title-policy");
|
|
5
|
+
var askUserIdentity = require("./ask-user-request-identity");
|
|
5
6
|
|
|
6
7
|
var OVERLOAD_RETRY_LIMIT = 3;
|
|
7
8
|
|
|
@@ -379,7 +380,24 @@ function attachMessageProcessor(ctx) {
|
|
|
379
380
|
if (block && block.type === "tool_use") {
|
|
380
381
|
var input = {};
|
|
381
382
|
try { input = JSON.parse(block.inputJson); } catch (e) {}
|
|
382
|
-
|
|
383
|
+
// Native AskUserQuestion is recorded when canUseTool opens the
|
|
384
|
+
// request. The later block_stop is the SDK's completion notification
|
|
385
|
+
// for that same request, not a second question. Keeping both records
|
|
386
|
+
// made replay render two independently editable cards and allowed the
|
|
387
|
+
// client to submit the same request twice.
|
|
388
|
+
var alreadyRecordedAsk = false;
|
|
389
|
+
if (block.name === "AskUserQuestion" && block.id && Array.isArray(session.history)) {
|
|
390
|
+
for (var ahi = session.history.length - 1; ahi >= 0; ahi--) {
|
|
391
|
+
var priorAsk = session.history[ahi];
|
|
392
|
+
if (askUserIdentity.sameAskUserRequest(priorAsk, block.id, input)) {
|
|
393
|
+
alreadyRecordedAsk = true;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!alreadyRecordedAsk) {
|
|
399
|
+
sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
|
|
400
|
+
}
|
|
383
401
|
|
|
384
402
|
// Track active Task tools for sub-agent done detection
|
|
385
403
|
if (block.name === "Task") {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// Partner-control tools for sessions that belong to a split group.
|
|
2
2
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
|
+
var z;
|
|
5
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
6
|
+
var evaluationSchema = z ? z.union([
|
|
7
|
+
z.string(),
|
|
8
|
+
z.object({ outcome: z.string(), note: z.string().optional() }),
|
|
9
|
+
]) : null;
|
|
4
10
|
|
|
5
11
|
// `options.lifecycle` adds the autonomous management tools. They are omitted
|
|
6
12
|
// for a plain side-by-side split, which has no Driver role to exercise them.
|
|
@@ -68,7 +74,7 @@ function getToolDefs(handlers, options) {
|
|
|
68
74
|
workerEffort: { type: "string", description: "Optional reasoning effort for the new Worker." },
|
|
69
75
|
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended replacement vendor, model, and effort fit the next task." },
|
|
70
76
|
evaluation: {
|
|
71
|
-
|
|
77
|
+
schema: evaluationSchema,
|
|
72
78
|
description: "Optional bounded assessment of the Worker being replaced, recorded against that exact generation.",
|
|
73
79
|
},
|
|
74
80
|
operationId: { type: "string", description: "Optional stable id for this replacement. Reusing it within the same human turn returns the original operation instead of replacing twice." },
|
|
@@ -69,6 +69,7 @@ function restore(meta, session) {
|
|
|
69
69
|
createdVia: stored.createdVia === "split-worker" ? "split-worker" : null,
|
|
70
70
|
createdAt: typeof stored.createdAt === "number" && isFinite(stored.createdAt) ? stored.createdAt : null,
|
|
71
71
|
};
|
|
72
|
+
session._pairGeneration = session.sessionProvenance.generation;
|
|
72
73
|
return storedOriginId !== session.sessionOriginId || JSON.stringify(stored) !== JSON.stringify(session.sessionProvenance);
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -12,7 +12,8 @@ function buildShape(props, required) {
|
|
|
12
12
|
var key = keys[i];
|
|
13
13
|
var prop = props[key];
|
|
14
14
|
var field;
|
|
15
|
-
if (prop.
|
|
15
|
+
if (prop.schema) field = prop.schema;
|
|
16
|
+
else if (prop.type === "number") field = z.number();
|
|
16
17
|
else if (prop.type === "boolean") field = z.boolean();
|
|
17
18
|
else if (prop.enum) field = z.enum(prop.enum);
|
|
18
19
|
else field = z.string();
|
package/lib/sessions.js
CHANGED
|
@@ -186,6 +186,7 @@ function createSessionManager(opts) {
|
|
|
186
186
|
var provenanceMeta = sessionProvenance.metadata(session);
|
|
187
187
|
metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
|
|
188
188
|
if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
|
|
189
|
+
if (Array.isArray(session._workerGenerations)) metaObj.workerGenerations = session._workerGenerations;
|
|
189
190
|
var meta = JSON.stringify(metaObj);
|
|
190
191
|
var lines = [meta];
|
|
191
192
|
for (var i = 0; i < session.history.length; i++) {
|
|
@@ -308,6 +309,7 @@ function createSessionManager(opts) {
|
|
|
308
309
|
if (m.ownerId) session.ownerId = m.ownerId;
|
|
309
310
|
session.sessionOriginId = m.sessionOriginId || null;
|
|
310
311
|
session._provenanceMigrationNeeded = sessionProvenance.restore(m, session);
|
|
312
|
+
if (Array.isArray(m.workerGenerations)) session._workerGenerations = m.workerGenerations;
|
|
311
313
|
// Born-TUI session: PTY is gone after restart, but the cliSessionId
|
|
312
314
|
// is still resumable via `claude --resume <id>`. We mark the mode
|
|
313
315
|
// here so it shows up in the sidebar with the right icon; the
|
|
@@ -193,7 +193,8 @@ function flattenEvent(notification, state) {
|
|
|
193
193
|
if (turnStatus === "interrupted" || state.aborted) {
|
|
194
194
|
events.push({ yokeType: "interrupted" });
|
|
195
195
|
}
|
|
196
|
-
var inputTokens = state.lastInputTokens
|
|
196
|
+
var inputTokens = state.lastInputTokens !== null && state.lastInputTokens !== undefined
|
|
197
|
+
? state.lastInputTokens : (usage ? (usage.input_tokens || 0) : 0);
|
|
197
198
|
var outputTokens = (usage ? (usage.output_tokens || 0) : 0) || state.lastOutputTokens || 0;
|
|
198
199
|
var cachedTokens = (usage ? (usage.cached_input_tokens || 0) : 0) || state.lastCachedTokens || 0;
|
|
199
200
|
var hasTokenData = inputTokens > 0 || outputTokens > 0;
|
|
@@ -217,7 +218,7 @@ function flattenEvent(notification, state) {
|
|
|
217
218
|
} : null,
|
|
218
219
|
modelUsage: resultModelUsage,
|
|
219
220
|
sessionId: state.threadId || null,
|
|
220
|
-
lastStreamInputTokens:
|
|
221
|
+
lastStreamInputTokens: null,
|
|
221
222
|
});
|
|
222
223
|
state.lastInputTokens = null;
|
|
223
224
|
state.lastCachedTokens = null;
|
|
@@ -675,15 +676,28 @@ function flattenEvent(notification, state) {
|
|
|
675
676
|
// context gauge must use the LAST turn, not the running total: each turn's
|
|
676
677
|
// input already contains the whole conversation, so summing turns
|
|
677
678
|
// overstates occupancy (a 5k + 8k thread reads as 13k of an 8k context).
|
|
678
|
-
//
|
|
679
|
+
// A missing lastTurn is not evidence of current context occupancy. The
|
|
680
|
+
// protocol's total is cumulative across the thread and must never be used
|
|
681
|
+
// as the context gauge.
|
|
679
682
|
if (method === "thread/tokenUsage/updated") {
|
|
680
683
|
var tu = params.tokenUsage;
|
|
681
684
|
if (tu) {
|
|
682
|
-
var turnUsage = tu.lastTurn
|
|
685
|
+
var turnUsage = tu.lastTurn;
|
|
683
686
|
if (turnUsage) {
|
|
684
|
-
|
|
687
|
+
var candidateInput = typeof turnUsage.inputTokens === "number" ? turnUsage.inputTokens : null;
|
|
688
|
+
var currentWindow = typeof tu.modelContextWindow === "number" ? tu.modelContextWindow : null;
|
|
689
|
+
state.lastInputTokens = candidateInput !== null && (currentWindow === null || candidateInput <= currentWindow) ? candidateInput : null;
|
|
685
690
|
state.lastCachedTokens = turnUsage.cachedInputTokens || 0;
|
|
686
691
|
state.lastOutputTokens = turnUsage.outputTokens || 0;
|
|
692
|
+
state.currentContextUsage = state.lastInputTokens !== null &&
|
|
693
|
+
(currentWindow === null || state.lastInputTokens <= currentWindow)
|
|
694
|
+
? { input_tokens: state.lastInputTokens, contextWindow: currentWindow }
|
|
695
|
+
: { input_tokens: null, contextWindow: currentWindow };
|
|
696
|
+
} else {
|
|
697
|
+
state.lastInputTokens = null;
|
|
698
|
+
state.lastCachedTokens = null;
|
|
699
|
+
state.lastOutputTokens = null;
|
|
700
|
+
state.currentContextUsage = null;
|
|
687
701
|
}
|
|
688
702
|
if (tu.modelContextWindow) state.modelContextWindow = tu.modelContextWindow;
|
|
689
703
|
}
|
|
@@ -734,6 +748,7 @@ function createEventState(model) {
|
|
|
734
748
|
lastInputTokens: null,
|
|
735
749
|
lastCachedTokens: null,
|
|
736
750
|
lastOutputTokens: null,
|
|
751
|
+
currentContextUsage: null,
|
|
737
752
|
modelContextWindow: null,
|
|
738
753
|
done: false,
|
|
739
754
|
aborted: false,
|
|
@@ -1107,6 +1122,9 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1107
1122
|
|
|
1108
1123
|
var threadResult;
|
|
1109
1124
|
if (queryOpts.resumeSessionId) {
|
|
1125
|
+
// ThreadResumeParams in the installed app-server schema has no
|
|
1126
|
+
// dynamicTools field. New tools therefore require a new Driver/Worker
|
|
1127
|
+
// thread; resumed threads can only refresh handlers for persisted tools.
|
|
1110
1128
|
threadResult = await appServer.send("thread/resume", {
|
|
1111
1129
|
threadId: queryOpts.resumeSessionId,
|
|
1112
1130
|
model: threadParams.model,
|
|
@@ -1269,11 +1287,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1269
1287
|
},
|
|
1270
1288
|
|
|
1271
1289
|
getContextUsage: function() {
|
|
1272
|
-
|
|
1273
|
-
return Promise.resolve({
|
|
1274
|
-
input_tokens: state.lastInputTokens == null ? null : state.lastInputTokens,
|
|
1275
|
-
contextWindow: state.modelContextWindow || null,
|
|
1276
|
-
});
|
|
1290
|
+
return Promise.resolve(state.currentContextUsage);
|
|
1277
1291
|
},
|
|
1278
1292
|
|
|
1279
1293
|
abort: function() {
|
package/package.json
CHANGED