taskchef 8.7.1 → 9.0.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 +7 -10
- package/.mcp.json +3 -2
- package/README.md +31 -158
- package/docs/mcp-app.md +3 -3
- package/docs/spec.md +38 -746
- package/docs/taskchef-next-storage.md +2 -2
- package/index.js +2 -99
- package/package.json +12 -49
- package/skills/delegate/SKILL.md +44 -0
- package/skills/delegate/agents/openai.yaml +4 -0
- package/src/codex-app.js +3 -152
- package/src/codex-session-scanner.js +2 -2
- package/src/file-lock.js +25 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
- package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
- package/src/mcp-app/react/main.test.tsx +3 -3
- package/src/mcp-app/react/main.tsx +7 -7
- package/src/mcp-app.js +2 -2
- package/src/mcp.js +4 -542
- package/src/next-cache.js +2 -2
- package/src/next-github.js +3 -3
- package/src/next-notifications.js +2 -2
- package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
- package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
- package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
- package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
- package/src/{dashboard/react → ui}/presentation.ts +1 -1
- package/src/version.js +0 -1
- package/BACKLOG.md +0 -48
- package/assets/e2e-benchmark-example.json +0 -50
- package/assets/taskchef-dispatcher-instructions.md +0 -44
- package/bin/taskchef.js +0 -20
- package/docs/advanced-guide.md +0 -497
- package/docs/dashboard-lifecycle.md +0 -149
- package/docs/dashboard-ui.md +0 -40
- package/docs/firstmate-taskchef-comparison.md +0 -283
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/images/dashboard-identity.jpg +0 -0
- package/docs/images/dashboard-token-pending.jpg +0 -0
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/readme-board.png +0 -0
- package/docs/images/readme-completed.png +0 -0
- package/docs/images/readme-list.png +0 -0
- package/docs/images/readme-needs-input.png +0 -0
- package/docs/images/readme-roles.png +0 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/workflows.md +0 -476
- package/mcp/dashboard-session.js +0 -51
- package/scripts/benchmark-dispatch-prepare.js +0 -90
- package/scripts/e2e-benchmark.js +0 -206
- package/scripts/roles/resolve_roles.py +0 -293
- package/skills/taskchef-bootstrap/SKILL.md +0 -96
- package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
- package/skills/taskchef-bootstrap/references/project-index.md +0 -154
- package/skills/taskchef-copilot/SKILL.md +0 -75
- package/skills/taskchef-copilot/agents/openai.yaml +0 -4
- package/skills/taskchef-dashboard/SKILL.md +0 -24
- package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/SKILL.md +0 -102
- package/skills/taskchef-delegate/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/references/model-roles.md +0 -29
- package/skills/taskchef-executor/SKILL.md +0 -154
- package/skills/taskchef-executor/agents/openai.yaml +0 -4
- package/skills/taskchef-executor/references/compatibility.md +0 -22
- package/skills/taskchef-executor/references/ending-actions.md +0 -26
- package/src/backups.js +0 -359
- package/src/cli.js +0 -733
- package/src/config-audit.js +0 -99
- package/src/config-mutations.js +0 -106
- package/src/dashboard/actions.js +0 -136
- package/src/dashboard/app.js +0 -1172
- package/src/dashboard/dist/app.js +0 -269
- package/src/dashboard/dist/app.js.map +0 -1
- package/src/dashboard/dist/index.html +0 -16
- package/src/dashboard/dist/styles.css +0 -1
- package/src/dashboard/index.html +0 -148
- package/src/dashboard/react/App.test.tsx +0 -346
- package/src/dashboard/react/App.tsx +0 -515
- package/src/dashboard/react/api.test.ts +0 -40
- package/src/dashboard/react/api.ts +0 -103
- package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
- package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
- package/src/dashboard/react/components/ModelSettings.tsx +0 -106
- package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
- package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
- package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
- package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
- package/src/dashboard/react/components/TaskDetail.tsx +0 -271
- package/src/dashboard/react/components/UsagePanel.tsx +0 -96
- package/src/dashboard/react/index.html +0 -15
- package/src/dashboard/react/main.tsx +0 -12
- package/src/dashboard/react/overlay-state.test.ts +0 -110
- package/src/dashboard/react/overlay-state.ts +0 -60
- package/src/dashboard/react/presentation.test.tsx +0 -571
- package/src/dashboard/styles.css +0 -261
- package/src/dashboard-control.js +0 -34
- package/src/dashboard-manager.js +0 -339
- package/src/dashboard-session-process.js +0 -88
- package/src/dashboard-session.js +0 -86
- package/src/dashboard.js +0 -1303
- package/src/delegation.js +0 -277
- package/src/github.js +0 -107
- package/src/model-roles.js +0 -70
- package/src/recorded-usage-tracker.js +0 -92
- package/src/usage-tracker.js +0 -650
- package/src/usage.js +0 -367
- package/src/workspace-path.js +0 -48
- package/src/workspace.js +0 -3629
- /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
- /package/src/{dashboard → ui}/github-links.js +0 -0
- /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
- /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
- /package/src/{dashboard → ui}/state.js +0 -0
- /package/src/{dashboard/react → ui}/styles.css +0 -0
- /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
- /package/src/{dashboard → ui}/time.js +0 -0
- /package/src/{dashboard/react → ui}/types.ts +0 -0
package/src/usage-tracker.js
DELETED
|
@@ -1,650 +0,0 @@
|
|
|
1
|
-
import { createRecordedUsageTracker } from "./recorded-usage-tracker.js";
|
|
2
|
-
import { realpath } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import { acquireWorkspaceLock } from "./workspace.js";
|
|
6
|
-
import {
|
|
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 isInterruptedTurn(turn) {
|
|
21
|
-
return turn?.result?.status === "interrupted";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function hasTerminalLatestTurn(task) {
|
|
25
|
-
return task.latestTurn !== null
|
|
26
|
-
&& task.latestTurn !== undefined
|
|
27
|
-
&& !isAdministrativeTurn(task.latestTurn)
|
|
28
|
-
&& task.latestTurn.result !== null
|
|
29
|
-
&& TERMINAL_STATUSES.has(task.latestTurn.result.status);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function lifecycleGeneration(task) {
|
|
33
|
-
return {
|
|
34
|
-
turnCount: task.turns.length,
|
|
35
|
-
terminal: hasTerminalLatestTurn(task),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function generationIsOlder(task, existing) {
|
|
40
|
-
if (!existing) return false;
|
|
41
|
-
const incoming = lifecycleGeneration(task);
|
|
42
|
-
if (incoming.turnCount !== existing.generationTurnCount) {
|
|
43
|
-
return incoming.turnCount < existing.generationTurnCount;
|
|
44
|
-
}
|
|
45
|
-
return !incoming.terminal && existing.generationTerminal;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function publicReason(error) {
|
|
49
|
-
const message = String(error?.message ?? "");
|
|
50
|
-
if (/not installed/i.test(message)) return "Usage provider is not installed.";
|
|
51
|
-
if (/could not resolve/i.test(message)) return "No matching Codex usage session was found.";
|
|
52
|
-
if (/timed out/i.test(message)) return "Usage calculation did not finish in time.";
|
|
53
|
-
return "Codex usage is unavailable.";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function calculatingTask(task, existing = null, now = new Date().toISOString()) {
|
|
57
|
-
const latestTurnRef = task.latestTurn?.turnRef ?? null;
|
|
58
|
-
const preserveAvailable = existing?.generationTurnRef === latestTurnRef
|
|
59
|
-
&& existing?.status === "available"
|
|
60
|
-
&& existing?.turns?.[latestTurnRef]?.status === "available";
|
|
61
|
-
const recentTurns = task.turns.slice(-MAX_TRACKED_TURNS);
|
|
62
|
-
const turns = Object.fromEntries(recentTurns.flatMap((turn) => (
|
|
63
|
-
existing?.turns?.[turn.turnRef]
|
|
64
|
-
? [[turn.turnRef, existing.turns[turn.turnRef]]]
|
|
65
|
-
: []
|
|
66
|
-
)));
|
|
67
|
-
const generation = lifecycleGeneration(task);
|
|
68
|
-
for (const turn of recentTurns) {
|
|
69
|
-
if (isAdministrativeTurn(turn)) {
|
|
70
|
-
turns[turn.turnRef] = {
|
|
71
|
-
status: "unavailable",
|
|
72
|
-
reason: ADMINISTRATIVE_USAGE_REASON,
|
|
73
|
-
updatedAt: now,
|
|
74
|
-
};
|
|
75
|
-
} else if (turn.result === null) {
|
|
76
|
-
turns[turn.turnRef] = { status: "calculating", updatedAt: now };
|
|
77
|
-
} else if (turn.result.status === "interrupted") {
|
|
78
|
-
turns[turn.turnRef] = {
|
|
79
|
-
status: "unavailable",
|
|
80
|
-
reason: "The turn ended without a terminal usage boundary.",
|
|
81
|
-
updatedAt: now,
|
|
82
|
-
};
|
|
83
|
-
} else if (turns[turn.turnRef]?.status === "calculating"
|
|
84
|
-
&& turn.turnRef !== task.latestTurn?.turnRef) {
|
|
85
|
-
turns[turn.turnRef] = {
|
|
86
|
-
status: "unavailable",
|
|
87
|
-
reason: "A newer turn started before a stable usage boundary was recorded.",
|
|
88
|
-
updatedAt: now,
|
|
89
|
-
};
|
|
90
|
-
} else if (!turns[turn.turnRef]) {
|
|
91
|
-
turns[turn.turnRef] = {
|
|
92
|
-
status: "unavailable",
|
|
93
|
-
reason: "No reliable cumulative boundary was recorded for this historical turn.",
|
|
94
|
-
updatedAt: now,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (task.latestTurn?.result
|
|
99
|
-
&& hasTerminalLatestTurn(task)
|
|
100
|
-
&& !preserveAvailable) {
|
|
101
|
-
turns[task.latestTurn.turnRef] = { status: "calculating", updatedAt: now };
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
threadId: task.threadId,
|
|
105
|
-
generationTurnRef: latestTurnRef,
|
|
106
|
-
generationTurnCount: generation.turnCount,
|
|
107
|
-
generationTerminal: generation.terminal,
|
|
108
|
-
zeroBaselineTurnRef: existing?.zeroBaselineTurnRef ?? (
|
|
109
|
-
task.turns.length === 1 && task.latestTurn?.result === null
|
|
110
|
-
? task.latestTurn.turnRef
|
|
111
|
-
: null
|
|
112
|
-
),
|
|
113
|
-
status: preserveAvailable ? "available" : "calculating",
|
|
114
|
-
updatedAt: preserveAvailable ? existing.updatedAt : now,
|
|
115
|
-
retryAfter: null,
|
|
116
|
-
task: existing?.task ?? null,
|
|
117
|
-
turns,
|
|
118
|
-
boundaries: existing?.boundaries ?? {},
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function administrativeTask(task, existing = null, now = new Date().toISOString()) {
|
|
123
|
-
const usage = calculatingTask(task, existing, now);
|
|
124
|
-
const hasTaskProjection = existing?.task !== null && existing?.task !== undefined;
|
|
125
|
-
return {
|
|
126
|
-
...usage,
|
|
127
|
-
status: hasTaskProjection ? "available" : "unavailable",
|
|
128
|
-
...(hasTaskProjection ? {} : { reason: ADMINISTRATIVE_USAGE_REASON }),
|
|
129
|
-
updatedAt: now,
|
|
130
|
-
retryAfter: null,
|
|
131
|
-
task: existing?.task ?? null,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
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
|
-
|
|
148
|
-
async function updateStore(workspace, taskId, transform) {
|
|
149
|
-
const release = await acquireWorkspaceLock(workspace);
|
|
150
|
-
try {
|
|
151
|
-
const store = await readUsageStore(workspace);
|
|
152
|
-
const next = await transform(store.tasks[taskId] ?? null);
|
|
153
|
-
store.tasks[taskId] = next;
|
|
154
|
-
await writeUsageStore(workspace, store);
|
|
155
|
-
return next;
|
|
156
|
-
} finally {
|
|
157
|
-
await release();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function snapshotFingerprint(snapshot) {
|
|
162
|
-
return JSON.stringify([
|
|
163
|
-
snapshot.inputTokens,
|
|
164
|
-
snapshot.cachedInputTokens,
|
|
165
|
-
snapshot.outputTokens,
|
|
166
|
-
snapshot.reasoningOutputTokens,
|
|
167
|
-
snapshot.totalTokens,
|
|
168
|
-
snapshot.estimatedCostUsd,
|
|
169
|
-
snapshot.sourceUpdatedAt,
|
|
170
|
-
]);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function snapshotSupersedes(current, incoming) {
|
|
174
|
-
if (!current) return true;
|
|
175
|
-
const currentSampledAt = Date.parse(current.sampledAt ?? 0);
|
|
176
|
-
const incomingSampledAt = Date.parse(incoming.sampledAt ?? 0);
|
|
177
|
-
if (!Number.isFinite(incomingSampledAt) || incomingSampledAt < currentSampledAt) return false;
|
|
178
|
-
return [
|
|
179
|
-
"inputTokens",
|
|
180
|
-
"cachedInputTokens",
|
|
181
|
-
"outputTokens",
|
|
182
|
-
"reasoningOutputTokens",
|
|
183
|
-
"totalTokens",
|
|
184
|
-
].every((field) => incoming[field] >= current[field]);
|
|
185
|
-
}
|
|
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 = `The usage provider${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
|
-
|
|
228
|
-
function candidateHasAdvanced(task, existing, snapshot) {
|
|
229
|
-
const latestIndex = task.turns.findIndex((turn) => turn.turnRef === task.latestTurn?.turnRef);
|
|
230
|
-
if (latestIndex <= 0) return true;
|
|
231
|
-
const previousTurn = task.turns
|
|
232
|
-
.slice(0, latestIndex)
|
|
233
|
-
.reverse()
|
|
234
|
-
.find((turn) => !isAdministrativeTurn(turn));
|
|
235
|
-
const previousBoundary = previousTurn
|
|
236
|
-
? existing.boundaries[previousTurn.turnRef] ?? null
|
|
237
|
-
: null;
|
|
238
|
-
if (!previousBoundary) return true;
|
|
239
|
-
const delta = usageDelta(snapshot, previousBoundary);
|
|
240
|
-
return delta !== null && delta.totalTokens > 0;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function reconcileRecord(task, existing, snapshot, { boundaryReliable = true } = {}) {
|
|
244
|
-
const now = snapshot.sampledAt;
|
|
245
|
-
const boundaries = { ...(existing?.boundaries ?? {}) };
|
|
246
|
-
const recentTurns = task.turns.slice(-MAX_TRACKED_TURNS);
|
|
247
|
-
const turns = Object.fromEntries(recentTurns.flatMap((turn) => (
|
|
248
|
-
existing?.turns?.[turn.turnRef]
|
|
249
|
-
? [[turn.turnRef, existing.turns[turn.turnRef]]]
|
|
250
|
-
: []
|
|
251
|
-
)));
|
|
252
|
-
const terminalTurns = recentTurns.filter((turn) => (
|
|
253
|
-
!isAdministrativeTurn(turn)
|
|
254
|
-
&& turn.result !== null
|
|
255
|
-
&& TERMINAL_STATUSES.has(turn.result.status)
|
|
256
|
-
));
|
|
257
|
-
const generation = lifecycleGeneration(task);
|
|
258
|
-
for (const turn of recentTurns.filter(isAdministrativeTurn)) {
|
|
259
|
-
turns[turn.turnRef] = {
|
|
260
|
-
status: "unavailable",
|
|
261
|
-
reason: ADMINISTRATIVE_USAGE_REASON,
|
|
262
|
-
updatedAt: now,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
for (const turn of terminalTurns) {
|
|
266
|
-
if (!turns[turn.turnRef]) {
|
|
267
|
-
turns[turn.turnRef] = {
|
|
268
|
-
status: "unavailable",
|
|
269
|
-
reason: "No reliable cumulative boundary was recorded for this historical turn.",
|
|
270
|
-
updatedAt: now,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const latest = terminalTurns.at(-1);
|
|
276
|
-
if (latest && latest.turnRef === task.latestTurn?.turnRef && boundaryReliable) {
|
|
277
|
-
const index = task.turns.findIndex((turn) => turn.turnRef === latest.turnRef);
|
|
278
|
-
const previousTurn = index > 0
|
|
279
|
-
? task.turns.slice(0, index).reverse().find((turn) => !isAdministrativeTurn(turn)) ?? null
|
|
280
|
-
: null;
|
|
281
|
-
const previousBoundary = previousTurn ? boundaries[previousTurn.turnRef] ?? null : null;
|
|
282
|
-
const delta = index === 0
|
|
283
|
-
? (existing?.zeroBaselineTurnRef === latest.turnRef ? usageDelta(snapshot, null) : null)
|
|
284
|
-
: (previousBoundary !== null ? usageDelta(snapshot, previousBoundary) : null);
|
|
285
|
-
const advanced = index === 0 || (delta !== null && delta.totalTokens > 0);
|
|
286
|
-
if (advanced) boundaries[latest.turnRef] = snapshot;
|
|
287
|
-
turns[latest.turnRef] = delta === null || !advanced
|
|
288
|
-
? {
|
|
289
|
-
status: "unavailable",
|
|
290
|
-
reason: delta === null
|
|
291
|
-
? (previousTurn === null
|
|
292
|
-
? "No live zero-token boundary was recorded for this historical turn."
|
|
293
|
-
: "The preceding turn has no reliable cumulative boundary.")
|
|
294
|
-
: "Cumulative usage did not advance beyond the preceding turn.",
|
|
295
|
-
updatedAt: now,
|
|
296
|
-
}
|
|
297
|
-
: {
|
|
298
|
-
status: "available",
|
|
299
|
-
...delta,
|
|
300
|
-
provenance: snapshot.provenance,
|
|
301
|
-
sampledAt: snapshot.sampledAt,
|
|
302
|
-
sourceUpdatedAt: snapshot.sourceUpdatedAt,
|
|
303
|
-
updatedAt: now,
|
|
304
|
-
};
|
|
305
|
-
} else if (latest && latest.turnRef === task.latestTurn?.turnRef) {
|
|
306
|
-
turns[latest.turnRef] = {
|
|
307
|
-
status: "unavailable",
|
|
308
|
-
reason: "Usage did not stabilize before reconciliation finished.",
|
|
309
|
-
updatedAt: now,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
threadId: task.threadId,
|
|
315
|
-
generationTurnRef: task.latestTurn?.turnRef ?? null,
|
|
316
|
-
generationTurnCount: generation.turnCount,
|
|
317
|
-
generationTerminal: generation.terminal,
|
|
318
|
-
zeroBaselineTurnRef: existing?.zeroBaselineTurnRef ?? null,
|
|
319
|
-
status: "available",
|
|
320
|
-
updatedAt: now,
|
|
321
|
-
retryAfter: null,
|
|
322
|
-
task: snapshot,
|
|
323
|
-
turns,
|
|
324
|
-
boundaries,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function createUsageTracker({
|
|
329
|
-
workspace,
|
|
330
|
-
maxConcurrentJobs = 2,
|
|
331
|
-
readThreadUsage,
|
|
332
|
-
retryDelaysMs = [2_000, 3_000, 4_000, 1_000],
|
|
333
|
-
retryCooldownMs = 60_000,
|
|
334
|
-
setTimer = setTimeout,
|
|
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
|
-
}
|
|
339
|
-
if (!readThreadUsage) return createRecordedUsageTracker({ workspace, maxConcurrentJobs });
|
|
340
|
-
const jobs = new Map();
|
|
341
|
-
const observationChains = new Map();
|
|
342
|
-
const readyJobs = [];
|
|
343
|
-
let activeJobs = 0;
|
|
344
|
-
let canonicalWorkspace = null;
|
|
345
|
-
let closed = false;
|
|
346
|
-
let preloadChain = Promise.resolve();
|
|
347
|
-
|
|
348
|
-
const root = async () => {
|
|
349
|
-
canonicalWorkspace ??= await realpath(path.resolve(workspace));
|
|
350
|
-
return canonicalWorkspace;
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const markCalculating = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
354
|
-
generationIsOlder(task, existing) ? existing : calculatingTask(task, existing)
|
|
355
|
-
));
|
|
356
|
-
const markAdministrative = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
357
|
-
generationIsOlder(task, existing) ? existing : administrativeTask(task, existing)
|
|
358
|
-
));
|
|
359
|
-
const markInterrupted = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
360
|
-
generationIsOlder(task, existing) ? existing : interruptedTask(task, existing)
|
|
361
|
-
));
|
|
362
|
-
|
|
363
|
-
const reconcile = async (task, {
|
|
364
|
-
finalAttempt = false,
|
|
365
|
-
isCurrent = () => true,
|
|
366
|
-
job,
|
|
367
|
-
} = {}) => {
|
|
368
|
-
let snapshot;
|
|
369
|
-
try {
|
|
370
|
-
snapshot = await readThreadUsage(task.threadId);
|
|
371
|
-
} catch (error) {
|
|
372
|
-
if (!finalAttempt) throw error;
|
|
373
|
-
if (!isCurrent()) return null;
|
|
374
|
-
return updateStore(await root(), task.id, (existing) => {
|
|
375
|
-
if (existing?.generationTurnRef !== task.latestTurn?.turnRef) return existing;
|
|
376
|
-
if (existing?.status === "available"
|
|
377
|
-
&& existing?.turns?.[task.latestTurn?.turnRef]?.status === "available") return existing;
|
|
378
|
-
const calculating = calculatingTask(task, existing);
|
|
379
|
-
return {
|
|
380
|
-
...calculating,
|
|
381
|
-
status: "unavailable",
|
|
382
|
-
reason: publicReason(error),
|
|
383
|
-
updatedAt: new Date().toISOString(),
|
|
384
|
-
retryAfter: new Date(Date.now() + retryCooldownMs).toISOString(),
|
|
385
|
-
turns: Object.fromEntries(Object.entries(calculating.turns).map(
|
|
386
|
-
([turnRef, usage]) => [turnRef, usage.status === "calculating"
|
|
387
|
-
? { status: "unavailable", reason: publicReason(error), updatedAt: new Date().toISOString() }
|
|
388
|
-
: usage],
|
|
389
|
-
)),
|
|
390
|
-
};
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (!isCurrent()) return null;
|
|
394
|
-
const fingerprint = snapshotFingerprint(snapshot);
|
|
395
|
-
const stable = job.previousFingerprint === fingerprint;
|
|
396
|
-
job.previousFingerprint = fingerprint;
|
|
397
|
-
if (!stable && !finalAttempt) return null;
|
|
398
|
-
if (stable && !finalAttempt) {
|
|
399
|
-
const store = await readUsageStore(await root());
|
|
400
|
-
if (!candidateHasAdvanced(task, store.tasks[task.id], snapshot)) return null;
|
|
401
|
-
}
|
|
402
|
-
return updateStore(await root(), task.id, (existing) => {
|
|
403
|
-
const latestTurnRef = task.latestTurn?.turnRef;
|
|
404
|
-
if (existing?.generationTurnRef !== latestTurnRef) return existing;
|
|
405
|
-
if (stable && snapshotCorrects(existing?.task, snapshot)) {
|
|
406
|
-
return correctedRecord(task, snapshot);
|
|
407
|
-
}
|
|
408
|
-
const supersedes = snapshotSupersedes(existing?.task, snapshot);
|
|
409
|
-
if (existing?.boundaries?.[latestTurnRef]
|
|
410
|
-
|| existing?.turns?.[latestTurnRef]?.status === "available") {
|
|
411
|
-
return supersedes
|
|
412
|
-
? { ...existing, status: "available", updatedAt: snapshot.sampledAt, task: snapshot }
|
|
413
|
-
: existing;
|
|
414
|
-
}
|
|
415
|
-
if (!supersedes) return existing;
|
|
416
|
-
return reconcileRecord(task, existing, snapshot, { boundaryReliable: stable });
|
|
417
|
-
});
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const drainReadyJobs = () => {
|
|
421
|
-
while (!closed && activeJobs < maxConcurrentJobs && readyJobs.length > 0) {
|
|
422
|
-
const job = readyJobs.shift();
|
|
423
|
-
job.queued = false;
|
|
424
|
-
if (job.cancelled || jobs.get(job.task.id) !== job) {
|
|
425
|
-
for (const resolve of job.waiters.splice(0)) resolve();
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
const waiters = job.waiters.splice(0);
|
|
429
|
-
activeJobs += 1;
|
|
430
|
-
void job.run().finally(() => {
|
|
431
|
-
activeJobs -= 1;
|
|
432
|
-
for (const resolve of waiters) resolve();
|
|
433
|
-
drainReadyJobs();
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const queueJob = (job) => {
|
|
439
|
-
if (closed || job.cancelled || jobs.get(job.task.id) !== job) return Promise.resolve();
|
|
440
|
-
return new Promise((resolve) => {
|
|
441
|
-
job.waiters.push(resolve);
|
|
442
|
-
if (!job.queued) {
|
|
443
|
-
job.queued = true;
|
|
444
|
-
readyJobs.push(job);
|
|
445
|
-
drainReadyJobs();
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const schedule = (task, { immediate = false } = {}) => {
|
|
451
|
-
if (closed || !task.threadId || jobs.has(task.id)) return;
|
|
452
|
-
const job = {
|
|
453
|
-
cancelled: false,
|
|
454
|
-
previousFingerprint: null,
|
|
455
|
-
queued: false,
|
|
456
|
-
run: null,
|
|
457
|
-
task,
|
|
458
|
-
turnRef: task.latestTurn?.turnRef ?? null,
|
|
459
|
-
waiters: [],
|
|
460
|
-
};
|
|
461
|
-
let attempt = 0;
|
|
462
|
-
job.run = async () => {
|
|
463
|
-
if (job.cancelled || jobs.get(task.id) !== job) return;
|
|
464
|
-
let complete = false;
|
|
465
|
-
try {
|
|
466
|
-
const result = await reconcile(task, {
|
|
467
|
-
finalAttempt: attempt >= retryDelaysMs.length,
|
|
468
|
-
isCurrent: () => !job.cancelled && jobs.get(task.id) === job,
|
|
469
|
-
job,
|
|
470
|
-
});
|
|
471
|
-
complete = result !== null;
|
|
472
|
-
} catch {
|
|
473
|
-
// Retry bounded transient analyzer failures.
|
|
474
|
-
}
|
|
475
|
-
if (job.cancelled || jobs.get(task.id) !== job) return;
|
|
476
|
-
if (complete || attempt >= retryDelaysMs.length) {
|
|
477
|
-
if (jobs.get(task.id) === job) jobs.delete(task.id);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const delay = retryDelaysMs[attempt] ?? 0;
|
|
481
|
-
attempt += 1;
|
|
482
|
-
const timer = setTimer(() => queueJob(job), delay);
|
|
483
|
-
timer?.unref?.();
|
|
484
|
-
};
|
|
485
|
-
jobs.set(task.id, job);
|
|
486
|
-
const timer = setTimer(() => queueJob(job), immediate ? 0 : retryDelaysMs[attempt++]);
|
|
487
|
-
timer?.unref?.();
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
const observe = (task) => {
|
|
491
|
-
if (closed) return Promise.resolve(null);
|
|
492
|
-
const previous = observationChains.get(task.id) ?? Promise.resolve();
|
|
493
|
-
const observation = previous.catch(() => {}).then(async () => {
|
|
494
|
-
if (closed || !task.threadId) return null;
|
|
495
|
-
const active = jobs.get(task.id);
|
|
496
|
-
const latestTurnRef = task.latestTurn?.turnRef ?? null;
|
|
497
|
-
if (active && active.turnRef !== latestTurnRef) {
|
|
498
|
-
active.cancelled = true;
|
|
499
|
-
jobs.delete(task.id);
|
|
500
|
-
}
|
|
501
|
-
const usage = isAdministrativeTurn(task.latestTurn)
|
|
502
|
-
? await markAdministrative(task)
|
|
503
|
-
: isInterruptedTurn(task.latestTurn)
|
|
504
|
-
? await markInterrupted(task)
|
|
505
|
-
: await markCalculating(task);
|
|
506
|
-
if (hasTerminalLatestTurn(task)) schedule(task);
|
|
507
|
-
return usage;
|
|
508
|
-
});
|
|
509
|
-
observationChains.set(task.id, observation);
|
|
510
|
-
void observation.finally(() => {
|
|
511
|
-
if (observationChains.get(task.id) === observation) observationChains.delete(task.id);
|
|
512
|
-
}).catch(() => {});
|
|
513
|
-
return observation;
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
const needsPreload = (task, usage, now = Date.now()) => {
|
|
517
|
-
if (!task.threadId || !task.latestTurn) return false;
|
|
518
|
-
const latestTurnRef = task.latestTurn.turnRef ?? null;
|
|
519
|
-
if (isAdministrativeTurn(task.latestTurn)) {
|
|
520
|
-
return !usage
|
|
521
|
-
|| usage.threadId !== task.threadId
|
|
522
|
-
|| usage.generationTurnRef !== latestTurnRef;
|
|
523
|
-
}
|
|
524
|
-
if (isInterruptedTurn(task.latestTurn)) {
|
|
525
|
-
const latestUsage = usage?.turns?.[latestTurnRef];
|
|
526
|
-
const expectedStatus = usage?.task ? "available" : "unavailable";
|
|
527
|
-
return !usage
|
|
528
|
-
|| usage.threadId !== task.threadId
|
|
529
|
-
|| usage.generationTurnRef !== latestTurnRef
|
|
530
|
-
|| usage.status !== expectedStatus
|
|
531
|
-
|| latestUsage?.status !== "unavailable";
|
|
532
|
-
}
|
|
533
|
-
if (!usage || usage.threadId !== task.threadId || usage.generationTurnRef !== latestTurnRef) {
|
|
534
|
-
return true;
|
|
535
|
-
}
|
|
536
|
-
if (!hasTerminalLatestTurn(task)) return false;
|
|
537
|
-
if (usage.status === "available") return false;
|
|
538
|
-
if (usage.status === "calculating") return true;
|
|
539
|
-
if (usage.status === "unavailable") {
|
|
540
|
-
return !usage.retryAfter || Date.parse(usage.retryAfter) <= now;
|
|
541
|
-
}
|
|
542
|
-
return false;
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
return {
|
|
546
|
-
close() {
|
|
547
|
-
closed = true;
|
|
548
|
-
readyJobs.length = 0;
|
|
549
|
-
for (const job of jobs.values()) {
|
|
550
|
-
job.cancelled = true;
|
|
551
|
-
for (const resolve of job.waiters.splice(0)) resolve();
|
|
552
|
-
}
|
|
553
|
-
jobs.clear();
|
|
554
|
-
},
|
|
555
|
-
observe,
|
|
556
|
-
preload(tasks) {
|
|
557
|
-
preloadChain = preloadChain.catch(() => {}).then(async () => {
|
|
558
|
-
if (closed) return;
|
|
559
|
-
const store = await readUsageStore(await root());
|
|
560
|
-
for (const task of tasks) {
|
|
561
|
-
if (needsPreload(task, store.tasks[task.id] ?? null)) await observe(task);
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
return preloadChain;
|
|
565
|
-
},
|
|
566
|
-
async get(task) {
|
|
567
|
-
const store = await readUsageStore(await root());
|
|
568
|
-
const usage = store.tasks[task.id] ?? null;
|
|
569
|
-
if (!task.threadId) return {
|
|
570
|
-
status: "unavailable",
|
|
571
|
-
reason: "This task has no linked Codex thread.",
|
|
572
|
-
task: null,
|
|
573
|
-
turns: {},
|
|
574
|
-
};
|
|
575
|
-
if (!task.latestTurn) return {
|
|
576
|
-
status: "unavailable",
|
|
577
|
-
reason: "No TaskChef turn has started yet.",
|
|
578
|
-
task: null,
|
|
579
|
-
turns: {},
|
|
580
|
-
};
|
|
581
|
-
const active = jobs.get(task.id);
|
|
582
|
-
const latestTurnRef = task.latestTurn?.turnRef ?? null;
|
|
583
|
-
if (active && active.turnRef !== latestTurnRef) {
|
|
584
|
-
active.cancelled = true;
|
|
585
|
-
jobs.delete(task.id);
|
|
586
|
-
}
|
|
587
|
-
if (isAdministrativeTurn(task.latestTurn)) {
|
|
588
|
-
const latestUsage = usage?.turns?.[task.latestTurn.turnRef];
|
|
589
|
-
if (usage?.generationTurnRef === task.latestTurn.turnRef
|
|
590
|
-
&& latestUsage?.status === "unavailable"
|
|
591
|
-
&& latestUsage.reason === ADMINISTRATIVE_USAGE_REASON) return usage;
|
|
592
|
-
const administrative = administrativeTask(task, usage);
|
|
593
|
-
void markAdministrative(task).catch(() => {});
|
|
594
|
-
return administrative;
|
|
595
|
-
}
|
|
596
|
-
if (isInterruptedTurn(task.latestTurn)) {
|
|
597
|
-
const latestUsage = usage?.turns?.[task.latestTurn.turnRef];
|
|
598
|
-
const expectedStatus = usage?.task ? "available" : "unavailable";
|
|
599
|
-
if (usage?.generationTurnRef === task.latestTurn.turnRef
|
|
600
|
-
&& usage.status === expectedStatus
|
|
601
|
-
&& latestUsage?.status === "unavailable") return usage;
|
|
602
|
-
const interrupted = interruptedTask(task, usage);
|
|
603
|
-
void markInterrupted(task).catch(() => {});
|
|
604
|
-
return interrupted;
|
|
605
|
-
}
|
|
606
|
-
if (!usage || usage.threadId !== task.threadId) {
|
|
607
|
-
const calculating = calculatingTask(task);
|
|
608
|
-
void markCalculating(task)
|
|
609
|
-
.then(() => {
|
|
610
|
-
if (hasTerminalLatestTurn(task)) schedule(task, { immediate: true });
|
|
611
|
-
})
|
|
612
|
-
.catch(() => {});
|
|
613
|
-
return calculating;
|
|
614
|
-
}
|
|
615
|
-
if (usage.generationTurnRef !== latestTurnRef) {
|
|
616
|
-
const calculating = calculatingTask(task, usage);
|
|
617
|
-
void markCalculating(task)
|
|
618
|
-
.then(() => {
|
|
619
|
-
if (hasTerminalLatestTurn(task)) schedule(task, { immediate: true });
|
|
620
|
-
})
|
|
621
|
-
.catch(() => {});
|
|
622
|
-
return calculating;
|
|
623
|
-
}
|
|
624
|
-
const latestTurnUsage = task.latestTurn
|
|
625
|
-
? usage.turns?.[task.latestTurn.turnRef]
|
|
626
|
-
: null;
|
|
627
|
-
if (!latestTurnUsage) {
|
|
628
|
-
const calculating = calculatingTask(task, usage);
|
|
629
|
-
void markCalculating(task)
|
|
630
|
-
.then(() => {
|
|
631
|
-
if (hasTerminalLatestTurn(task)) schedule(task, { immediate: true });
|
|
632
|
-
})
|
|
633
|
-
.catch(() => {});
|
|
634
|
-
return calculating;
|
|
635
|
-
}
|
|
636
|
-
if (hasTerminalLatestTurn(task) && usage.status !== "available") {
|
|
637
|
-
if (usage.status === "unavailable"
|
|
638
|
-
&& usage.retryAfter
|
|
639
|
-
&& Date.parse(usage.retryAfter) > Date.now()) return usage;
|
|
640
|
-
const calculating = calculatingTask(task, usage);
|
|
641
|
-
void markCalculating(task)
|
|
642
|
-
.then(() => schedule(task, { immediate: true }))
|
|
643
|
-
.catch(() => {});
|
|
644
|
-
return calculating;
|
|
645
|
-
}
|
|
646
|
-
return usage;
|
|
647
|
-
},
|
|
648
|
-
schedule,
|
|
649
|
-
};
|
|
650
|
-
}
|