clay-server 4.1.0-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/project-pair-lifecycle.js +69 -41
- package/lib/project-pair-usage.js +7 -4
- package/lib/project-worker-proposal.js +15 -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
|
@@ -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 });
|
|
@@ -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