taskchef 8.7.0 → 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.
Files changed (149) hide show
  1. package/.codex-plugin/plugin.json +7 -10
  2. package/.mcp.json +3 -2
  3. package/README.md +31 -158
  4. package/docs/mcp-app.md +21 -4
  5. package/docs/spec.md +38 -746
  6. package/docs/taskchef-next-storage.md +2 -2
  7. package/index.js +2 -99
  8. package/package.json +12 -49
  9. package/skills/delegate/SKILL.md +44 -0
  10. package/skills/delegate/agents/openai.yaml +4 -0
  11. package/src/codex-app.js +3 -152
  12. package/src/codex-session-scanner.js +2 -2
  13. package/src/file-lock.js +25 -0
  14. package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
  15. package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
  16. package/src/mcp-app/react/main.test.tsx +3 -3
  17. package/src/mcp-app/react/main.tsx +7 -7
  18. package/src/mcp-app.js +4 -4
  19. package/src/mcp.js +4 -542
  20. package/src/next-cache.js +2 -2
  21. package/src/next-github.js +3 -3
  22. package/src/next-notifications.js +2 -2
  23. package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
  24. package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
  25. package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
  26. package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
  27. package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
  28. package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
  29. package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
  30. package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
  31. package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
  32. package/src/{dashboard/react → ui}/presentation.ts +1 -1
  33. package/src/version.js +2 -1
  34. package/BACKLOG.md +0 -48
  35. package/assets/e2e-benchmark-example.json +0 -50
  36. package/assets/taskchef-dispatcher-instructions.md +0 -44
  37. package/bin/taskchef.js +0 -20
  38. package/docs/advanced-guide.md +0 -497
  39. package/docs/dashboard-lifecycle.md +0 -149
  40. package/docs/dashboard-ui.md +0 -40
  41. package/docs/firstmate-taskchef-comparison.md +0 -283
  42. package/docs/images/ccusage-token-consumption.png +0 -0
  43. package/docs/images/dashboard-identity.jpg +0 -0
  44. package/docs/images/dashboard-token-pending.jpg +0 -0
  45. package/docs/images/interrupted-turn-recovery.jpg +0 -0
  46. package/docs/images/notification-event-snapshots.jpg +0 -0
  47. package/docs/images/readme-board.png +0 -0
  48. package/docs/images/readme-completed.png +0 -0
  49. package/docs/images/readme-list.png +0 -0
  50. package/docs/images/readme-needs-input.png +0 -0
  51. package/docs/images/readme-roles.png +0 -0
  52. package/docs/images/reported-work-duration-desktop.jpg +0 -0
  53. package/docs/images/reported-work-duration-mobile.jpg +0 -0
  54. package/docs/images/result-history-dashboard.jpg +0 -0
  55. package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
  56. package/docs/images/taskchef-unified-status-popup.jpg +0 -0
  57. package/docs/workflows.md +0 -476
  58. package/mcp/dashboard-session.js +0 -51
  59. package/scripts/benchmark-dispatch-prepare.js +0 -90
  60. package/scripts/e2e-benchmark.js +0 -206
  61. package/scripts/roles/resolve_roles.py +0 -293
  62. package/skills/taskchef-bootstrap/SKILL.md +0 -96
  63. package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
  64. package/skills/taskchef-bootstrap/references/project-index.md +0 -154
  65. package/skills/taskchef-copilot/SKILL.md +0 -75
  66. package/skills/taskchef-copilot/agents/openai.yaml +0 -4
  67. package/skills/taskchef-dashboard/SKILL.md +0 -24
  68. package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
  69. package/skills/taskchef-delegate/SKILL.md +0 -102
  70. package/skills/taskchef-delegate/agents/openai.yaml +0 -4
  71. package/skills/taskchef-delegate/references/model-roles.md +0 -29
  72. package/skills/taskchef-executor/SKILL.md +0 -154
  73. package/skills/taskchef-executor/agents/openai.yaml +0 -4
  74. package/skills/taskchef-executor/references/compatibility.md +0 -22
  75. package/skills/taskchef-executor/references/ending-actions.md +0 -26
  76. package/src/backups.js +0 -359
  77. package/src/cli.js +0 -733
  78. package/src/config-audit.js +0 -99
  79. package/src/config-mutations.js +0 -106
  80. package/src/dashboard/actions.js +0 -136
  81. package/src/dashboard/app.js +0 -1172
  82. package/src/dashboard/dist/app.js +0 -269
  83. package/src/dashboard/dist/app.js.map +0 -1
  84. package/src/dashboard/dist/index.html +0 -16
  85. package/src/dashboard/dist/styles.css +0 -1
  86. package/src/dashboard/index.html +0 -148
  87. package/src/dashboard/react/App.test.tsx +0 -346
  88. package/src/dashboard/react/App.tsx +0 -515
  89. package/src/dashboard/react/api.test.ts +0 -40
  90. package/src/dashboard/react/api.ts +0 -103
  91. package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
  92. package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
  93. package/src/dashboard/react/components/ModelSettings.tsx +0 -106
  94. package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
  95. package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
  96. package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
  97. package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
  98. package/src/dashboard/react/components/TaskDetail.tsx +0 -271
  99. package/src/dashboard/react/components/UsagePanel.tsx +0 -96
  100. package/src/dashboard/react/index.html +0 -15
  101. package/src/dashboard/react/main.tsx +0 -12
  102. package/src/dashboard/react/overlay-state.test.ts +0 -110
  103. package/src/dashboard/react/overlay-state.ts +0 -60
  104. package/src/dashboard/react/presentation.test.tsx +0 -571
  105. package/src/dashboard/styles.css +0 -261
  106. package/src/dashboard-control.js +0 -34
  107. package/src/dashboard-manager.js +0 -339
  108. package/src/dashboard-session-process.js +0 -88
  109. package/src/dashboard-session.js +0 -86
  110. package/src/dashboard.js +0 -1303
  111. package/src/delegation.js +0 -277
  112. package/src/github.js +0 -107
  113. package/src/model-roles.js +0 -70
  114. package/src/recorded-usage-tracker.js +0 -92
  115. package/src/usage-tracker.js +0 -650
  116. package/src/usage.js +0 -367
  117. package/src/workspace-path.js +0 -48
  118. package/src/workspace.js +0 -3629
  119. /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
  120. /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
  121. /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
  122. /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
  123. /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
  124. /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
  125. /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
  126. /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
  127. /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
  128. /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
  129. /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
  130. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
  131. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
  132. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
  133. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
  134. /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
  135. /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
  136. /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
  137. /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
  138. /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
  139. /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
  140. /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
  141. /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
  142. /package/src/{dashboard → ui}/github-links.js +0 -0
  143. /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
  144. /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
  145. /package/src/{dashboard → ui}/state.js +0 -0
  146. /package/src/{dashboard/react → ui}/styles.css +0 -0
  147. /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
  148. /package/src/{dashboard → ui}/time.js +0 -0
  149. /package/src/{dashboard/react → ui}/types.ts +0 -0
@@ -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
- }