taskchef 8.7.1 → 9.0.1

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