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
package/src/usage.js DELETED
@@ -1,367 +0,0 @@
1
- import { lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { randomUUID } from "node:crypto";
4
-
5
- const USAGE_FILE_NAME = ".taskchef-usage.json";
6
- const USAGE_SCHEMA_VERSION = 2;
7
- const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
8
- const TOKEN_FIELDS = [
9
- "inputTokens",
10
- "cachedInputTokens",
11
- "outputTokens",
12
- "reasoningOutputTokens",
13
- "totalTokens",
14
- ];
15
- const USAGE_STATUSES = new Set(["calculating", "available", "unavailable"]);
16
- const MAX_USAGE_FILE_BYTES = 16 * 1024 * 1024;
17
- const USAGE_WRITE_BUDGET_BYTES = 8 * 1024 * 1024;
18
- const MAX_PERSISTED_TASKS = 1_000;
19
- const MAX_PERSISTED_TURNS = 250;
20
-
21
- function nonNegativeNumber(value, name) {
22
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
23
- throw new Error(`${name} must be a non-negative number`);
24
- }
25
- return value;
26
- }
27
-
28
- function matchingSession(session, threadId) {
29
- const identity = `${session.sessionId ?? ""} ${session.sessionFile ?? ""}`;
30
- const [primaryThreadId] = identity.match(UUID_PATTERN) ?? [];
31
- return primaryThreadId?.toLowerCase() === threadId;
32
- }
33
-
34
- function normalizeModelUsage(models, name) {
35
- if (!models || typeof models !== "object" || Array.isArray(models)) {
36
- throw new Error(`${name}.models must be an object`);
37
- }
38
- return Object.fromEntries(Object.entries(models).map(([model, usage]) => {
39
- if (model.length === 0 || model.length > 256) throw new Error(`${name}.models has an invalid model name`);
40
- return [model, {
41
- inputTokens: nonNegativeNumber(usage.inputTokens, `${name}.models.${model}.inputTokens`),
42
- cachedInputTokens: nonNegativeNumber(usage.cacheReadTokens, `${name}.models.${model}.cacheReadTokens`),
43
- outputTokens: nonNegativeNumber(usage.outputTokens, `${name}.models.${model}.outputTokens`),
44
- reasoningOutputTokens: nonNegativeNumber(
45
- usage.reasoningOutputTokens,
46
- `${name}.models.${model}.reasoningOutputTokens`,
47
- ),
48
- totalTokens: nonNegativeNumber(usage.totalTokens, `${name}.models.${model}.totalTokens`),
49
- }];
50
- }));
51
- }
52
-
53
- function addModelUsage(target, source) {
54
- for (const [model, usage] of Object.entries(source)) {
55
- if (!Object.hasOwn(target, model)) {
56
- target[model] = Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
57
- }
58
- for (const field of TOKEN_FIELDS) target[model][field] += usage[field];
59
- }
60
- }
61
-
62
- function timestampOrNull(value, name) {
63
- if (value === null) return null;
64
- if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
65
- throw new Error(`${name} must be an ISO timestamp or null`);
66
- }
67
- return value;
68
- }
69
-
70
- function normalizeProvenance(value, name, { includeSessionCount = false } = {}) {
71
- if (!value || !["taskchef", "ccusage"].includes(value.provider)) {
72
- throw new Error(`${name}.provenance is invalid`);
73
- }
74
- return {
75
- provider: value.provider,
76
- version: typeof value.version === "string" ? value.version.slice(0, 64) : null,
77
- pricingMode: ["online", "offline", "standard"].includes(value.pricingMode)
78
- ? value.pricingMode
79
- : null,
80
- ...(includeSessionCount ? {
81
- sessionCount: nonNegativeNumber(value.sessionCount, `${name}.provenance.sessionCount`),
82
- } : {}),
83
- };
84
- }
85
-
86
- function normalizeStoredSnapshot(value, name) {
87
- if (!value || typeof value !== "object" || Array.isArray(value)) {
88
- throw new Error(`${name} must be an object`);
89
- }
90
- const snapshot = Object.fromEntries(TOKEN_FIELDS.map((field) => [
91
- field,
92
- nonNegativeNumber(value[field], `${name}.${field}`),
93
- ]));
94
- if (value.estimatedCostUsd !== null) {
95
- snapshot.estimatedCostUsd = nonNegativeNumber(value.estimatedCostUsd, `${name}.estimatedCostUsd`);
96
- } else {
97
- snapshot.estimatedCostUsd = null;
98
- }
99
- snapshot.costStatus = value.costStatus === "estimated" ? "estimated" : "unavailable";
100
- const models = normalizeModelUsage(Object.fromEntries(Object.entries(value.models ?? {}).map(
101
- ([model, usage]) => [model, {
102
- ...usage,
103
- cacheReadTokens: usage.cachedInputTokens,
104
- }],
105
- )), name);
106
- if (Object.keys(models).length > 64 || Object.keys(models).some((model) => model.length > 256)) {
107
- throw new Error(`${name}.models exceeds the usage cache limit`);
108
- }
109
- snapshot.models = models;
110
- if (value.partial === true) snapshot.partial = true;
111
- if (value.costPartial === true) snapshot.costPartial = true;
112
- snapshot.provenance = normalizeProvenance(value.provenance, name, { includeSessionCount: true });
113
- snapshot.sampledAt = timestampOrNull(value.sampledAt, `${name}.sampledAt`);
114
- snapshot.sourceUpdatedAt = timestampOrNull(value.sourceUpdatedAt, `${name}.sourceUpdatedAt`);
115
- return snapshot;
116
- }
117
-
118
- function normalizeStoredTurn(value, name) {
119
- if (!value || typeof value !== "object" || !USAGE_STATUSES.has(value.status)) {
120
- throw new Error(`${name} has an invalid status`);
121
- }
122
- const updatedAt = timestampOrNull(value.updatedAt, `${name}.updatedAt`);
123
- if (value.status === "available") {
124
- const normalized = Object.fromEntries(TOKEN_FIELDS.map((field) => [
125
- field,
126
- nonNegativeNumber(value[field], `${name}.${field}`),
127
- ]));
128
- normalized.estimatedCostUsd = value.estimatedCostUsd === null
129
- ? null
130
- : nonNegativeNumber(value.estimatedCostUsd, `${name}.estimatedCostUsd`);
131
- const provenance = normalizeProvenance(value.provenance, name);
132
- if (provenance.pricingMode === null) {
133
- normalized.estimatedCostUsd = null;
134
- }
135
- return {
136
- status: "available",
137
- ...(value.partial === true ? { partial: true } : {}),
138
- ...(value.costPartial === true ? { costPartial: true } : {}),
139
- ...normalized,
140
- costStatus: normalized.estimatedCostUsd === null ? "unavailable" : "estimated",
141
- provenance,
142
- sampledAt: timestampOrNull(value.sampledAt, `${name}.sampledAt`),
143
- sourceUpdatedAt: timestampOrNull(value.sourceUpdatedAt, `${name}.sourceUpdatedAt`),
144
- updatedAt,
145
- };
146
- }
147
- return {
148
- status: value.status,
149
- ...(value.status === "unavailable" ? {
150
- reason: typeof value.reason === "string" ? value.reason.slice(0, 256) : "Usage is unavailable.",
151
- } : {}),
152
- updatedAt,
153
- };
154
- }
155
-
156
- function normalizeStoredRecord(value, name) {
157
- if (!value || typeof value !== "object" || !USAGE_STATUSES.has(value.status)) {
158
- throw new Error(`${name} is invalid`);
159
- }
160
- const threadId = String(value.threadId ?? "").toLowerCase();
161
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(threadId)) {
162
- throw new Error(`${name}.threadId is invalid`);
163
- }
164
- const generationTurnRef = value.generationTurnRef ?? null;
165
- if (generationTurnRef !== null
166
- && (typeof generationTurnRef !== "string" || generationTurnRef.length === 0 || generationTurnRef.length > 256)) {
167
- throw new Error(`${name}.generationTurnRef is invalid`);
168
- }
169
- const zeroBaselineTurnRef = value.zeroBaselineTurnRef ?? null;
170
- if (zeroBaselineTurnRef !== null
171
- && (typeof zeroBaselineTurnRef !== "string" || zeroBaselineTurnRef.length === 0 || zeroBaselineTurnRef.length > 256)) {
172
- throw new Error(`${name}.zeroBaselineTurnRef is invalid`);
173
- }
174
- const generationTurnCount = nonNegativeNumber(
175
- value.generationTurnCount ?? 0,
176
- `${name}.generationTurnCount`,
177
- );
178
- const generationTerminal = value.generationTerminal === true;
179
- const normalizeMap = (entries, normalizer, mapName) => {
180
- if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
181
- throw new Error(`${mapName} must be an object`);
182
- }
183
- const pairs = Object.entries(entries);
184
- if (pairs.length > 10_000) throw new Error(`${mapName} exceeds the usage cache limit`);
185
- return Object.fromEntries(pairs.map(([key, item]) => [key, normalizer(item, `${mapName}.${key}`)]));
186
- };
187
- return {
188
- threadId,
189
- generationTurnRef,
190
- generationTurnCount,
191
- generationTerminal,
192
- zeroBaselineTurnRef,
193
- status: value.status,
194
- updatedAt: timestampOrNull(value.updatedAt, `${name}.updatedAt`),
195
- retryAfter: timestampOrNull(value.retryAfter ?? null, `${name}.retryAfter`),
196
- task: value.task === null ? null : normalizeStoredSnapshot(value.task, `${name}.task`),
197
- turns: normalizeMap(value.turns, normalizeStoredTurn, `${name}.turns`),
198
- boundaries: normalizeMap(value.boundaries, normalizeStoredSnapshot, `${name}.boundaries`),
199
- ...(value.status === "unavailable" ? {
200
- reason: typeof value.reason === "string" ? value.reason.slice(0, 256) : "Usage is unavailable.",
201
- } : {}),
202
- };
203
- }
204
-
205
- export function aggregateCcusageSessions(payload, threadId, {
206
- sampledAt = new Date().toISOString(),
207
- version = null,
208
- pricingMode = null,
209
- } = {}) {
210
- if (!payload || typeof payload !== "object" || !Array.isArray(payload.sessions)) {
211
- throw new Error("ccusage output must contain a sessions array");
212
- }
213
- const normalizedThreadId = String(threadId).toLowerCase();
214
- const sessions = payload.sessions.filter((session) => matchingSession(session, normalizedThreadId));
215
- if (sessions.length === 0) throw new Error("ccusage could not resolve this Codex thread");
216
-
217
- const usage = Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
218
- const models = Object.create(null);
219
- let estimatedCostUsd = 0;
220
- let hasUnpricedUsage = false;
221
- let sourceUpdatedAt = null;
222
- for (const [index, session] of sessions.entries()) {
223
- const name = `ccusage.sessions[${index}]`;
224
- usage.inputTokens += nonNegativeNumber(session.inputTokens, `${name}.inputTokens`);
225
- usage.cachedInputTokens += nonNegativeNumber(session.cacheReadTokens, `${name}.cacheReadTokens`);
226
- usage.outputTokens += nonNegativeNumber(session.outputTokens, `${name}.outputTokens`);
227
- usage.reasoningOutputTokens += nonNegativeNumber(
228
- session.reasoningOutputTokens,
229
- `${name}.reasoningOutputTokens`,
230
- );
231
- const sessionTotalTokens = nonNegativeNumber(session.totalTokens, `${name}.totalTokens`);
232
- usage.totalTokens += sessionTotalTokens;
233
- const sessionCost = session.costUSD === undefined || session.costUSD === null
234
- ? null
235
- : nonNegativeNumber(session.costUSD, `${name}.costUSD`);
236
- if (sessionCost !== null) estimatedCostUsd += sessionCost;
237
- if (sessionTotalTokens > 0 && (sessionCost === null || sessionCost === 0)) hasUnpricedUsage = true;
238
- addModelUsage(models, normalizeModelUsage(session.models, name));
239
- if (typeof session.lastActivity === "string" && !Number.isNaN(Date.parse(session.lastActivity))) {
240
- if (sourceUpdatedAt === null || Date.parse(session.lastActivity) > Date.parse(sourceUpdatedAt)) {
241
- sourceUpdatedAt = session.lastActivity;
242
- }
243
- }
244
- }
245
-
246
- return {
247
- ...usage,
248
- estimatedCostUsd: hasUnpricedUsage ? null : estimatedCostUsd,
249
- costStatus: hasUnpricedUsage ? "unavailable" : "estimated",
250
- models,
251
- provenance: {
252
- provider: "ccusage",
253
- version,
254
- pricingMode,
255
- sessionCount: sessions.length,
256
- },
257
- sampledAt,
258
- sourceUpdatedAt,
259
- };
260
- }
261
-
262
- export function usageStorePath(workspace) {
263
- return path.join(workspace, USAGE_FILE_NAME);
264
- }
265
-
266
- export async function readUsageStore(workspace) {
267
- const filePath = usageStorePath(workspace);
268
- const details = await lstat(filePath).catch((error) => {
269
- if (error.code === "ENOENT") return null;
270
- throw error;
271
- });
272
- if (details === null) return { schemaVersion: USAGE_SCHEMA_VERSION, tasks: {} };
273
- if (details.isSymbolicLink() || !details.isFile()) {
274
- throw new Error("TaskChef usage cache must be a regular file");
275
- }
276
- if (details.size > MAX_USAGE_FILE_BYTES) {
277
- return { schemaVersion: USAGE_SCHEMA_VERSION, tasks: {} };
278
- }
279
- const value = JSON.parse(await readFile(filePath, "utf8"));
280
- if (![1, USAGE_SCHEMA_VERSION].includes(value?.schemaVersion)
281
- || !value.tasks
282
- || typeof value.tasks !== "object") {
283
- throw new Error("TaskChef usage cache has an unsupported schema");
284
- }
285
- const tasks = Object.entries(value.tasks);
286
- if (tasks.length > 2_000) return { schemaVersion: USAGE_SCHEMA_VERSION, tasks: {} };
287
- return {
288
- schemaVersion: USAGE_SCHEMA_VERSION,
289
- tasks: Object.fromEntries(tasks.map(([taskId, record]) => [
290
- taskId,
291
- normalizeStoredRecord(record, `usage task ${taskId}`),
292
- ])),
293
- };
294
- }
295
-
296
- function compactUsageRecord(record) {
297
- const turnEntries = Object.entries(record.turns ?? {}).slice(-MAX_PERSISTED_TURNS);
298
- const boundaryEntries = Object.entries(record.boundaries ?? {}).slice(-1);
299
- return {
300
- ...record,
301
- turns: Object.fromEntries(turnEntries),
302
- boundaries: Object.fromEntries(boundaryEntries),
303
- };
304
- }
305
-
306
- export function compactUsageStore(store) {
307
- const entries = Object.entries(store.tasks ?? {})
308
- .sort(([, left], [, right]) => Date.parse(right.updatedAt ?? 0) - Date.parse(left.updatedAt ?? 0))
309
- .slice(0, MAX_PERSISTED_TASKS);
310
- const tasks = {};
311
- let approximateBytes = 64;
312
- for (const [taskId, record] of entries) {
313
- const compacted = compactUsageRecord(record);
314
- const entryBytes = Buffer.byteLength(JSON.stringify([taskId, compacted]), "utf8");
315
- if (Object.keys(tasks).length > 0 && approximateBytes + entryBytes > USAGE_WRITE_BUDGET_BYTES) break;
316
- tasks[taskId] = compacted;
317
- approximateBytes += entryBytes;
318
- }
319
- return { schemaVersion: USAGE_SCHEMA_VERSION, tasks };
320
- }
321
-
322
- export async function writeUsageStore(workspace, store) {
323
- await mkdir(workspace, { recursive: true });
324
- const filePath = usageStorePath(workspace);
325
- const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
326
- const serialized = `${JSON.stringify(compactUsageStore(store), null, 2)}\n`;
327
- if (Buffer.byteLength(serialized, "utf8") > MAX_USAGE_FILE_BYTES) {
328
- throw new Error("TaskChef usage cache cannot be compacted below the size limit");
329
- }
330
- await writeFile(temporaryPath, serialized, {
331
- encoding: "utf8",
332
- flag: "wx",
333
- mode: 0o600,
334
- });
335
- try {
336
- await rename(temporaryPath, filePath);
337
- } catch (error) {
338
- await unlink(temporaryPath).catch(() => {});
339
- throw error;
340
- }
341
- }
342
-
343
- export function usageDelta(current, previous = null) {
344
- const baseline = previous ?? Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
345
- const delta = {};
346
- for (const field of TOKEN_FIELDS) {
347
- const value = current[field] - baseline[field];
348
- if (!Number.isFinite(value) || value < 0) return null;
349
- delta[field] = value;
350
- }
351
- const samePricingSource = previous === null || ["version", "pricingMode"]
352
- .every((field) => current.provenance?.[field] === previous.provenance?.[field]);
353
- let estimatedCostUsd = !samePricingSource
354
- || current.estimatedCostUsd === null
355
- || (previous !== null && previous.estimatedCostUsd === null)
356
- ? null
357
- : current.estimatedCostUsd - (previous?.estimatedCostUsd ?? 0);
358
- if (estimatedCostUsd !== null && (!Number.isFinite(estimatedCostUsd) || estimatedCostUsd < 0)) {
359
- estimatedCostUsd = null;
360
- }
361
- if (estimatedCostUsd === 0 && delta.totalTokens > 0) estimatedCostUsd = null;
362
- return {
363
- ...delta,
364
- estimatedCostUsd,
365
- costStatus: estimatedCostUsd === null ? "unavailable" : "estimated",
366
- };
367
- }
@@ -1,48 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
-
4
- export const TASKCHEF_WORKSPACE_ENV = "TASKCHEF_WORKSPACE";
5
-
6
- export function defaultWorkspacePath({ homedir = os.homedir() } = {}) {
7
- return path.join(homedir, ".agents", "taskchef");
8
- }
9
-
10
- function expandHome(value, homedir) {
11
- if (value === "~") return homedir;
12
- if (value.startsWith("~/") || value.startsWith("~\\")) {
13
- return path.join(homedir, value.slice(2));
14
- }
15
- return value;
16
- }
17
-
18
- export function resolveWorkspacePath({
19
- explicit = null,
20
- env = process.env,
21
- homedir = os.homedir(),
22
- cwd = process.cwd(),
23
- } = {}) {
24
- let source = "default";
25
- let value = defaultWorkspacePath({ homedir });
26
- if (env[TASKCHEF_WORKSPACE_ENV] !== undefined) {
27
- if (typeof env[TASKCHEF_WORKSPACE_ENV] !== "string" || env[TASKCHEF_WORKSPACE_ENV].trim() === "") {
28
- throw new Error(`${TASKCHEF_WORKSPACE_ENV} must be a non-empty path`);
29
- }
30
- source = "environment";
31
- value = env[TASKCHEF_WORKSPACE_ENV];
32
- }
33
- if (explicit !== null) {
34
- if (typeof explicit !== "string" || explicit.trim() === "") {
35
- throw new Error("--workspace must be a non-empty path");
36
- }
37
- source = "explicit";
38
- value = explicit;
39
- }
40
- const expanded = expandHome(value, homedir);
41
- if (source === "environment" && !path.isAbsolute(expanded)) {
42
- throw new Error(`${TASKCHEF_WORKSPACE_ENV} must be an absolute path or start with ~/`);
43
- }
44
- return {
45
- workspace: path.resolve(cwd, expanded),
46
- source,
47
- };
48
- }