taskchef 9.6.0 → 9.7.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.
- package/.codex-plugin/plugin.json +1 -1
- package/docs/mcp-app.md +57 -22
- package/package.json +1 -1
- package/scripts/migrate-taskchef-state.js +1 -1
- package/src/codex-session-scanner.js +5 -5
- package/src/codex-session-usage.js +21 -8
- package/src/mcp-app/dist/index.html +75 -75
- package/src/mcp-app/react/UsageView.test.tsx +67 -2
- package/src/mcp-app/react/UsageView.tsx +89 -17
- package/src/mcp-app/react/main.test.tsx +27 -8
- package/src/mcp-app/react/main.tsx +1 -1
- package/src/mcp-app/react/styles.css +7 -2
- package/src/mcp-app.js +23 -8
- package/src/usage-analytics.js +17 -2
- package/src/usage-history-cache.js +38 -0
package/docs/mcp-app.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TaskChef sidebar
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The released app is **TaskChef**. The development app is **TaskChef Dev**, with plugin and MCP server ID `taskchef-dev`.
|
|
4
4
|
|
|
5
5
|
The native dashboard requires Node.js 22.18+, 23.2+, or 24+ for local Codex SQLite access.
|
|
6
6
|
|
|
@@ -196,7 +196,7 @@ Use this order for every development change:
|
|
|
196
196
|
For a full Dev update, give the staging package a unique development version
|
|
197
197
|
in both `package.json` and `.codex-plugin/plugin.json` (and package-lock metadata
|
|
198
198
|
if present), such as `9.0.0-dev.1`. Set only the staging manifest's plugin name
|
|
199
|
-
to `taskchef-
|
|
199
|
+
to `taskchef-dev`; keep the source release manifest named `taskchef`.
|
|
200
200
|
Before installing the staging package through the local development marketplace,
|
|
201
201
|
run:
|
|
202
202
|
|
|
@@ -205,12 +205,21 @@ node scripts/prepare-mcp-app-preview.js /path/to/staging/plugin
|
|
|
205
205
|
```
|
|
206
206
|
|
|
207
207
|
The script sets the copied plugin’s display name to **TaskChef Dev** and
|
|
208
|
-
normalizes its MCP server key to `taskchef-
|
|
208
|
+
normalizes its MCP server key to `taskchef-dev`. It refuses release manifests. The sidebar tool reads its
|
|
209
209
|
name from that manifest. The release checkout and release package keep
|
|
210
210
|
**TaskChef**. Run this step again whenever rebuilding the development package.
|
|
211
211
|
Do not register a standalone preview MCP server alongside these two plugins.
|
|
212
212
|
Restart Codex after changes to plugin identity or backend code.
|
|
213
213
|
|
|
214
|
+
When migrating an existing Dev installation, prepare a new staging package with
|
|
215
|
+
plugin ID `taskchef-dev` and a unique Dev version. Set its local marketplace
|
|
216
|
+
name and plugin entry to `taskchef-dev`, then add the marketplace and install
|
|
217
|
+
`taskchef-dev@taskchef-dev`. Verify the new installation before removing
|
|
218
|
+
`taskchef-next@taskchef-next-preview` and the old marketplace registration.
|
|
219
|
+
Both editions continue to share `~/.agents/taskchef`; preserve that state and
|
|
220
|
+
the legacy GitHub credential key. After restarting Codex, the Dev skill is
|
|
221
|
+
`$taskchef-dev:delegate` and its MCP namespace is `taskchef_dev`.
|
|
222
|
+
|
|
214
223
|
## Local UI review without restarting Codex
|
|
215
224
|
|
|
216
225
|
For UI-only changes, update the HTML in the installation that the running MCP
|
|
@@ -222,10 +231,10 @@ folder. Its next resource read then fails with `ENOENT`, and the sidebar shows
|
|
|
222
231
|
From the source checkout, run:
|
|
223
232
|
|
|
224
233
|
```sh
|
|
225
|
-
npm run update:mcp-app-preview -- /path/to/the/running/taskchef-
|
|
234
|
+
npm run update:mcp-app-preview -- /path/to/the/running/taskchef-dev/version
|
|
226
235
|
```
|
|
227
236
|
|
|
228
|
-
The helper builds the UI, checks the development plugin ID is `taskchef-
|
|
237
|
+
The helper builds the UI, checks the development plugin ID is `taskchef-dev`, and replaces only
|
|
229
238
|
`src/mcp-app/dist/index.html` atomically. It preserves the plugin version, server
|
|
230
239
|
code, dependencies, and local data. It refuses missing or redirected HTML paths.
|
|
231
240
|
Then right-click **TaskChef Dev** in the Codex sidebar and choose **Refresh**.
|
|
@@ -351,13 +360,13 @@ uses this repository's GitHub token; no shared-marketplace deploy key is needed.
|
|
|
351
360
|
If the push loses a race with another main update, rerun the Release workflow
|
|
352
361
|
on main to retry after checking the current published version. Do not force push.
|
|
353
362
|
|
|
354
|
-
Keep TaskChef Dev in a separate local marketplace named `taskchef-
|
|
355
|
-
with plugin ID `taskchef-
|
|
363
|
+
Keep TaskChef Dev in a separate local marketplace named `taskchef-dev`,
|
|
364
|
+
with plugin ID `taskchef-dev` and display name **TaskChef Dev**. Store that
|
|
356
365
|
marketplace and its staging packages in a persistent local folder, outside
|
|
357
366
|
this public checkout. Its catalog uses `source.source: local` with a path to
|
|
358
367
|
the prepared staging package. Add that folder with
|
|
359
368
|
`codex plugin marketplace add /path/to/local-dev-marketplace`, then install
|
|
360
|
-
`codex plugin add taskchef-
|
|
369
|
+
`codex plugin add taskchef-dev@taskchef-dev`. This can coexist with
|
|
361
370
|
`taskchef@taskchef`. Never rename the source release manifest for Dev.
|
|
362
371
|
|
|
363
372
|
Retire the old shared catalog entry only after this repository's catalog is
|
|
@@ -367,7 +376,7 @@ a marketplace does not automatically move their installed plugin identity.
|
|
|
367
376
|
|
|
368
377
|
## Usage analytics
|
|
369
378
|
|
|
370
|
-

|
|
371
380
|
|
|
372
381
|
The Board/Usage selector sits beside the logo in the navbar, separated from
|
|
373
382
|
Board filters by a divider. It opens an on-demand historical usage view. Ranges are
|
|
@@ -376,8 +385,16 @@ Last month (the full previous calendar month). The default is 30 days; the
|
|
|
376
385
|
selected range is remembered locally when Usage is reopened.
|
|
377
386
|
There is no custom range. Dates use the view's local timezone, including DST.
|
|
378
387
|
Range changes and reopening Usage query one shared history snapshot without
|
|
379
|
-
rescanning logs. The snapshot is
|
|
380
|
-
|
|
388
|
+
rescanning logs. The combined snapshot is also saved atomically with private file
|
|
389
|
+
permissions under `~/.agents/taskchef/cache/usage-history/`, scoped by Codex home
|
|
390
|
+
and the all-sources accounting policy. It contains the fields needed for charts, including
|
|
391
|
+
weekly readings, and no message content or per-file parsing records. After an
|
|
392
|
+
MCP server restart, the first Usage request loads a compatible disk snapshot
|
|
393
|
+
and returns saved charts before refreshing history in the background. Range and
|
|
394
|
+
timezone changes share that refresh. A missing, damaged, or incompatible snapshot
|
|
395
|
+
falls back to a normal scan. A failed background refresh retains the last good
|
|
396
|
+
snapshot, shows a Retry action, and never overwrites it with a failed result. The snapshot is independent of Board visibility, range, and timezone. Older
|
|
397
|
+
visibility-filtered snapshots are not reused. After one minute it refreshes in the background while cached
|
|
381
398
|
charts remain visible. Refresh explicitly rebuilds it, reusing existing per-file
|
|
382
399
|
metadata and incremental parsing caches. Unchanged files retain parsed records
|
|
383
400
|
on disk across restarts and turn or price changes. A session filename index
|
|
@@ -387,17 +404,35 @@ metadata changes; transient I/O failures retry while retaining healthy sources.
|
|
|
387
404
|
Rescan explicitly reparses available files and refreshes the inventory, while
|
|
388
405
|
retaining known history for missing sources. Board polling never starts a history scan.
|
|
389
406
|
|
|
390
|
-
The combined chart uses cost bars on the left axis and a token
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
407
|
+
The combined Tokens and cost chart uses cost bars on the left axis and a token
|
|
408
|
+
line on the right. Bars emphasize daily spending; the line shows activity changes.
|
|
409
|
+
Today's cost bars have a lighter fill and dashed borders; token segments leading
|
|
410
|
+
into today are dashed too. Finished days stay solid.
|
|
411
|
+
Tokens and cost by project shows stacked cost bars followed by a stacked token
|
|
412
|
+
area chart. Both use the period's top ten projects by estimated cost plus Other,
|
|
413
|
+
with matching colors. Select a legend entry or a Top 10 row to isolate that
|
|
414
|
+
project in both charts; All projects restores the stacks. Hovering or focusing
|
|
415
|
+
a legend entry highlights that project in both charts. Hover or focus a day in
|
|
416
|
+
the token chart to inspect exact project tokens and cost. Totals, the combined
|
|
417
|
+
chart, and rankings always describe the full selected period.
|
|
418
|
+
|
|
419
|
+
Weekly limits occupies one third of the summary row beside Recorded tokens and
|
|
420
|
+
Estimated cost. Narrow views stack these cards. Its heading shows the latest
|
|
421
|
+
recorded Codex percentage left and the next reset in the viewer's timezone.
|
|
422
|
+
The latest reading is independent of the chart's selected historical period;
|
|
423
|
+
a reading whose reset has passed is unavailable until a new observation arrives.
|
|
424
|
+
Only the main Codex bucket appears; model-specific limits are excluded. The
|
|
425
|
+
compact chart uses the last observation per local day and connects observations
|
|
426
|
+
across missing days. Known resets add an explicit refill jump. Connecting lines
|
|
427
|
+
interpolate between recorded points; they are not additional measurements.
|
|
428
|
+
Codex writes service-provided seven-day primary or secondary allowance windows
|
|
429
|
+
into local rollout token-count events, including events without token counters.
|
|
430
|
+
TaskChef reads these logs; it does not poll the service for current allowance.
|
|
431
|
+
Weekly observations include all root-chat sources, regardless of Board visibility.
|
|
432
|
+
|
|
433
|
+
Desktop, CLI, exec, archived chats, and descendant usage are included. Board
|
|
434
|
+
visibility settings do not narrow Usage or change its cached history.
|
|
435
|
+
Descendants are counted once under their root chat's
|
|
401
436
|
current project. Unassigned chats share one Unassigned project. Daily usage
|
|
402
437
|
comes from recorded call timestamps, not chat creation or last-update dates.
|
|
403
438
|
Unexplained cumulative counter gaps stay in lifetime totals but are excluded
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@ export async function assertNoStateWriters(paths, runCommand = run) {
|
|
|
28
28
|
if (!identities.has(root)) {
|
|
29
29
|
let name;
|
|
30
30
|
try { name = JSON.parse(await readFile(join(root, ".codex-plugin/plugin.json"), "utf8")).name; } catch { /* Unrelated process. */ }
|
|
31
|
-
identities.set(root, ["taskchef", "taskchef-next"].includes(name));
|
|
31
|
+
identities.set(root, ["taskchef", "taskchef-dev", "taskchef-next"].includes(name));
|
|
32
32
|
}
|
|
33
33
|
return identities.get(root);
|
|
34
34
|
}
|
|
@@ -674,10 +674,10 @@ export class CodexSessionScanner {
|
|
|
674
674
|
}
|
|
675
675
|
return { month, timeZone, chats };
|
|
676
676
|
}
|
|
677
|
-
async usageAnalytics({ range, timeZone
|
|
678
|
-
return aggregateUsage(await this.usageHistory(
|
|
677
|
+
async usageAnalytics({ range, timeZone }) {
|
|
678
|
+
return aggregateUsage(await this.usageHistory(), { range, timeZone, now: this.now() });
|
|
679
679
|
}
|
|
680
|
-
async usageHistory({
|
|
680
|
+
async usageHistory({ onProgress = () => {}, rescan = false } = {}) {
|
|
681
681
|
if (rescan) this.sessionUsage.inventoryAt = -Infinity;
|
|
682
682
|
const snapshot = await this.refresh();
|
|
683
683
|
if (!snapshot.healthy) throw new Error(snapshot.scan.error);
|
|
@@ -690,8 +690,8 @@ export class CodexSessionScanner {
|
|
|
690
690
|
})), records.projects);
|
|
691
691
|
const families = [];
|
|
692
692
|
const seen = new Set();
|
|
693
|
-
//
|
|
694
|
-
const tasks = grouped.tasks
|
|
693
|
+
// Accounting includes every root, independent of Board visibility settings.
|
|
694
|
+
const tasks = grouped.tasks;
|
|
695
695
|
let completed = 0;
|
|
696
696
|
onProgress({ completed, total: tasks.length });
|
|
697
697
|
for (const task of tasks) {
|
|
@@ -19,9 +19,10 @@ const bucket = () => ({ tokens: zero(), costUsd: 0, partial: false, costPartial:
|
|
|
19
19
|
// Cumulative totals and call counters are independent. A missing boundary can
|
|
20
20
|
// leave one turn partial without preventing later calls from being allocated.
|
|
21
21
|
export function summarizeRecords(records, latestTurnId, models, child = false) {
|
|
22
|
-
const total = bucket(), latest = bucket(), durations = new Map(), seen = new Set(), timeline = [];
|
|
22
|
+
const total = bucket(), latest = bucket(), durations = new Map(), seen = new Set(), timeline = [], weeklyLimits = [];
|
|
23
23
|
let previous = zero();
|
|
24
24
|
for (const record of [...records].sort((a, b) => a.time - b.time || a.order - b.order)) {
|
|
25
|
+
if (record.kind === "weekly-limit") { weeklyLimits.push(record); continue; }
|
|
25
26
|
const key = JSON.stringify([record.time, record.kind, record.turn, record.root, record.model, record.total, record.last, record.duration]);
|
|
26
27
|
if (seen.has(key)) continue;
|
|
27
28
|
seen.add(key);
|
|
@@ -71,7 +72,7 @@ export function summarizeRecords(records, latestTurnId, models, child = false) {
|
|
|
71
72
|
latest.partial = latest.costPartial = true;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
return { total, latest, durations, timeline };
|
|
75
|
+
return { total, latest, durations, timeline, weeklyLimits };
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
async function inventory(root, paths = []) {
|
|
@@ -138,7 +139,7 @@ export class CodexSessionUsage {
|
|
|
138
139
|
if (!saved && cachePath) {
|
|
139
140
|
try {
|
|
140
141
|
const value = JSON.parse(await readFile(cachePath, "utf8"));
|
|
141
|
-
if ([1, 2, 3].includes(value.version) && validResult(value.result) && (value.version === 1 || (Array.isArray(value.result.timeline) && value.result.timeline.every(sample => Number.isFinite(sample.time) && usage(sample.tokens) && Number.isFinite(sample.costUsd) && sample.costUsd >= 0 && sample.samples === 1 && typeof sample.partial === "boolean" && typeof sample.costPartial === "boolean")))) saved = value;
|
|
142
|
+
if ([1, 2, 3, 4].includes(value.version) && validResult(value.result) && (value.version === 1 || (Array.isArray(value.result.timeline) && value.result.timeline.every(sample => Number.isFinite(sample.time) && usage(sample.tokens) && Number.isFinite(sample.costUsd) && sample.costUsd >= 0 && sample.samples === 1 && typeof sample.partial === "boolean" && typeof sample.costPartial === "boolean")))) saved = value;
|
|
142
143
|
} catch { /* An absent or damaged cache is rebuilt from source records. */ }
|
|
143
144
|
}
|
|
144
145
|
const sourceKeys = (items) => items.flatMap(([sessionId, versions]) => versions
|
|
@@ -164,7 +165,7 @@ export class CodexSessionUsage {
|
|
|
164
165
|
})) knownSources = [];
|
|
165
166
|
} catch { knownSources = []; /* No prior valid source coverage is available. */ }
|
|
166
167
|
const missingSources = knownSources.filter(key => !currentSources.has(key));
|
|
167
|
-
if (!rescan && !saved?.retryRead && saved?.version ===
|
|
168
|
+
if (!rescan && !saved?.retryRead && saved?.version === 4 && typeof saved.result.analyticsPartial === "boolean" && saved.recordSources && saved.sourceSignature === sourceSignature) { this.results.set(resultKey, saved); return saved.result; }
|
|
168
169
|
const availablePaths = new Set(sources.flatMap(([, versions]) => versions.filter(version => version.length > 2).map(version => version[0])));
|
|
169
170
|
let retryRead = false;
|
|
170
171
|
const parsed = [];
|
|
@@ -183,7 +184,7 @@ export class CodexSessionUsage {
|
|
|
183
184
|
if (!availablePaths.has(path)) throw new Error("Rollout is missing.");
|
|
184
185
|
const previous = saved?.sourceFiles?.[sourceKey];
|
|
185
186
|
let file;
|
|
186
|
-
if (!rescan && previous && JSON.stringify(previous.stamp) === JSON.stringify(stamp) && Array.isArray(saved.recordSources?.[sourceKey])) {
|
|
187
|
+
if (!rescan && saved?.version === 4 && previous && JSON.stringify(previous.stamp) === JSON.stringify(stamp) && Array.isArray(saved.recordSources?.[sourceKey])) {
|
|
187
188
|
file = { records: saved.recordSources[sourceKey], partial: previous.partial };
|
|
188
189
|
} else {
|
|
189
190
|
if (rescan) this.files.delete(path);
|
|
@@ -208,10 +209,10 @@ export class CodexSessionUsage {
|
|
|
208
209
|
const retainedSources = saved?.recordSources && typeof saved.recordSources === "object" ? saved.recordSources : {};
|
|
209
210
|
for (const key of missingSources) {
|
|
210
211
|
const rows = retainedSources[key];
|
|
211
|
-
if (Array.isArray(rows) && rows.every(row => row && Number.isFinite(row.time) && ["usage", "duration", "issue"].includes(row.kind))) recordSources[key] = rows;
|
|
212
|
+
if (Array.isArray(rows) && rows.every(row => row && Number.isFinite(row.time) && ["usage", "duration", "issue", "weekly-limit"].includes(row.kind))) recordSources[key] = rows;
|
|
212
213
|
}
|
|
213
214
|
const result = { latest: bucket(), total: bucket(), subagents: sessions.length - 1, pricingDate: pricing.date,
|
|
214
|
-
durationMs: 0, durationTurns: 0, timeline: [], analyticsPartial: inventoryFailed, partial: inventoryFailed };
|
|
215
|
+
durationMs: 0, durationTurns: 0, timeline: [], weeklyLimits: [], analyticsPartial: inventoryFailed, partial: inventoryFailed };
|
|
215
216
|
for (const { session, records, failed, latestFailed, timePartial } of parsed) {
|
|
216
217
|
const summary = summarizeRecords(records, latestTurnId, pricing.models, session.id !== id);
|
|
217
218
|
const retained = missingSources.filter(key => JSON.parse(key)[0] === session.id).flatMap(key => recordSources[key] ?? []);
|
|
@@ -226,9 +227,11 @@ export class CodexSessionUsage {
|
|
|
226
227
|
summary.latest.costPartial ||= missing.latest.samples > 0 || missing.latest.costPartial;
|
|
227
228
|
summary.durations = combined.durations;
|
|
228
229
|
summary.timeline = combined.timeline;
|
|
230
|
+
summary.weeklyLimits = combined.weeklyLimits;
|
|
229
231
|
}
|
|
230
232
|
result.analyticsPartial ||= timePartial || summary.total.tokens.total_tokens !== summary.timeline.reduce((sum, sample) => sum + sample.tokens.total_tokens, 0);
|
|
231
233
|
result.timeline.push(...summary.timeline);
|
|
234
|
+
result.weeklyLimits.push(...summary.weeklyLimits);
|
|
232
235
|
for (const key of ["total", "latest"]) {
|
|
233
236
|
add(result[key].tokens, summary[key].tokens);
|
|
234
237
|
result[key].costUsd += summary[key].costUsd;
|
|
@@ -267,7 +270,7 @@ export class CodexSessionUsage {
|
|
|
267
270
|
}
|
|
268
271
|
if (missingSources.some(key => JSON.parse(key)[0] === id)) result.partial = true;
|
|
269
272
|
}
|
|
270
|
-
const entry = { version:
|
|
273
|
+
const entry = { version: 4, sourceSignature, result, recordSources, sourceFiles, retryRead, knownSources: [...new Set([...knownSources, ...currentSources])] };
|
|
271
274
|
// Keep successful sources even if another source failed. Retry failures on
|
|
272
275
|
// the next read without reparsing all healthy historical files.
|
|
273
276
|
this.results.set(resultKey, entry);
|
|
@@ -341,6 +344,16 @@ export class CodexSessionUsage {
|
|
|
341
344
|
if (p.type === "task_started") { file.turn = p.turn_id; file.root = p.root_turn_id ?? file.root; }
|
|
342
345
|
const time = Date.parse(row.timestamp);
|
|
343
346
|
if (!Number.isFinite(time)) { file.partial = true; return; }
|
|
347
|
+
if (p.type === "token_count" && p.rate_limits) {
|
|
348
|
+
const limits = p.rate_limits;
|
|
349
|
+
for (const window of [limits.primary, limits.secondary]) {
|
|
350
|
+
if (window?.window_minutes !== 10080 || !Number.isFinite(window.used_percent)
|
|
351
|
+
|| window.used_percent < 0 || window.used_percent > 100 || !Number.isSafeInteger(window.resets_at) || window.resets_at * 1000 <= time) continue;
|
|
352
|
+
file.records.push({ kind: "weekly-limit", time, id: typeof limits.limit_id === "string" ? limits.limit_id : "codex",
|
|
353
|
+
name: typeof limits.limit_name === "string" ? limits.limit_name : null,
|
|
354
|
+
remainingPercent: 100 - window.used_percent, resetsAt: window.resets_at * 1000 });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
344
357
|
if (p.type === "token_count" && p.info) file.records.push({ kind: "usage", time, order: file.offset,
|
|
345
358
|
turn: file.turn, root: file.root, model: file.model, total: p.info.total_token_usage, last: p.info.last_token_usage });
|
|
346
359
|
if (["task_complete", "turn_aborted"].includes(p.type)) {
|