taskchef 7.28.1 → 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 +6 -1
- package/docs/dashboard-lifecycle.md +7 -0
- package/docs/spec.md +9 -0
- package/package.json +1 -1
- package/src/dashboard/dist/app.js +25 -25
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/react/components/ModelSettings.test.tsx +14 -0
- package/src/dashboard/react/components/ModelSettings.tsx +14 -1
- package/src/dashboard.js +28 -3
- package/src/usage-tracker.js +192 -23
- package/src/usage.js +40 -12
|
@@ -19,6 +19,20 @@ vi.mock('@mantine/core', async () => {
|
|
|
19
19
|
|
|
20
20
|
afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.clearAllMocks(); });
|
|
21
21
|
|
|
22
|
+
test('shows the version reported by the active usage provider', async () => {
|
|
23
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
24
|
+
ok: true,
|
|
25
|
+
json: async () => ({
|
|
26
|
+
profiles: [],
|
|
27
|
+
usageProvider: { provider: 'ccusage', status: 'available', version: '20.0.21' },
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
render(<MantineProvider><ModelSettings /></MantineProvider>);
|
|
31
|
+
expect(await screen.findByText('ccusage 20.0.21')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText('Usage provider')).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText(/Version reported by the executable/)).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
22
36
|
test('refresh shows configuration failures and does not label them honored', async () => {
|
|
23
37
|
const role = { role: 'reviewer', source: '/fixture/reviewer.toml', effectiveSource: '/fixture/reviewer.toml', model: 'fixture', effort: 'low', status: 'configured', availability: 'not checked', fallback: 'inherit parent settings', problems: [] as string[] };
|
|
24
38
|
const profile = { id: 'personal', project: 'Personal', roles: [role], problems: [], catalogSource: null };
|
|
@@ -6,9 +6,11 @@ import { updateModelRole } from '../api';
|
|
|
6
6
|
type ModelOption = { value: string; label: string; efforts: string[] };
|
|
7
7
|
type Role = { role: string; source: string | null; displaySource?: string | null; model: unknown; effort: unknown; status: string; problems: string[] };
|
|
8
8
|
type Profile = { id: string; roles: Role[]; problems: string[]; modelOptions?: ModelOption[] };
|
|
9
|
+
type UsageProvider = { provider: string; status: 'available' | 'unavailable'; version: string | null };
|
|
9
10
|
|
|
10
11
|
export function ModelSettings() {
|
|
11
12
|
const [profiles, setProfiles] = useState<Profile[]>([]);
|
|
13
|
+
const [usageProvider, setUsageProvider] = useState<UsageProvider | null>(null);
|
|
12
14
|
const [error, setError] = useState<string | null>(null);
|
|
13
15
|
const [loading, setLoading] = useState(true);
|
|
14
16
|
const [saving, setSaving] = useState<string | null>(null);
|
|
@@ -20,8 +22,9 @@ export function ModelSettings() {
|
|
|
20
22
|
setError(null);
|
|
21
23
|
fetch('/api/settings', { signal: controller.signal }).then(async (response) => {
|
|
22
24
|
if (!response.ok) throw new Error('Settings preview could not be loaded.');
|
|
23
|
-
const data = await response.json() as { profiles: Profile[] };
|
|
25
|
+
const data = await response.json() as { profiles: Profile[]; usageProvider?: UsageProvider };
|
|
24
26
|
setProfiles(data.profiles);
|
|
27
|
+
setUsageProvider(data.usageProvider ?? null);
|
|
25
28
|
}).catch((failure: Error) => {
|
|
26
29
|
if (!controller.signal.aborted) setError(failure.message);
|
|
27
30
|
}).finally(() => { if (!controller.signal.aborted) setLoading(false); });
|
|
@@ -87,5 +90,15 @@ export function ModelSettings() {
|
|
|
87
90
|
</Stack>
|
|
88
91
|
</Paper>)}
|
|
89
92
|
</>}
|
|
93
|
+
{usageProvider && <Paper withBorder p="md">
|
|
94
|
+
<Stack gap="xs">
|
|
95
|
+
<Group justify="space-between">
|
|
96
|
+
<Title order={3} size="h4">Usage provider</Title>
|
|
97
|
+
<Badge color={usageProvider.status === 'available' ? 'teal' : 'red'}>{usageProvider.status}</Badge>
|
|
98
|
+
</Group>
|
|
99
|
+
<Text>{usageProvider.provider}{usageProvider.version ? ` ${usageProvider.version}` : ' version unavailable'}</Text>
|
|
100
|
+
<Text c="dimmed" size="sm">Version reported by the executable TaskChef uses for token and cost estimates.</Text>
|
|
101
|
+
</Stack>
|
|
102
|
+
</Paper>}
|
|
90
103
|
</Stack>;
|
|
91
104
|
}
|
package/src/dashboard.js
CHANGED
|
@@ -27,7 +27,7 @@ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
|
27
27
|
import { taskGitHubProjection } from "./dashboard/github-links.js";
|
|
28
28
|
import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
|
|
29
29
|
import { createUsageTracker } from "./usage-tracker.js";
|
|
30
|
-
import { readUsageStore, usageStorePath } from "./usage.js";
|
|
30
|
+
import { readCcusageRuntimeInfo, readUsageStore, usageStorePath } from "./usage.js";
|
|
31
31
|
import { reportedWorkSummary } from "./reported-work.js";
|
|
32
32
|
import {
|
|
33
33
|
MAX_DASHBOARD_SESSION_PIDS,
|
|
@@ -804,7 +804,9 @@ export async function createDashboardServer({
|
|
|
804
804
|
serverVersion = DASHBOARD_SERVER_VERSION,
|
|
805
805
|
usageTracker = null,
|
|
806
806
|
usageSummaryMonitor = null,
|
|
807
|
+
usagePreloadIntervalMs = 30_000,
|
|
807
808
|
resolveRoles = resolveModelRoles,
|
|
809
|
+
resolveUsageProvider = readCcusageRuntimeInfo,
|
|
808
810
|
updateRole = updateModelRole,
|
|
809
811
|
control = null,
|
|
810
812
|
controlReplayCache = createDashboardControlReplayCache(),
|
|
@@ -818,6 +820,9 @@ export async function createDashboardServer({
|
|
|
818
820
|
if (!Number.isInteger(maxEventClients) || maxEventClients < 0) {
|
|
819
821
|
throw new Error("dashboard event-client limit must be a non-negative integer");
|
|
820
822
|
}
|
|
823
|
+
if (!Number.isFinite(usagePreloadIntervalMs) || usagePreloadIntervalMs < 1_000) {
|
|
824
|
+
throw new Error("dashboard usage preload interval must be at least one second");
|
|
825
|
+
}
|
|
821
826
|
if (!new Set(["mcp", "session", "standalone"]).has(launcher)) {
|
|
822
827
|
throw new Error("dashboard launcher must be mcp, session, or standalone");
|
|
823
828
|
}
|
|
@@ -851,11 +856,13 @@ export async function createDashboardServer({
|
|
|
851
856
|
}
|
|
852
857
|
} catch (error) {
|
|
853
858
|
taskUsageSummaryMonitor?.close();
|
|
859
|
+
taskUsageTracker?.close?.();
|
|
854
860
|
monitor.close();
|
|
855
861
|
throw error;
|
|
856
862
|
}
|
|
857
863
|
const clients = new Set();
|
|
858
864
|
const archiveRequests = new Set();
|
|
865
|
+
let usageProviderPromise = null;
|
|
859
866
|
let controlHandoff = null;
|
|
860
867
|
let controlHandoffTimer = null;
|
|
861
868
|
let allowedAuthority;
|
|
@@ -875,12 +882,21 @@ export async function createDashboardServer({
|
|
|
875
882
|
})),
|
|
876
883
|
};
|
|
877
884
|
};
|
|
878
|
-
const
|
|
885
|
+
const preloadUsage = () => {
|
|
886
|
+
void Promise.resolve(taskUsageTracker.preload?.(monitor.tasks)).catch(() => {});
|
|
887
|
+
};
|
|
888
|
+
const snapshotListener = () => {
|
|
889
|
+
preloadUsage();
|
|
890
|
+
broadcast("snapshot", dashboardSnapshot());
|
|
891
|
+
};
|
|
879
892
|
const usageSummaryListener = () => broadcast("snapshot", dashboardSnapshot());
|
|
880
893
|
const errorListener = () => broadcast("dashboard-error", publicMonitorError());
|
|
881
894
|
monitor.on("snapshot", snapshotListener);
|
|
882
895
|
monitor.on("monitorError", errorListener);
|
|
883
896
|
taskUsageSummaryMonitor.on("change", usageSummaryListener);
|
|
897
|
+
preloadUsage();
|
|
898
|
+
const usagePreloadTimer = setInterval(preloadUsage, usagePreloadIntervalMs);
|
|
899
|
+
usagePreloadTimer.unref?.();
|
|
884
900
|
|
|
885
901
|
const handleRequest = async (request, response) => {
|
|
886
902
|
const method = request.method ?? "GET";
|
|
@@ -1183,7 +1199,12 @@ export async function createDashboardServer({
|
|
|
1183
1199
|
|
|
1184
1200
|
if (url.pathname === "/api/settings" && method === "GET") {
|
|
1185
1201
|
const profiles = [{ id: "personal", project: "Personal", ...await resolveRoles({ includeCatalog: true }) }];
|
|
1186
|
-
|
|
1202
|
+
usageProviderPromise ??= Promise.resolve(resolveUsageProvider()).catch(() => ({
|
|
1203
|
+
provider: "ccusage",
|
|
1204
|
+
status: "unavailable",
|
|
1205
|
+
version: null,
|
|
1206
|
+
}));
|
|
1207
|
+
sendJson(response, 200, { profiles, usageProvider: await usageProviderPromise });
|
|
1187
1208
|
return;
|
|
1188
1209
|
}
|
|
1189
1210
|
|
|
@@ -1469,8 +1490,10 @@ export async function createDashboardServer({
|
|
|
1469
1490
|
server.once("error", reject);
|
|
1470
1491
|
server.listen(port, host, resolve);
|
|
1471
1492
|
}).catch((error) => {
|
|
1493
|
+
clearInterval(usagePreloadTimer);
|
|
1472
1494
|
monitor.close();
|
|
1473
1495
|
taskUsageSummaryMonitor.close();
|
|
1496
|
+
taskUsageTracker.close?.();
|
|
1474
1497
|
throw error;
|
|
1475
1498
|
});
|
|
1476
1499
|
|
|
@@ -1493,6 +1516,8 @@ export async function createDashboardServer({
|
|
|
1493
1516
|
taskUsageSummaryMonitor.off("change", usageSummaryListener);
|
|
1494
1517
|
monitor.close();
|
|
1495
1518
|
taskUsageSummaryMonitor.close();
|
|
1519
|
+
taskUsageTracker.close?.();
|
|
1520
|
+
clearInterval(usagePreloadTimer);
|
|
1496
1521
|
for (const client of clients) client.close();
|
|
1497
1522
|
const closed = new Promise((resolve, reject) => server.close((error) =>
|
|
1498
1523
|
error ? reject(error) : resolve()));
|
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
|
}
|