taskchef 7.19.1 → 7.21.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 +45 -11
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/spec.md +100 -15
- package/docs/workflows.md +13 -9
- package/index.js +1 -0
- package/package.json +2 -1
- package/src/dashboard/actions.js +81 -0
- package/src/dashboard/app.js +317 -29
- package/src/dashboard/index.html +17 -4
- package/src/dashboard/state.js +60 -3
- package/src/dashboard/styles.css +24 -4
- package/src/dashboard.js +129 -1
- package/src/mcp.js +23 -1
- package/src/usage-tracker.js +480 -0
- package/src/usage.js +388 -0
- package/src/workspace.js +368 -20
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { acquireWorkspaceLock } from "./workspace.js";
|
|
5
|
+
import {
|
|
6
|
+
readCcusageThreadUsage,
|
|
7
|
+
readUsageStore,
|
|
8
|
+
usageDelta,
|
|
9
|
+
writeUsageStore,
|
|
10
|
+
} from "./usage.js";
|
|
11
|
+
|
|
12
|
+
const TERMINAL_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
13
|
+
const MAX_TRACKED_TURNS = 250;
|
|
14
|
+
const ADMINISTRATIVE_USAGE_REASON = "Administrative action; no Codex usage boundary.";
|
|
15
|
+
|
|
16
|
+
function isAdministrativeTurn(turn) {
|
|
17
|
+
return turn?.provenance?.kind === "dashboard_manual";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasTerminalLatestTurn(task) {
|
|
21
|
+
return task.latestTurn !== null
|
|
22
|
+
&& task.latestTurn !== undefined
|
|
23
|
+
&& !isAdministrativeTurn(task.latestTurn)
|
|
24
|
+
&& task.latestTurn.result !== null
|
|
25
|
+
&& TERMINAL_STATUSES.has(task.latestTurn.result.status);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function lifecycleGeneration(task) {
|
|
29
|
+
return {
|
|
30
|
+
turnCount: task.turns.length,
|
|
31
|
+
terminal: hasTerminalLatestTurn(task),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function generationIsOlder(task, existing) {
|
|
36
|
+
if (!existing) return false;
|
|
37
|
+
const incoming = lifecycleGeneration(task);
|
|
38
|
+
if (incoming.turnCount !== existing.generationTurnCount) {
|
|
39
|
+
return incoming.turnCount < existing.generationTurnCount;
|
|
40
|
+
}
|
|
41
|
+
return !incoming.terminal && existing.generationTerminal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function publicReason(error) {
|
|
45
|
+
const message = String(error?.message ?? "");
|
|
46
|
+
if (/not installed/i.test(message)) return "ccusage is not installed.";
|
|
47
|
+
if (/could not resolve/i.test(message)) return "No matching Codex usage session was found.";
|
|
48
|
+
if (/timed out/i.test(message)) return "ccusage did not finish in time.";
|
|
49
|
+
return "Codex usage is unavailable from ccusage.";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function calculatingTask(task, existing = null, now = new Date().toISOString()) {
|
|
53
|
+
const latestTurnRef = task.latestTurn?.turnRef ?? null;
|
|
54
|
+
const preserveAvailable = existing?.generationTurnRef === latestTurnRef
|
|
55
|
+
&& existing?.status === "available"
|
|
56
|
+
&& existing?.turns?.[latestTurnRef]?.status === "available";
|
|
57
|
+
const recentTurns = task.turns.slice(-MAX_TRACKED_TURNS);
|
|
58
|
+
const turns = Object.fromEntries(recentTurns.flatMap((turn) => (
|
|
59
|
+
existing?.turns?.[turn.turnRef]
|
|
60
|
+
? [[turn.turnRef, existing.turns[turn.turnRef]]]
|
|
61
|
+
: []
|
|
62
|
+
)));
|
|
63
|
+
const generation = lifecycleGeneration(task);
|
|
64
|
+
for (const turn of recentTurns) {
|
|
65
|
+
if (isAdministrativeTurn(turn)) {
|
|
66
|
+
turns[turn.turnRef] = {
|
|
67
|
+
status: "unavailable",
|
|
68
|
+
reason: ADMINISTRATIVE_USAGE_REASON,
|
|
69
|
+
updatedAt: now,
|
|
70
|
+
};
|
|
71
|
+
} else if (turn.result === null) {
|
|
72
|
+
turns[turn.turnRef] = { status: "calculating", updatedAt: now };
|
|
73
|
+
} else if (turn.result.status === "interrupted") {
|
|
74
|
+
turns[turn.turnRef] = {
|
|
75
|
+
status: "unavailable",
|
|
76
|
+
reason: "The turn ended without a terminal usage boundary.",
|
|
77
|
+
updatedAt: now,
|
|
78
|
+
};
|
|
79
|
+
} else if (turns[turn.turnRef]?.status === "calculating"
|
|
80
|
+
&& turn.turnRef !== task.latestTurn?.turnRef) {
|
|
81
|
+
turns[turn.turnRef] = {
|
|
82
|
+
status: "unavailable",
|
|
83
|
+
reason: "A newer turn started before a stable usage boundary was recorded.",
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
} else if (!turns[turn.turnRef]) {
|
|
87
|
+
turns[turn.turnRef] = {
|
|
88
|
+
status: "unavailable",
|
|
89
|
+
reason: "No reliable cumulative boundary was recorded for this historical turn.",
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (task.latestTurn?.result
|
|
95
|
+
&& hasTerminalLatestTurn(task)
|
|
96
|
+
&& !preserveAvailable) {
|
|
97
|
+
turns[task.latestTurn.turnRef] = { status: "calculating", updatedAt: now };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
threadId: task.threadId,
|
|
101
|
+
generationTurnRef: latestTurnRef,
|
|
102
|
+
generationTurnCount: generation.turnCount,
|
|
103
|
+
generationTerminal: generation.terminal,
|
|
104
|
+
zeroBaselineTurnRef: existing?.zeroBaselineTurnRef ?? (
|
|
105
|
+
task.turns.length === 1 && task.latestTurn?.result === null
|
|
106
|
+
? task.latestTurn.turnRef
|
|
107
|
+
: null
|
|
108
|
+
),
|
|
109
|
+
status: preserveAvailable ? "available" : "calculating",
|
|
110
|
+
updatedAt: preserveAvailable ? existing.updatedAt : now,
|
|
111
|
+
retryAfter: null,
|
|
112
|
+
task: existing?.task ?? null,
|
|
113
|
+
turns,
|
|
114
|
+
boundaries: existing?.boundaries ?? {},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function administrativeTask(task, existing = null, now = new Date().toISOString()) {
|
|
119
|
+
const usage = calculatingTask(task, existing, now);
|
|
120
|
+
const hasTaskProjection = existing?.task !== null && existing?.task !== undefined;
|
|
121
|
+
return {
|
|
122
|
+
...usage,
|
|
123
|
+
status: hasTaskProjection ? "available" : "unavailable",
|
|
124
|
+
...(hasTaskProjection ? {} : { reason: ADMINISTRATIVE_USAGE_REASON }),
|
|
125
|
+
updatedAt: now,
|
|
126
|
+
retryAfter: null,
|
|
127
|
+
task: existing?.task ?? null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function updateStore(workspace, taskId, transform) {
|
|
132
|
+
const release = await acquireWorkspaceLock(workspace);
|
|
133
|
+
try {
|
|
134
|
+
const store = await readUsageStore(workspace);
|
|
135
|
+
const next = await transform(store.tasks[taskId] ?? null);
|
|
136
|
+
store.tasks[taskId] = next;
|
|
137
|
+
await writeUsageStore(workspace, store);
|
|
138
|
+
return next;
|
|
139
|
+
} finally {
|
|
140
|
+
await release();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function snapshotFingerprint(snapshot) {
|
|
145
|
+
return JSON.stringify([
|
|
146
|
+
snapshot.inputTokens,
|
|
147
|
+
snapshot.cachedInputTokens,
|
|
148
|
+
snapshot.outputTokens,
|
|
149
|
+
snapshot.reasoningOutputTokens,
|
|
150
|
+
snapshot.totalTokens,
|
|
151
|
+
snapshot.estimatedCostUsd,
|
|
152
|
+
snapshot.sourceUpdatedAt,
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function snapshotSupersedes(current, incoming) {
|
|
157
|
+
if (!current) return true;
|
|
158
|
+
const currentSampledAt = Date.parse(current.sampledAt ?? 0);
|
|
159
|
+
const incomingSampledAt = Date.parse(incoming.sampledAt ?? 0);
|
|
160
|
+
if (!Number.isFinite(incomingSampledAt) || incomingSampledAt < currentSampledAt) return false;
|
|
161
|
+
return [
|
|
162
|
+
"inputTokens",
|
|
163
|
+
"cachedInputTokens",
|
|
164
|
+
"outputTokens",
|
|
165
|
+
"reasoningOutputTokens",
|
|
166
|
+
"totalTokens",
|
|
167
|
+
].every((field) => incoming[field] >= current[field]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function candidateHasAdvanced(task, existing, snapshot) {
|
|
171
|
+
const latestIndex = task.turns.findIndex((turn) => turn.turnRef === task.latestTurn?.turnRef);
|
|
172
|
+
if (latestIndex <= 0) return true;
|
|
173
|
+
const previousTurn = task.turns
|
|
174
|
+
.slice(0, latestIndex)
|
|
175
|
+
.reverse()
|
|
176
|
+
.find((turn) => !isAdministrativeTurn(turn));
|
|
177
|
+
const previousBoundary = previousTurn
|
|
178
|
+
? existing.boundaries[previousTurn.turnRef] ?? null
|
|
179
|
+
: null;
|
|
180
|
+
if (!previousBoundary) return true;
|
|
181
|
+
const delta = usageDelta(snapshot, previousBoundary);
|
|
182
|
+
return delta !== null && delta.totalTokens > 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function reconcileRecord(task, existing, snapshot, { boundaryReliable = true } = {}) {
|
|
186
|
+
const now = snapshot.sampledAt;
|
|
187
|
+
const boundaries = { ...(existing?.boundaries ?? {}) };
|
|
188
|
+
const recentTurns = task.turns.slice(-MAX_TRACKED_TURNS);
|
|
189
|
+
const turns = Object.fromEntries(recentTurns.flatMap((turn) => (
|
|
190
|
+
existing?.turns?.[turn.turnRef]
|
|
191
|
+
? [[turn.turnRef, existing.turns[turn.turnRef]]]
|
|
192
|
+
: []
|
|
193
|
+
)));
|
|
194
|
+
const terminalTurns = recentTurns.filter((turn) => (
|
|
195
|
+
!isAdministrativeTurn(turn)
|
|
196
|
+
&& turn.result !== null
|
|
197
|
+
&& TERMINAL_STATUSES.has(turn.result.status)
|
|
198
|
+
));
|
|
199
|
+
const generation = lifecycleGeneration(task);
|
|
200
|
+
for (const turn of recentTurns.filter(isAdministrativeTurn)) {
|
|
201
|
+
turns[turn.turnRef] = {
|
|
202
|
+
status: "unavailable",
|
|
203
|
+
reason: ADMINISTRATIVE_USAGE_REASON,
|
|
204
|
+
updatedAt: now,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
for (const turn of terminalTurns) {
|
|
208
|
+
if (!turns[turn.turnRef]) {
|
|
209
|
+
turns[turn.turnRef] = {
|
|
210
|
+
status: "unavailable",
|
|
211
|
+
reason: "No reliable cumulative boundary was recorded for this historical turn.",
|
|
212
|
+
updatedAt: now,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const latest = terminalTurns.at(-1);
|
|
218
|
+
if (latest && latest.turnRef === task.latestTurn?.turnRef && boundaryReliable) {
|
|
219
|
+
const index = task.turns.findIndex((turn) => turn.turnRef === latest.turnRef);
|
|
220
|
+
const previousTurn = index > 0
|
|
221
|
+
? task.turns.slice(0, index).reverse().find((turn) => !isAdministrativeTurn(turn)) ?? null
|
|
222
|
+
: null;
|
|
223
|
+
const previousBoundary = previousTurn ? boundaries[previousTurn.turnRef] ?? null : null;
|
|
224
|
+
const delta = index === 0
|
|
225
|
+
? (existing?.zeroBaselineTurnRef === latest.turnRef ? usageDelta(snapshot, null) : null)
|
|
226
|
+
: (previousBoundary !== null ? usageDelta(snapshot, previousBoundary) : null);
|
|
227
|
+
const advanced = index === 0 || (delta !== null && delta.totalTokens > 0);
|
|
228
|
+
if (advanced) boundaries[latest.turnRef] = snapshot;
|
|
229
|
+
turns[latest.turnRef] = delta === null || !advanced
|
|
230
|
+
? {
|
|
231
|
+
status: "unavailable",
|
|
232
|
+
reason: delta === null
|
|
233
|
+
? (previousTurn === null
|
|
234
|
+
? "No live zero-token boundary was recorded for this historical turn."
|
|
235
|
+
: "The preceding turn has no reliable cumulative boundary.")
|
|
236
|
+
: "Cumulative usage did not advance beyond the preceding turn.",
|
|
237
|
+
updatedAt: now,
|
|
238
|
+
}
|
|
239
|
+
: {
|
|
240
|
+
status: "available",
|
|
241
|
+
...delta,
|
|
242
|
+
provenance: snapshot.provenance,
|
|
243
|
+
sampledAt: snapshot.sampledAt,
|
|
244
|
+
sourceUpdatedAt: snapshot.sourceUpdatedAt,
|
|
245
|
+
updatedAt: now,
|
|
246
|
+
};
|
|
247
|
+
} else if (latest && latest.turnRef === task.latestTurn?.turnRef) {
|
|
248
|
+
turns[latest.turnRef] = {
|
|
249
|
+
status: "unavailable",
|
|
250
|
+
reason: "Usage did not stabilize before reconciliation finished.",
|
|
251
|
+
updatedAt: now,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
threadId: task.threadId,
|
|
257
|
+
generationTurnRef: task.latestTurn?.turnRef ?? null,
|
|
258
|
+
generationTurnCount: generation.turnCount,
|
|
259
|
+
generationTerminal: generation.terminal,
|
|
260
|
+
zeroBaselineTurnRef: existing?.zeroBaselineTurnRef ?? null,
|
|
261
|
+
status: "available",
|
|
262
|
+
updatedAt: now,
|
|
263
|
+
retryAfter: null,
|
|
264
|
+
task: snapshot,
|
|
265
|
+
turns,
|
|
266
|
+
boundaries,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createUsageTracker({
|
|
271
|
+
workspace,
|
|
272
|
+
readThreadUsage = readCcusageThreadUsage,
|
|
273
|
+
retryDelaysMs = [2_000, 3_000, 4_000, 1_000],
|
|
274
|
+
retryCooldownMs = 60_000,
|
|
275
|
+
setTimer = setTimeout,
|
|
276
|
+
} = {}) {
|
|
277
|
+
const jobs = new Map();
|
|
278
|
+
const observationChains = new Map();
|
|
279
|
+
let canonicalWorkspace = null;
|
|
280
|
+
|
|
281
|
+
const root = async () => {
|
|
282
|
+
canonicalWorkspace ??= await realpath(path.resolve(workspace));
|
|
283
|
+
return canonicalWorkspace;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const markCalculating = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
287
|
+
generationIsOlder(task, existing) ? existing : calculatingTask(task, existing)
|
|
288
|
+
));
|
|
289
|
+
const markAdministrative = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
290
|
+
generationIsOlder(task, existing) ? existing : administrativeTask(task, existing)
|
|
291
|
+
));
|
|
292
|
+
|
|
293
|
+
const reconcile = async (task, {
|
|
294
|
+
finalAttempt = false,
|
|
295
|
+
isCurrent = () => true,
|
|
296
|
+
job,
|
|
297
|
+
} = {}) => {
|
|
298
|
+
let snapshot;
|
|
299
|
+
try {
|
|
300
|
+
snapshot = await readThreadUsage(task.threadId);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (!finalAttempt) throw error;
|
|
303
|
+
if (!isCurrent()) return null;
|
|
304
|
+
return updateStore(await root(), task.id, (existing) => {
|
|
305
|
+
if (existing?.generationTurnRef !== task.latestTurn?.turnRef) return existing;
|
|
306
|
+
if (existing?.status === "available"
|
|
307
|
+
&& existing?.turns?.[task.latestTurn?.turnRef]?.status === "available") return existing;
|
|
308
|
+
const calculating = calculatingTask(task, existing);
|
|
309
|
+
return {
|
|
310
|
+
...calculating,
|
|
311
|
+
status: "unavailable",
|
|
312
|
+
reason: publicReason(error),
|
|
313
|
+
updatedAt: new Date().toISOString(),
|
|
314
|
+
retryAfter: new Date(Date.now() + retryCooldownMs).toISOString(),
|
|
315
|
+
turns: Object.fromEntries(Object.entries(calculating.turns).map(
|
|
316
|
+
([turnRef, usage]) => [turnRef, usage.status === "calculating"
|
|
317
|
+
? { status: "unavailable", reason: publicReason(error), updatedAt: new Date().toISOString() }
|
|
318
|
+
: usage],
|
|
319
|
+
)),
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (!isCurrent()) return null;
|
|
324
|
+
const fingerprint = snapshotFingerprint(snapshot);
|
|
325
|
+
const stable = job.previousFingerprint === fingerprint;
|
|
326
|
+
job.previousFingerprint = fingerprint;
|
|
327
|
+
if (!stable && !finalAttempt) return null;
|
|
328
|
+
if (stable && !finalAttempt) {
|
|
329
|
+
const store = await readUsageStore(await root());
|
|
330
|
+
if (!candidateHasAdvanced(task, store.tasks[task.id], snapshot)) return null;
|
|
331
|
+
}
|
|
332
|
+
return updateStore(await root(), task.id, (existing) => {
|
|
333
|
+
const latestTurnRef = task.latestTurn?.turnRef;
|
|
334
|
+
if (existing?.generationTurnRef !== latestTurnRef) return existing;
|
|
335
|
+
const supersedes = snapshotSupersedes(existing?.task, snapshot);
|
|
336
|
+
if (existing?.boundaries?.[latestTurnRef]
|
|
337
|
+
|| existing?.turns?.[latestTurnRef]?.status === "available") {
|
|
338
|
+
return supersedes
|
|
339
|
+
? { ...existing, status: "available", updatedAt: snapshot.sampledAt, task: snapshot }
|
|
340
|
+
: existing;
|
|
341
|
+
}
|
|
342
|
+
if (!supersedes) return existing;
|
|
343
|
+
return reconcileRecord(task, existing, snapshot, { boundaryReliable: stable });
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const schedule = (task, { immediate = false } = {}) => {
|
|
348
|
+
if (!task.threadId || jobs.has(task.id)) return;
|
|
349
|
+
const job = {
|
|
350
|
+
cancelled: false,
|
|
351
|
+
previousFingerprint: null,
|
|
352
|
+
turnRef: task.latestTurn?.turnRef ?? null,
|
|
353
|
+
};
|
|
354
|
+
let attempt = 0;
|
|
355
|
+
const run = async () => {
|
|
356
|
+
if (job.cancelled || jobs.get(task.id) !== job) return;
|
|
357
|
+
let complete = false;
|
|
358
|
+
try {
|
|
359
|
+
const result = await reconcile(task, {
|
|
360
|
+
finalAttempt: attempt >= retryDelaysMs.length,
|
|
361
|
+
isCurrent: () => !job.cancelled && jobs.get(task.id) === job,
|
|
362
|
+
job,
|
|
363
|
+
});
|
|
364
|
+
complete = result !== null;
|
|
365
|
+
} catch {
|
|
366
|
+
// Retry bounded transient analyzer failures.
|
|
367
|
+
}
|
|
368
|
+
if (job.cancelled || jobs.get(task.id) !== job) return;
|
|
369
|
+
if (complete || attempt >= retryDelaysMs.length) {
|
|
370
|
+
if (jobs.get(task.id) === job) jobs.delete(task.id);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const delay = retryDelaysMs[attempt] ?? 0;
|
|
374
|
+
attempt += 1;
|
|
375
|
+
const timer = setTimer(run, delay);
|
|
376
|
+
timer?.unref?.();
|
|
377
|
+
};
|
|
378
|
+
jobs.set(task.id, job);
|
|
379
|
+
const timer = setTimer(run, immediate ? 0 : retryDelaysMs[attempt++]);
|
|
380
|
+
timer?.unref?.();
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
observe(task) {
|
|
385
|
+
const previous = observationChains.get(task.id) ?? Promise.resolve();
|
|
386
|
+
const observation = previous.catch(() => {}).then(async () => {
|
|
387
|
+
if (!task.threadId) return null;
|
|
388
|
+
const active = jobs.get(task.id);
|
|
389
|
+
const latestTurnRef = task.latestTurn?.turnRef ?? null;
|
|
390
|
+
if (active && active.turnRef !== latestTurnRef) {
|
|
391
|
+
active.cancelled = true;
|
|
392
|
+
jobs.delete(task.id);
|
|
393
|
+
}
|
|
394
|
+
const usage = isAdministrativeTurn(task.latestTurn)
|
|
395
|
+
? await markAdministrative(task)
|
|
396
|
+
: await markCalculating(task);
|
|
397
|
+
if (hasTerminalLatestTurn(task)) schedule(task);
|
|
398
|
+
return usage;
|
|
399
|
+
});
|
|
400
|
+
observationChains.set(task.id, observation);
|
|
401
|
+
void observation.finally(() => {
|
|
402
|
+
if (observationChains.get(task.id) === observation) observationChains.delete(task.id);
|
|
403
|
+
}).catch(() => {});
|
|
404
|
+
return observation;
|
|
405
|
+
},
|
|
406
|
+
async get(task) {
|
|
407
|
+
const store = await readUsageStore(await root());
|
|
408
|
+
const usage = store.tasks[task.id] ?? null;
|
|
409
|
+
if (!task.threadId) return {
|
|
410
|
+
status: "unavailable",
|
|
411
|
+
reason: "This task has no linked Codex thread.",
|
|
412
|
+
task: null,
|
|
413
|
+
turns: {},
|
|
414
|
+
};
|
|
415
|
+
if (!task.latestTurn) return {
|
|
416
|
+
status: "unavailable",
|
|
417
|
+
reason: "No TaskChef turn has started yet.",
|
|
418
|
+
task: null,
|
|
419
|
+
turns: {},
|
|
420
|
+
};
|
|
421
|
+
const active = jobs.get(task.id);
|
|
422
|
+
const latestTurnRef = task.latestTurn?.turnRef ?? null;
|
|
423
|
+
if (active && active.turnRef !== latestTurnRef) {
|
|
424
|
+
active.cancelled = true;
|
|
425
|
+
jobs.delete(task.id);
|
|
426
|
+
}
|
|
427
|
+
if (isAdministrativeTurn(task.latestTurn)) {
|
|
428
|
+
const latestUsage = usage?.turns?.[task.latestTurn.turnRef];
|
|
429
|
+
if (usage?.generationTurnRef === task.latestTurn.turnRef
|
|
430
|
+
&& latestUsage?.status === "unavailable"
|
|
431
|
+
&& latestUsage.reason === ADMINISTRATIVE_USAGE_REASON) return usage;
|
|
432
|
+
const administrative = administrativeTask(task, usage);
|
|
433
|
+
void markAdministrative(task).catch(() => {});
|
|
434
|
+
return administrative;
|
|
435
|
+
}
|
|
436
|
+
if (!usage || usage.threadId !== task.threadId) {
|
|
437
|
+
const calculating = calculatingTask(task);
|
|
438
|
+
void markCalculating(task)
|
|
439
|
+
.then(() => {
|
|
440
|
+
if (hasTerminalLatestTurn(task)) schedule(task, { immediate: true });
|
|
441
|
+
})
|
|
442
|
+
.catch(() => {});
|
|
443
|
+
return calculating;
|
|
444
|
+
}
|
|
445
|
+
if (usage.generationTurnRef !== latestTurnRef) {
|
|
446
|
+
const calculating = calculatingTask(task, usage);
|
|
447
|
+
void markCalculating(task)
|
|
448
|
+
.then(() => {
|
|
449
|
+
if (hasTerminalLatestTurn(task)) schedule(task, { immediate: true });
|
|
450
|
+
})
|
|
451
|
+
.catch(() => {});
|
|
452
|
+
return calculating;
|
|
453
|
+
}
|
|
454
|
+
const latestTurnUsage = task.latestTurn
|
|
455
|
+
? usage.turns?.[task.latestTurn.turnRef]
|
|
456
|
+
: null;
|
|
457
|
+
if (!latestTurnUsage) {
|
|
458
|
+
const calculating = calculatingTask(task, usage);
|
|
459
|
+
void markCalculating(task)
|
|
460
|
+
.then(() => {
|
|
461
|
+
if (hasTerminalLatestTurn(task)) schedule(task, { immediate: true });
|
|
462
|
+
})
|
|
463
|
+
.catch(() => {});
|
|
464
|
+
return calculating;
|
|
465
|
+
}
|
|
466
|
+
if (hasTerminalLatestTurn(task) && usage.status !== "available") {
|
|
467
|
+
if (usage.status === "unavailable"
|
|
468
|
+
&& usage.retryAfter
|
|
469
|
+
&& Date.parse(usage.retryAfter) > Date.now()) return usage;
|
|
470
|
+
const calculating = calculatingTask(task, usage);
|
|
471
|
+
void markCalculating(task)
|
|
472
|
+
.then(() => schedule(task, { immediate: true }))
|
|
473
|
+
.catch(() => {});
|
|
474
|
+
return calculating;
|
|
475
|
+
}
|
|
476
|
+
return usage;
|
|
477
|
+
},
|
|
478
|
+
schedule,
|
|
479
|
+
};
|
|
480
|
+
}
|