taskchef 7.28.0 → 7.29.0
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/.codex-plugin/plugin.json +1 -1
- package/README.md +16 -1
- package/docs/dashboard-lifecycle.md +7 -0
- package/docs/spec.md +9 -0
- package/package.json +1 -1
- package/scripts/roles/resolve_roles.py +128 -7
- package/skills/taskchef-delegate/SKILL.md +1 -1
- package/skills/taskchef-delegate/references/model-roles.md +12 -9
- package/src/dashboard/dist/app.js +50 -35
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/App.test.tsx +10 -1
- package/src/dashboard/react/App.tsx +8 -4
- package/src/dashboard/react/api.test.ts +17 -1
- package/src/dashboard/react/api.ts +8 -0
- package/src/dashboard/react/components/ModelSettings.test.tsx +71 -10
- package/src/dashboard/react/components/ModelSettings.tsx +68 -25
- package/src/dashboard/react/styles.css +15 -0
- package/src/dashboard.js +62 -8
- package/src/mcp.js +0 -1
- package/src/model-roles.js +34 -8
- package/src/usage-tracker.js +192 -23
- package/src/usage.js +40 -12
- package/src/workspace.js +0 -3
package/src/usage-tracker.js
CHANGED
|
@@ -17,6 +17,10 @@ function isAdministrativeTurn(turn) {
|
|
|
17
17
|
return turn?.provenance?.kind === "dashboard_manual";
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function isInterruptedTurn(turn) {
|
|
21
|
+
return turn?.result?.status === "interrupted";
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
function hasTerminalLatestTurn(task) {
|
|
21
25
|
return task.latestTurn !== null
|
|
22
26
|
&& task.latestTurn !== undefined
|
|
@@ -128,6 +132,19 @@ function administrativeTask(task, existing = null, now = new Date().toISOString(
|
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
function interruptedTask(task, existing = null, now = new Date().toISOString()) {
|
|
136
|
+
const usage = calculatingTask(task, existing, now);
|
|
137
|
+
const hasTaskProjection = existing?.task !== null && existing?.task !== undefined;
|
|
138
|
+
return {
|
|
139
|
+
...usage,
|
|
140
|
+
status: hasTaskProjection ? "available" : "unavailable",
|
|
141
|
+
...(hasTaskProjection ? {} : { reason: "The turn ended without a terminal usage boundary." }),
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
retryAfter: null,
|
|
144
|
+
task: existing?.task ?? null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
131
148
|
async function updateStore(workspace, taskId, transform) {
|
|
132
149
|
const release = await acquireWorkspaceLock(workspace);
|
|
133
150
|
try {
|
|
@@ -167,6 +184,47 @@ function snapshotSupersedes(current, incoming) {
|
|
|
167
184
|
].every((field) => incoming[field] >= current[field]);
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
function snapshotCorrects(current, incoming) {
|
|
188
|
+
if (!current) return false;
|
|
189
|
+
const currentSampledAt = Date.parse(current.sampledAt ?? 0);
|
|
190
|
+
const incomingSampledAt = Date.parse(incoming.sampledAt ?? 0);
|
|
191
|
+
return Number.isFinite(incomingSampledAt)
|
|
192
|
+
&& incomingSampledAt >= currentSampledAt
|
|
193
|
+
&& [
|
|
194
|
+
"inputTokens",
|
|
195
|
+
"cachedInputTokens",
|
|
196
|
+
"outputTokens",
|
|
197
|
+
"reasoningOutputTokens",
|
|
198
|
+
"totalTokens",
|
|
199
|
+
].some((field) => incoming[field] < current[field]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function correctedRecord(task, snapshot) {
|
|
203
|
+
const now = snapshot.sampledAt;
|
|
204
|
+
const version = snapshot.provenance.version ? ` ${snapshot.provenance.version}` : "";
|
|
205
|
+
const reason = `ccusage${version} corrected an older cumulative total; reliable per-turn usage is unavailable.`;
|
|
206
|
+
const turns = Object.fromEntries(task.turns.slice(-MAX_TRACKED_TURNS).map((turn) => [
|
|
207
|
+
turn.turnRef,
|
|
208
|
+
isAdministrativeTurn(turn)
|
|
209
|
+
? { status: "unavailable", reason: ADMINISTRATIVE_USAGE_REASON, updatedAt: now }
|
|
210
|
+
: { status: "unavailable", reason, updatedAt: now },
|
|
211
|
+
]));
|
|
212
|
+
const generation = lifecycleGeneration(task);
|
|
213
|
+
return {
|
|
214
|
+
threadId: task.threadId,
|
|
215
|
+
generationTurnRef: task.latestTurn?.turnRef ?? null,
|
|
216
|
+
generationTurnCount: generation.turnCount,
|
|
217
|
+
generationTerminal: generation.terminal,
|
|
218
|
+
zeroBaselineTurnRef: null,
|
|
219
|
+
status: "available",
|
|
220
|
+
updatedAt: now,
|
|
221
|
+
retryAfter: null,
|
|
222
|
+
task: snapshot,
|
|
223
|
+
turns,
|
|
224
|
+
boundaries: { [task.latestTurn.turnRef]: snapshot },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
170
228
|
function candidateHasAdvanced(task, existing, snapshot) {
|
|
171
229
|
const latestIndex = task.turns.findIndex((turn) => turn.turnRef === task.latestTurn?.turnRef);
|
|
172
230
|
if (latestIndex <= 0) return true;
|
|
@@ -269,14 +327,22 @@ function reconcileRecord(task, existing, snapshot, { boundaryReliable = true } =
|
|
|
269
327
|
|
|
270
328
|
export function createUsageTracker({
|
|
271
329
|
workspace,
|
|
330
|
+
maxConcurrentJobs = 2,
|
|
272
331
|
readThreadUsage = readCcusageThreadUsage,
|
|
273
332
|
retryDelaysMs = [2_000, 3_000, 4_000, 1_000],
|
|
274
333
|
retryCooldownMs = 60_000,
|
|
275
334
|
setTimer = setTimeout,
|
|
276
335
|
} = {}) {
|
|
336
|
+
if (!Number.isSafeInteger(maxConcurrentJobs) || maxConcurrentJobs < 1 || maxConcurrentJobs > 8) {
|
|
337
|
+
throw new Error("usage tracker concurrency must be an integer from 1 to 8");
|
|
338
|
+
}
|
|
277
339
|
const jobs = new Map();
|
|
278
340
|
const observationChains = new Map();
|
|
341
|
+
const readyJobs = [];
|
|
342
|
+
let activeJobs = 0;
|
|
279
343
|
let canonicalWorkspace = null;
|
|
344
|
+
let closed = false;
|
|
345
|
+
let preloadChain = Promise.resolve();
|
|
280
346
|
|
|
281
347
|
const root = async () => {
|
|
282
348
|
canonicalWorkspace ??= await realpath(path.resolve(workspace));
|
|
@@ -289,6 +355,9 @@ export function createUsageTracker({
|
|
|
289
355
|
const markAdministrative = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
290
356
|
generationIsOlder(task, existing) ? existing : administrativeTask(task, existing)
|
|
291
357
|
));
|
|
358
|
+
const markInterrupted = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
359
|
+
generationIsOlder(task, existing) ? existing : interruptedTask(task, existing)
|
|
360
|
+
));
|
|
292
361
|
|
|
293
362
|
const reconcile = async (task, {
|
|
294
363
|
finalAttempt = false,
|
|
@@ -332,6 +401,9 @@ export function createUsageTracker({
|
|
|
332
401
|
return updateStore(await root(), task.id, (existing) => {
|
|
333
402
|
const latestTurnRef = task.latestTurn?.turnRef;
|
|
334
403
|
if (existing?.generationTurnRef !== latestTurnRef) return existing;
|
|
404
|
+
if (stable && snapshotCorrects(existing?.task, snapshot)) {
|
|
405
|
+
return correctedRecord(task, snapshot);
|
|
406
|
+
}
|
|
335
407
|
const supersedes = snapshotSupersedes(existing?.task, snapshot);
|
|
336
408
|
if (existing?.boundaries?.[latestTurnRef]
|
|
337
409
|
|| existing?.turns?.[latestTurnRef]?.status === "available") {
|
|
@@ -344,15 +416,49 @@ export function createUsageTracker({
|
|
|
344
416
|
});
|
|
345
417
|
};
|
|
346
418
|
|
|
419
|
+
const drainReadyJobs = () => {
|
|
420
|
+
while (!closed && activeJobs < maxConcurrentJobs && readyJobs.length > 0) {
|
|
421
|
+
const job = readyJobs.shift();
|
|
422
|
+
job.queued = false;
|
|
423
|
+
if (job.cancelled || jobs.get(job.task.id) !== job) {
|
|
424
|
+
for (const resolve of job.waiters.splice(0)) resolve();
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const waiters = job.waiters.splice(0);
|
|
428
|
+
activeJobs += 1;
|
|
429
|
+
void job.run().finally(() => {
|
|
430
|
+
activeJobs -= 1;
|
|
431
|
+
for (const resolve of waiters) resolve();
|
|
432
|
+
drainReadyJobs();
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const queueJob = (job) => {
|
|
438
|
+
if (closed || job.cancelled || jobs.get(job.task.id) !== job) return Promise.resolve();
|
|
439
|
+
return new Promise((resolve) => {
|
|
440
|
+
job.waiters.push(resolve);
|
|
441
|
+
if (!job.queued) {
|
|
442
|
+
job.queued = true;
|
|
443
|
+
readyJobs.push(job);
|
|
444
|
+
drainReadyJobs();
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
|
|
347
449
|
const schedule = (task, { immediate = false } = {}) => {
|
|
348
|
-
if (!task.threadId || jobs.has(task.id)) return;
|
|
450
|
+
if (closed || !task.threadId || jobs.has(task.id)) return;
|
|
349
451
|
const job = {
|
|
350
452
|
cancelled: false,
|
|
351
453
|
previousFingerprint: null,
|
|
454
|
+
queued: false,
|
|
455
|
+
run: null,
|
|
456
|
+
task,
|
|
352
457
|
turnRef: task.latestTurn?.turnRef ?? null,
|
|
458
|
+
waiters: [],
|
|
353
459
|
};
|
|
354
460
|
let attempt = 0;
|
|
355
|
-
|
|
461
|
+
job.run = async () => {
|
|
356
462
|
if (job.cancelled || jobs.get(task.id) !== job) return;
|
|
357
463
|
let complete = false;
|
|
358
464
|
try {
|
|
@@ -372,36 +478,89 @@ export function createUsageTracker({
|
|
|
372
478
|
}
|
|
373
479
|
const delay = retryDelaysMs[attempt] ?? 0;
|
|
374
480
|
attempt += 1;
|
|
375
|
-
const timer = setTimer(
|
|
481
|
+
const timer = setTimer(() => queueJob(job), delay);
|
|
376
482
|
timer?.unref?.();
|
|
377
483
|
};
|
|
378
484
|
jobs.set(task.id, job);
|
|
379
|
-
const timer = setTimer(
|
|
485
|
+
const timer = setTimer(() => queueJob(job), immediate ? 0 : retryDelaysMs[attempt++]);
|
|
380
486
|
timer?.unref?.();
|
|
381
487
|
};
|
|
382
488
|
|
|
489
|
+
const observe = (task) => {
|
|
490
|
+
if (closed) return Promise.resolve(null);
|
|
491
|
+
const previous = observationChains.get(task.id) ?? Promise.resolve();
|
|
492
|
+
const observation = previous.catch(() => {}).then(async () => {
|
|
493
|
+
if (closed || !task.threadId) return null;
|
|
494
|
+
const active = jobs.get(task.id);
|
|
495
|
+
const latestTurnRef = task.latestTurn?.turnRef ?? null;
|
|
496
|
+
if (active && active.turnRef !== latestTurnRef) {
|
|
497
|
+
active.cancelled = true;
|
|
498
|
+
jobs.delete(task.id);
|
|
499
|
+
}
|
|
500
|
+
const usage = isAdministrativeTurn(task.latestTurn)
|
|
501
|
+
? await markAdministrative(task)
|
|
502
|
+
: isInterruptedTurn(task.latestTurn)
|
|
503
|
+
? await markInterrupted(task)
|
|
504
|
+
: await markCalculating(task);
|
|
505
|
+
if (hasTerminalLatestTurn(task)) schedule(task);
|
|
506
|
+
return usage;
|
|
507
|
+
});
|
|
508
|
+
observationChains.set(task.id, observation);
|
|
509
|
+
void observation.finally(() => {
|
|
510
|
+
if (observationChains.get(task.id) === observation) observationChains.delete(task.id);
|
|
511
|
+
}).catch(() => {});
|
|
512
|
+
return observation;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const needsPreload = (task, usage, now = Date.now()) => {
|
|
516
|
+
if (!task.threadId || !task.latestTurn) return false;
|
|
517
|
+
const latestTurnRef = task.latestTurn.turnRef ?? null;
|
|
518
|
+
if (isAdministrativeTurn(task.latestTurn)) {
|
|
519
|
+
return !usage
|
|
520
|
+
|| usage.threadId !== task.threadId
|
|
521
|
+
|| usage.generationTurnRef !== latestTurnRef;
|
|
522
|
+
}
|
|
523
|
+
if (isInterruptedTurn(task.latestTurn)) {
|
|
524
|
+
const latestUsage = usage?.turns?.[latestTurnRef];
|
|
525
|
+
const expectedStatus = usage?.task ? "available" : "unavailable";
|
|
526
|
+
return !usage
|
|
527
|
+
|| usage.threadId !== task.threadId
|
|
528
|
+
|| usage.generationTurnRef !== latestTurnRef
|
|
529
|
+
|| usage.status !== expectedStatus
|
|
530
|
+
|| latestUsage?.status !== "unavailable";
|
|
531
|
+
}
|
|
532
|
+
if (!usage || usage.threadId !== task.threadId || usage.generationTurnRef !== latestTurnRef) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
if (!hasTerminalLatestTurn(task)) return false;
|
|
536
|
+
if (usage.status === "available") return false;
|
|
537
|
+
if (usage.status === "calculating") return true;
|
|
538
|
+
if (usage.status === "unavailable") {
|
|
539
|
+
return !usage.retryAfter || Date.parse(usage.retryAfter) <= now;
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
};
|
|
543
|
+
|
|
383
544
|
return {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
545
|
+
close() {
|
|
546
|
+
closed = true;
|
|
547
|
+
readyJobs.length = 0;
|
|
548
|
+
for (const job of jobs.values()) {
|
|
549
|
+
job.cancelled = true;
|
|
550
|
+
for (const resolve of job.waiters.splice(0)) resolve();
|
|
551
|
+
}
|
|
552
|
+
jobs.clear();
|
|
553
|
+
},
|
|
554
|
+
observe,
|
|
555
|
+
preload(tasks) {
|
|
556
|
+
preloadChain = preloadChain.catch(() => {}).then(async () => {
|
|
557
|
+
if (closed) return;
|
|
558
|
+
const store = await readUsageStore(await root());
|
|
559
|
+
for (const task of tasks) {
|
|
560
|
+
if (needsPreload(task, store.tasks[task.id] ?? null)) await observe(task);
|
|
393
561
|
}
|
|
394
|
-
const usage = isAdministrativeTurn(task.latestTurn)
|
|
395
|
-
? await markAdministrative(task)
|
|
396
|
-
: await markCalculating(task);
|
|
397
|
-
if (hasTerminalLatestTurn(task)) schedule(task);
|
|
398
|
-
return usage;
|
|
399
562
|
});
|
|
400
|
-
|
|
401
|
-
void observation.finally(() => {
|
|
402
|
-
if (observationChains.get(task.id) === observation) observationChains.delete(task.id);
|
|
403
|
-
}).catch(() => {});
|
|
404
|
-
return observation;
|
|
563
|
+
return preloadChain;
|
|
405
564
|
},
|
|
406
565
|
async get(task) {
|
|
407
566
|
const store = await readUsageStore(await root());
|
|
@@ -433,6 +592,16 @@ export function createUsageTracker({
|
|
|
433
592
|
void markAdministrative(task).catch(() => {});
|
|
434
593
|
return administrative;
|
|
435
594
|
}
|
|
595
|
+
if (isInterruptedTurn(task.latestTurn)) {
|
|
596
|
+
const latestUsage = usage?.turns?.[task.latestTurn.turnRef];
|
|
597
|
+
const expectedStatus = usage?.task ? "available" : "unavailable";
|
|
598
|
+
if (usage?.generationTurnRef === task.latestTurn.turnRef
|
|
599
|
+
&& usage.status === expectedStatus
|
|
600
|
+
&& latestUsage?.status === "unavailable") return usage;
|
|
601
|
+
const interrupted = interruptedTask(task, usage);
|
|
602
|
+
void markInterrupted(task).catch(() => {});
|
|
603
|
+
return interrupted;
|
|
604
|
+
}
|
|
436
605
|
if (!usage || usage.threadId !== task.threadId) {
|
|
437
606
|
const calculating = calculatingTask(task);
|
|
438
607
|
void markCalculating(task)
|
package/src/usage.js
CHANGED
|
@@ -538,18 +538,7 @@ export async function readCcusageThreadUsage(threadId, {
|
|
|
538
538
|
[...resolvedInvocation.args, ...args],
|
|
539
539
|
options,
|
|
540
540
|
);
|
|
541
|
-
|
|
542
|
-
if (version === null) {
|
|
543
|
-
try {
|
|
544
|
-
const result = await invoke(["--version"], {
|
|
545
|
-
timeout: Math.min(timeoutMs, 2_000),
|
|
546
|
-
maxBuffer: 64 * 1024,
|
|
547
|
-
});
|
|
548
|
-
version = String(result.stdout).trim().replace(/^ccusage\s+/i, "") || null;
|
|
549
|
-
} catch {
|
|
550
|
-
// Usage remains useful if a custom compatible ccusage cannot print its version.
|
|
551
|
-
}
|
|
552
|
-
}
|
|
541
|
+
const version = await readCcusageInvocationVersion(invoke, timeoutMs);
|
|
553
542
|
const report = async (pricingMode) => {
|
|
554
543
|
let result;
|
|
555
544
|
try {
|
|
@@ -589,6 +578,45 @@ export async function readCcusageThreadUsage(threadId, {
|
|
|
589
578
|
}
|
|
590
579
|
}
|
|
591
580
|
|
|
581
|
+
async function readCcusageInvocationVersion(invoke, timeoutMs) {
|
|
582
|
+
try {
|
|
583
|
+
const result = await invoke(["--version"], {
|
|
584
|
+
timeout: Math.min(timeoutMs, 2_000),
|
|
585
|
+
maxBuffer: 64 * 1024,
|
|
586
|
+
});
|
|
587
|
+
const version = String(result.stdout).trim().replace(/^ccusage\s+/i, "");
|
|
588
|
+
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(version) ? version : null;
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function readCcusageRuntimeInfo({
|
|
595
|
+
run = runBoundedProcess,
|
|
596
|
+
timeoutMs = 8_000,
|
|
597
|
+
} = {}) {
|
|
598
|
+
try {
|
|
599
|
+
const resolved = await resolveManagedCcusageInvocation(
|
|
600
|
+
managedCcusageInvocation(),
|
|
601
|
+
run,
|
|
602
|
+
timeoutMs,
|
|
603
|
+
);
|
|
604
|
+
const invoke = (args, options) => run(
|
|
605
|
+
resolved.command,
|
|
606
|
+
[...resolved.args, ...args],
|
|
607
|
+
options,
|
|
608
|
+
);
|
|
609
|
+
const version = await readCcusageInvocationVersion(invoke, timeoutMs);
|
|
610
|
+
return {
|
|
611
|
+
provider: "ccusage",
|
|
612
|
+
status: version === null ? "unavailable" : "available",
|
|
613
|
+
version,
|
|
614
|
+
};
|
|
615
|
+
} catch {
|
|
616
|
+
return { provider: "ccusage", status: "unavailable", version: null };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
592
620
|
export function usageStorePath(workspace) {
|
|
593
621
|
return path.join(workspace, USAGE_FILE_NAME);
|
|
594
622
|
}
|
package/src/workspace.js
CHANGED
|
@@ -706,9 +706,6 @@ export async function prepareDispatch(workspaceRoot, {
|
|
|
706
706
|
projectCount: projects.length,
|
|
707
707
|
projects,
|
|
708
708
|
modelRoles: await resolveModelRoles(),
|
|
709
|
-
projectModelRoles: await Promise.all(projects.map(async (project) => ({
|
|
710
|
-
project: project.name, path: project.path, ...await resolveModelRoles(project.path),
|
|
711
|
-
}))),
|
|
712
709
|
};
|
|
713
710
|
}
|
|
714
711
|
|