taskchef 9.4.0 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/docs/mcp-app.md +45 -5
- package/docs/taskchef-next-storage.md +55 -0
- package/package.json +3 -2
- package/scripts/migrate-taskchef-state.js +113 -0
- package/skills/delegate/SKILL.md +1 -1
- package/src/calendar-history.js +30 -0
- package/src/codex-session-scanner.js +24 -1
- package/src/mcp-app/dist/index.html +139 -132
- package/src/mcp-app/react/CalendarView.test.tsx +59 -0
- package/src/mcp-app/react/CalendarView.tsx +91 -0
- package/src/mcp-app/react/UsageView.test.tsx +27 -6
- package/src/mcp-app/react/UsageView.tsx +2 -1
- package/src/mcp-app/react/main.test.tsx +121 -0
- package/src/mcp-app/react/main.tsx +21 -14
- package/src/mcp-app/react/preferences.ts +34 -0
- package/src/mcp-app/react/styles.css +32 -4
- package/src/mcp-app.js +11 -1
- package/src/taskchef-state.js +47 -0
package/docs/mcp-app.md
CHANGED
|
@@ -6,7 +6,7 @@ The native dashboard requires Node.js 22.18+, 23.2+, or 24+ for local Codex SQLi
|
|
|
6
6
|
|
|
7
7
|
Call `open_taskchef_board` in Codex to open **TaskChef**. In a sidebar or fullscreen host view, it shows the read-only board, filters, and chat details. In an inline chat view, it shows a short recent-chat list with direct **Open chat** actions. The app uses local Codex metadata and does not require a dispatcher workspace.
|
|
8
8
|
|
|
9
|
-
The server reads all eligible top-level chats from `~/.codex/state_5.sqlite` and their latest turn from `~/.codex/thread_history_1.sqlite`. Both SQLite connections are read-only. It excludes typed subagents and Guardian reviews, JSON subagent sources, known `thread_spawn_edges` children, chats with no turn record. Standalone `source=exec` and regular `source=cli` chats remain eligible in the collector, but the view hides them by default. The plugin details page has native switches for Show exec sessions, Show CLI sessions, and Show archived chats. The MCP server stores these choices in `~/.agents/taskchef
|
|
9
|
+
The server reads all eligible top-level chats from `~/.codex/state_5.sqlite` and their latest turn from `~/.codex/thread_history_1.sqlite`. Both SQLite connections are read-only. It excludes typed subagents and Guardian reviews, JSON subagent sources, known `thread_spawn_edges` children, chats with no turn record. Standalone `source=exec` and regular `source=cli` chats remain eligible in the collector, but the view hides them by default. The plugin details page has native switches for Show exec sessions, Show CLI sessions, and Show archived chats. The MCP server stores these choices in `~/.agents/taskchef/settings.json`, shared by all TaskChef views. Each visible poll reads the current settings, even when the database snapshot is unchanged. These choices apply to the board, counts, project choices, Details eligibility, and the compact inline view. Archived chats are hidden by default and use their own Archived column when shown. Details shows the recorded lifetime direct child count. Names and titles can contain user text; the app prefers name, then title, then a chat ID fallback.
|
|
10
10
|
|
|
11
11
|
The board uses these queue labels:
|
|
12
12
|
|
|
@@ -21,7 +21,7 @@ The board uses these queue labels:
|
|
|
21
21
|
|
|
22
22
|
Archived takes precedence. Manual Done marks apply only without active schedules; every attached PR must be confirmed merged. A normally completed scheduled prompt returns to Scheduled regardless of PR status. After a completed human turn, attached PRs determine Done or Waiting for ordinary chats, and Scheduled or Waiting for actively scheduled chats. A manual acknowledgement returns an idle scheduled human turn to Scheduled. Active schedules remain visible as a badge even when the chat is Running or Waiting. After ordinary input, a completed scheduled chat goes to Waiting. The app reads active heartbeat links from `automations/*/automation.toml` and the latest first input from `thread_items`. It checks the observed `<heartbeat><automation_id>` wrapper; this internal format can change. Missing or unrecognized input markers use Waiting rather than guessing Scheduled. Schedule read errors appear as a warning. Paused schedules do not give an active schedule badge. Cron run history is not treated as an active heartbeat chat.
|
|
23
23
|
|
|
24
|
-
**Mark Done** changes only `~/.agents/taskchef
|
|
24
|
+
**Mark Done** changes only `~/.agents/taskchef/done.json`, using a lock and atomic write. The mark is bound to the current turn ID, so a new turn resets it. There is no Reopen button; send a new prompt in Codex to start another turn. Archived, in-progress, and actively scheduled chats cannot be marked Done from this app. PR-linked chats require confirmed merge status before a Done drop is allowed. Codex databases, rollout files, and dispatcher task reports are never modified. An unreadable or invalid Done state file produces a separate local-state error and blocks Done changes; the app preserves the file and does not call it a Codex database failure. The inferred input source and display-formatted request are returned to the app; original recorded input is not modified. See [the research note](taskchef-next-status-research.md) for the optional Luna judgment route.
|
|
25
25
|
|
|
26
26
|
Both databases and the expected tables must be available. Missing or incompatible databases, or failed queries, produce a fatal app error and clear the visible inventory. Rollout files never replace database inventory or status. The selected-chat detail may optionally read a bounded head and tail of the JSONL file at its database `rollout_path` for message counts and byte coverage. The path stays server-side. This detail never changes the database-derived status. If the path or file is absent or cannot be parsed, database metadata remains available. The board returns bounded latest-turn request and response excerpts for cards. Details returns the full recorded request and response for that turn, not the whole conversation. All reads are local and read-only.
|
|
27
27
|
|
|
@@ -299,7 +299,7 @@ UI surfaces. Native font families use locally available fonts with system fallba
|
|
|
299
299
|
read the installed desktop application or fetch theme files from the network.
|
|
300
300
|
|
|
301
301
|
The compact **Pinned** toggle button shows chats pinned in Codex and combines with project,
|
|
302
|
-
update window, and search filters. It
|
|
302
|
+
update window, and search filters. It defaults to off and remembers the user’s selection locally. Pin
|
|
303
303
|
changes appear on the next data refresh. The current desktop pin list takes
|
|
304
304
|
precedence over the database `is_pinned` field. TaskChef ignores old migration
|
|
305
305
|
snapshots and never writes pin state. Without either source, chats are unpinned.
|
|
@@ -371,8 +371,9 @@ a marketplace does not automatically move their installed plugin identity.
|
|
|
371
371
|
|
|
372
372
|
The Board/Usage selector sits beside the logo in the navbar, separated from
|
|
373
373
|
Board filters by a divider. It opens an on-demand historical usage view. Ranges are
|
|
374
|
-
7, 30, or 90 calendar days including today, This month (
|
|
375
|
-
|
|
374
|
+
7, 30, or 90 calendar days including today, This month (through today), and
|
|
375
|
+
Last month (the full previous calendar month). The default is 30 days; the
|
|
376
|
+
selected range is remembered locally when Usage is reopened.
|
|
376
377
|
There is no custom range. Dates use the view's local timezone, including DST.
|
|
377
378
|
Range changes and reopening Usage query one shared history snapshot without
|
|
378
379
|
rescanning logs. The snapshot is scoped by CLI/exec visibility, independently of
|
|
@@ -408,3 +409,42 @@ Cost is an estimate at current API-equivalent rates, not subscription billing
|
|
|
408
409
|
or remaining allowance. The first read shows a progress bar with the actual number of root chats
|
|
409
410
|
processed and polls an app-only tool. Cached charts stay visible during later
|
|
410
411
|
refreshes, with their last-update time and scan progress.
|
|
412
|
+
|
|
413
|
+
## Month calendar
|
|
414
|
+
|
|
415
|
+
Calendar sits beside Board and Usage. It opens on the current local month;
|
|
416
|
+
previous/next arrows surround the month title. Next is disabled at the current
|
|
417
|
+
local month, so future months cannot be selected. There is no Today control.
|
|
418
|
+
Arrows stay hidden until the month navigation is hovered or contains focus;
|
|
419
|
+
a clicked arrow remains visible while focused. This also applies in embedded
|
|
420
|
+
hosts that do not report mouse hover support. Project, search, update window, and Pinned
|
|
421
|
+
filters are remembered locally. An unavailable saved project resets to All
|
|
422
|
+
projects only after the project inventory loads.
|
|
423
|
+
Project, Search Chats, Pinned, and native visibility settings apply. The Board's
|
|
424
|
+
Updated filter does not apply to Calendar. A Calendar-only Scheduled
|
|
425
|
+
toggle starts enabled and hides chats with an active schedule when disabled,
|
|
426
|
+
including those currently Running or Waiting. Its choice is remembered locally across page visits and does not filter
|
|
427
|
+
Board or Usage. Selecting one project hides
|
|
428
|
+
project prefixes on calendar bars. The grid has no persistent helper captions. Scheduled and Pinned share
|
|
429
|
+
the same toggle styling for their on, off, and hover states.
|
|
430
|
+
|
|
431
|
+
Each chat's recorded turn intervals become calendar dates in the viewer's
|
|
432
|
+
timezone. Adjacent dates merge into one bar. One full idle date breaks the bar,
|
|
433
|
+
including weekends; a later segment has a resumed marker and opens the same
|
|
434
|
+
chat Details. Continuous work across a week boundary uses continuation arrows.
|
|
435
|
+
An exact midnight end does not occupy the following date. A current running
|
|
436
|
+
turn extends to now; completed turns do not extend through idle review waits.
|
|
437
|
+
Project colors stay consistent when navigating or filtering. Crowded weeks show
|
|
438
|
+
three bar rows and a control to expand the remaining chats. Narrow views scroll
|
|
439
|
+
horizontally to retain seven readable day columns.
|
|
440
|
+
|
|
441
|
+
The calendar reads all turn timing rows for each eligible chat's selected
|
|
442
|
+
SQLite history, cached until the databases change. The calendar rereads on
|
|
443
|
+
changed snapshots or database reads, explicit Refresh, and local date rollover;
|
|
444
|
+
an unchanged five-second poll does not reread calendar history. It does not scan usage,
|
|
445
|
+
modify Codex records, or create editable start/end dates. Earlier histories
|
|
446
|
+
replaced by a rewind may be absent. Missing start timestamps are omitted;
|
|
447
|
+
missing durations show only a known start date. Missing timing data does not
|
|
448
|
+
add a notice to the calendar.
|
|
449
|
+
|
|
450
|
+

|
|
@@ -10,6 +10,61 @@ The storage diagram groups tables inside their database files. The separate exam
|
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
13
|
+
## Native state directory and migration
|
|
14
|
+
|
|
15
|
+
TaskChef and TaskChef Dev share `~/.agents/taskchef/` for settings, Done marks,
|
|
16
|
+
GitHub state, notifications, delegation memory and caches. The Dev plugin keeps
|
|
17
|
+
its separate `taskchef-next` identity. Storage naming does not change plugin
|
|
18
|
+
registration or merge the released and Dev installations.
|
|
19
|
+
|
|
20
|
+
Before migration, upgraded packages can still use an existing native
|
|
21
|
+
`~/.agents/taskchef-next/` directory. They never treat retired dispatcher files
|
|
22
|
+
at `~/.agents/taskchef/` as native state. Two independent native directories,
|
|
23
|
+
unexpected links, or an unfinished migration lock stop startup with an error.
|
|
24
|
+
The runtime does not move user data automatically.
|
|
25
|
+
|
|
26
|
+
To migrate on macOS or Linux with `lsof` available:
|
|
27
|
+
|
|
28
|
+
1. Install the compatible released and Dev packages first. Retain the existing
|
|
29
|
+
published release until the normal release workflow produces the new one.
|
|
30
|
+
2. Resolve any saved Codex project whose root is `~/.agents/taskchef` through
|
|
31
|
+
Codex's project controls. Point the retired project at its archive destination
|
|
32
|
+
or remove the obsolete root while preserving its chats. Do not edit Codex
|
|
33
|
+
databases. Otherwise that project would point at native state after the move.
|
|
34
|
+
3. Quit Codex and all other TaskChef clients. Stop any old dashboard using the
|
|
35
|
+
retired directory through its normal controls. From an external terminal,
|
|
36
|
+
run the following inside the updated TaskChef package:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
node scripts/migrate-taskchef-state.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
4. Restart Codex and verify settings, Done marks, GitHub connection,
|
|
43
|
+
notifications and usage. Confirm both TaskChef plugins remain enabled.
|
|
44
|
+
|
|
45
|
+
The command first refuses active TaskChef MCP processes and any process with
|
|
46
|
+
its working directory in the stores. If process inspection fails, it stops.
|
|
47
|
+
It moves the retired dispatcher directory intact to `~/.agents/taskchef-old`.
|
|
48
|
+
An existing archive is never overwritten or merged with a retired dispatcher.
|
|
49
|
+
The obsolete `~/.codex/taskchef` registry moves separately to
|
|
50
|
+
`~/.agents/taskchef-old/codex-registry`; registry collisions or unknown top-level
|
|
51
|
+
entries stop the move. Active Codex databases, sessions and chat history are
|
|
52
|
+
outside this command's scope.
|
|
53
|
+
|
|
54
|
+
The full native directory then moves to `~/.agents/taskchef`, retaining file
|
|
55
|
+
contents and permissions, including historical usage counters. A relative
|
|
56
|
+
`taskchef-next -> taskchef` compatibility link lets older installed packages
|
|
57
|
+
reach the same files. It is not a second state store. Keep the link while any
|
|
58
|
+
older package or skill still refers to it. Current packages resolve the canonical
|
|
59
|
+
path after migration; current delegation instructions use that path too.
|
|
60
|
+
|
|
61
|
+
Repeating the command accepts a completed move or resumes after an individual
|
|
62
|
+
rename. It never merges two native directories or deletes history. An interrupted
|
|
63
|
+
process can leave `~/.agents/.taskchef-storage-migration`: inspect the directory
|
|
64
|
+
layout, confirm no migration or writer is running, then remove only that empty
|
|
65
|
+
lock directory with `rmdir` before retrying. Compare inventories before and after
|
|
66
|
+
migration; keep the archive for historical reference.
|
|
67
|
+
|
|
13
68
|
## One chat, two possible IDs
|
|
14
69
|
|
|
15
70
|
Suppose chat **C** has a turn **T**:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskchef",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.6.0",
|
|
4
4
|
"description": "A native Codex dashboard for chats, usage, pull requests, and schedules.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Favo Yang",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"docs/diagrams/taskchef-next-storage.drawio",
|
|
32
32
|
"docs/diagrams/taskchef-next-storage.drawio.png",
|
|
33
33
|
"docs/diagrams/taskchef-next-reply-lookup.drawio",
|
|
34
|
-
"docs/diagrams/taskchef-next-reply-lookup.drawio.png"
|
|
34
|
+
"docs/diagrams/taskchef-next-reply-lookup.drawio.png",
|
|
35
|
+
"scripts/migrate-taskchef-state.js"
|
|
35
36
|
],
|
|
36
37
|
"engines": {
|
|
37
38
|
"node": "^22.18.0 || >=23.2.0"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { lstat, mkdir, readdir, readFile, rename, rmdir, symlink, realpath } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, relative, isAbsolute, sep, resolve } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { hasDispatcherState, isNativeStateDir, taskChefStatePaths, taskChefStateDir } from "../src/taskchef-state.js";
|
|
9
|
+
|
|
10
|
+
const run = promisify(execFile);
|
|
11
|
+
const info = path => lstat(path).catch(error => { if (error.code === "ENOENT") return null; throw error; });
|
|
12
|
+
const inside = (root, path) => { const child = relative(root, path); return child === "" || (!isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`)); };
|
|
13
|
+
|
|
14
|
+
// lsof supplies cwd even when the MCP command uses ./mcp/server.js. Inspect
|
|
15
|
+
// command arguments too, so a test or build in a TaskChef checkout is harmless.
|
|
16
|
+
export async function assertNoStateWriters(paths, runCommand = run) {
|
|
17
|
+
let output, commands;
|
|
18
|
+
try {
|
|
19
|
+
({ stdout: output } = await runCommand("lsof", ["-n", "-a", "-u", String(process.getuid()), "-d", "cwd", "-Fpn"], { maxBuffer: 8 * 1024 * 1024 }));
|
|
20
|
+
const { stdout } = await runCommand("ps", ["-axo", "pid=,command="], { maxBuffer: 8 * 1024 * 1024 });
|
|
21
|
+
commands = new Map(stdout.split("\n").flatMap(line => {
|
|
22
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
23
|
+
return match ? [[Number(match[1]), match[2]]] : [];
|
|
24
|
+
}));
|
|
25
|
+
} catch (error) { throw new Error(`Cannot establish that TaskChef writers are stopped (lsof/ps): ${error.message}`); }
|
|
26
|
+
const identities = new Map();
|
|
27
|
+
async function isTaskChef(root) {
|
|
28
|
+
if (!identities.has(root)) {
|
|
29
|
+
let name;
|
|
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));
|
|
32
|
+
}
|
|
33
|
+
return identities.get(root);
|
|
34
|
+
}
|
|
35
|
+
let pid;
|
|
36
|
+
const blocked = [];
|
|
37
|
+
for (const line of output.split("\n")) {
|
|
38
|
+
if (line.startsWith("p")) pid = Number(line.slice(1));
|
|
39
|
+
if (!line.startsWith("n") || !pid || pid === process.pid) continue;
|
|
40
|
+
const cwd = line.slice(1);
|
|
41
|
+
if ([paths.current, paths.previous, paths.archive, paths.registry].some(root => inside(root, cwd))) { blocked.push(pid); continue; }
|
|
42
|
+
const command = commands.get(pid) ?? "";
|
|
43
|
+
if (/(?:^|[\s"'])(?:\.\/)?mcp\/server\.js(?:[\s"']|$)/.test(command) && await isTaskChef(cwd)) { blocked.push(pid); continue; }
|
|
44
|
+
// ps does not escape spaces consistently. Try each absolute suffix ending
|
|
45
|
+
// at the server filename and confirm the package's manifest on disk.
|
|
46
|
+
for (const match of command.matchAll(/\/mcp\/server\.js(?=[\s"']|$)/g)) {
|
|
47
|
+
const end = match.index + match[0].length;
|
|
48
|
+
for (let start = command.indexOf("/"); start >= 0 && start <= match.index; start = command.indexOf("/", start + 1)) {
|
|
49
|
+
if (await isTaskChef(dirname(dirname(command.slice(start, end))))) { blocked.push(pid); break; }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (blocked.length) throw new Error(`Stop TaskChef clients and processes using its storage before migrating (PIDs: ${[...new Set(blocked)].join(", ")}). Quit Codex; stop any listed legacy dashboard through its normal controls. Nothing was moved.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function assertNative(directory) {
|
|
57
|
+
if (!isNativeStateDir(directory)) {
|
|
58
|
+
throw new Error(`Unrecognized native state at ${directory}; inspect it before migrating.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function migrateTaskChefState(home = homedir(), { assertStopped = assertNoStateWriters } = {}) {
|
|
63
|
+
const paths = taskChefStatePaths(home);
|
|
64
|
+
await mkdir(paths.agents, { recursive: true, mode: 0o700 });
|
|
65
|
+
// A leftover lock requires inspection, never automatic deletion or a blind retry.
|
|
66
|
+
await mkdir(paths.lock, { mode: 0o700 });
|
|
67
|
+
try {
|
|
68
|
+
await assertStopped(paths);
|
|
69
|
+
const current = await info(paths.current), previous = await info(paths.previous), archive = await info(paths.archive);
|
|
70
|
+
if (current && (!current.isDirectory() || current.isSymbolicLink())) throw new Error(`Refusing unexpected destination ${paths.current}.`);
|
|
71
|
+
if (archive && (!archive.isDirectory() || archive.isSymbolicLink())) throw new Error(`Refusing unexpected archive ${paths.archive}.`);
|
|
72
|
+
const retired = current && hasDispatcherState(paths.current);
|
|
73
|
+
if (retired && archive) throw new Error(`Archive ${paths.archive} already exists; nothing will be overwritten or merged.`);
|
|
74
|
+
if (previous?.isSymbolicLink()) {
|
|
75
|
+
if (!current || retired || await realpath(paths.previous) !== await realpath(paths.current)) throw new Error("Unexpected taskchef-next link; inspect it before migrating.");
|
|
76
|
+
} else if (previous) {
|
|
77
|
+
if (!previous.isDirectory()) throw new Error("taskchef-next must be a directory.");
|
|
78
|
+
await assertNative(paths.previous);
|
|
79
|
+
if (current && !retired) throw new Error("Both native state directories exist. Inspect and reconcile them before migrating; no files were merged.");
|
|
80
|
+
} else if (!current || retired) throw new Error("No current native state was found; nothing was moved.");
|
|
81
|
+
if (current && !retired) await assertNative(paths.current);
|
|
82
|
+
|
|
83
|
+
// The obsolete registry is independent of Codex's active SQLite databases.
|
|
84
|
+
const registry = paths.registry, registryInfo = await info(registry);
|
|
85
|
+
const registryArchive = join(paths.archive, "codex-registry");
|
|
86
|
+
if (registryInfo) {
|
|
87
|
+
const futureArchive = join(retired ? paths.current : paths.archive, "codex-registry");
|
|
88
|
+
if (!registryInfo.isDirectory() || registryInfo.isSymbolicLink() || await info(futureArchive)) throw new Error("Unexpected or colliding legacy Codex registry; nothing was moved.");
|
|
89
|
+
const names = await readdir(registry);
|
|
90
|
+
if (names.some(name => !["workspaces.json", "projects"].includes(name))) throw new Error("Legacy Codex registry contains unknown entries; inspect it first.");
|
|
91
|
+
}
|
|
92
|
+
// Complete all collision checks before the first rename. Each rename is
|
|
93
|
+
// recoverable: a repeated run recognizes the resulting directory layout.
|
|
94
|
+
if (retired) await rename(paths.current, paths.archive);
|
|
95
|
+
if (registryInfo) {
|
|
96
|
+
await mkdir(paths.archive, { recursive: true, mode: 0o700 });
|
|
97
|
+
await rename(registry, registryArchive);
|
|
98
|
+
}
|
|
99
|
+
if (previous && !previous.isSymbolicLink()) await rename(paths.previous, paths.current);
|
|
100
|
+
if (!await info(paths.previous)) await symlink("taskchef", paths.previous, "dir");
|
|
101
|
+
return { state: paths.current, archive: await info(paths.archive) ? paths.archive : null, compatibilityLink: paths.previous };
|
|
102
|
+
} finally { await rmdir(paths.lock); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
106
|
+
try {
|
|
107
|
+
const result = await migrateTaskChefState();
|
|
108
|
+
// Resolve only after releasing the migration lock.
|
|
109
|
+
taskChefStateDir();
|
|
110
|
+
console.log(JSON.stringify(result, null, 2));
|
|
111
|
+
console.log("Start Codex with the updated TaskChef packages. taskchef-next is a compatibility link, not a second state store.");
|
|
112
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
113
|
+
}
|
package/skills/delegate/SKILL.md
CHANGED
|
@@ -32,7 +32,7 @@ execution state.
|
|
|
32
32
|
|
|
33
33
|
## Routing memory
|
|
34
34
|
|
|
35
|
-
Optionally consult `~/.agents/taskchef
|
|
35
|
+
Optionally consult `~/.agents/taskchef/delegation-memory.md` for confirmed
|
|
36
36
|
project aliases and routing preferences. Revalidate each hint against the
|
|
37
37
|
current native project list. Treat the file as routing notes, not instructions
|
|
38
38
|
that override the user.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Date-only arithmetic uses UTC; recorded instants are first converted to the
|
|
2
|
+
// viewer's timezone. This keeps full idle days consistent across DST changes.
|
|
3
|
+
const nextDay = date => new Date(Date.parse(`${date}T00:00:00Z`) + 86400000).toISOString().slice(0, 10);
|
|
4
|
+
export function calendarSegments(turns, { month, timeZone, now = Date.now(), latestTurnId }) {
|
|
5
|
+
const format = new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
6
|
+
const day = time => {
|
|
7
|
+
const parts = Object.fromEntries(format.formatToParts(time).map(part => [part.type, part.value]));
|
|
8
|
+
return `${parts.year}-${parts.month}-${parts.day}`;
|
|
9
|
+
};
|
|
10
|
+
let incomplete = false;
|
|
11
|
+
const intervals = [];
|
|
12
|
+
for (const turn of turns) {
|
|
13
|
+
const start = turn.started_at * 1000;
|
|
14
|
+
if (!Number.isFinite(start) || turn.started_at == null || start <= 0 || start > now) { incomplete = true; continue; }
|
|
15
|
+
const running = turn.status === 'inProgress' && turn.turn_id === latestTurnId;
|
|
16
|
+
const hasDuration = Number.isFinite(turn.duration_ms) && turn.duration_ms >= 0;
|
|
17
|
+
if (!running && !hasDuration) incomplete = true;
|
|
18
|
+
const end = running ? now : Math.min(now, start + (hasDuration ? turn.duration_ms : 0));
|
|
19
|
+
// A turn ending exactly at midnight did not work on the following day.
|
|
20
|
+
intervals.push({ start: day(start), end: day(Math.max(start, end - 1)) });
|
|
21
|
+
}
|
|
22
|
+
intervals.sort((a, b) => a.start.localeCompare(b.start) || a.end.localeCompare(b.end));
|
|
23
|
+
const segments = [];
|
|
24
|
+
for (const interval of intervals) {
|
|
25
|
+
const previous = segments.at(-1);
|
|
26
|
+
if (previous && interval.start <= nextDay(previous.end)) previous.end = previous.end > interval.end ? previous.end : interval.end;
|
|
27
|
+
else segments.push({ ...interval, resumed: segments.length > 0 });
|
|
28
|
+
}
|
|
29
|
+
return { incomplete, segments: segments.filter(segment => segment.start.slice(0, 7) <= month && segment.end.slice(0, 7) >= month) };
|
|
30
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { taskChefStateDir } from "./taskchef-state.js";
|
|
2
|
+
import { calendarSegments } from "./calendar-history.js";
|
|
1
3
|
import { aggregateUsage } from "./usage-analytics.js";
|
|
2
4
|
import { openRollout, rolloutChunks } from "./codex-rollout.js";
|
|
3
5
|
import { createHash } from "node:crypto";
|
|
@@ -479,7 +481,7 @@ async function readSession(file, now) {
|
|
|
479
481
|
}
|
|
480
482
|
|
|
481
483
|
export class CodexSessionScanner {
|
|
482
|
-
constructor({ codexHome = process.env.CODEX_HOME || join(homedir(), ".codex"), now = () => Date.now(), statePath = join(
|
|
484
|
+
constructor({ codexHome = process.env.CODEX_HOME || join(homedir(), ".codex"), now = () => Date.now(), statePath = join(taskChefStateDir(), "done.json"), nodeVersion = process.versions.node } = {}) {
|
|
483
485
|
this.codexHome = codexHome;
|
|
484
486
|
this.now = now;
|
|
485
487
|
this.projectCatalog = new CodexProjects(codexHome);
|
|
@@ -651,6 +653,27 @@ export class CodexSessionScanner {
|
|
|
651
653
|
) SELECT id, rollout_path FROM threads WHERE id IN (SELECT id FROM family)`).all(id);
|
|
652
654
|
return this.sessionUsage.read({ id, latestTurnId: task.turnId, sessions, savedDurations });
|
|
653
655
|
}
|
|
656
|
+
async calendarHistory({ month, timeZone, settings }) {
|
|
657
|
+
const snapshot = await this.refresh();
|
|
658
|
+
if (!snapshot.healthy) throw new Error(snapshot.scan.error);
|
|
659
|
+
const records = this.databaseCache.records;
|
|
660
|
+
const cache = records.calendarTurns ??= new Map();
|
|
661
|
+
const rows = new Map(records.rows.map(row => [row.id, row]));
|
|
662
|
+
const query = this.connections.history.prepare(`SELECT turn_id, started_at, duration_ms, status
|
|
663
|
+
FROM thread_turns WHERE thread_id = ? ORDER BY rollout_ordinal`);
|
|
664
|
+
const chats = [];
|
|
665
|
+
for (const task of snapshot.tasks) {
|
|
666
|
+
if ((!settings.showArchived && task.observed?.archive) ||
|
|
667
|
+
(!settings.showCli && task.observed?.source === "cli") ||
|
|
668
|
+
(!settings.showExec && task.observed?.source === "exec")) continue;
|
|
669
|
+
const row = rows.get(task.id);
|
|
670
|
+
let turns = cache.get(task.id);
|
|
671
|
+
if (!turns) { turns = query.all(historyId(row)); cache.set(task.id, turns); }
|
|
672
|
+
const result = calendarSegments(turns, { month, timeZone, now: this.now(), latestTurnId: task.turnId });
|
|
673
|
+
if (result.segments.length || result.incomplete) chats.push({ id: task.id, ...result });
|
|
674
|
+
}
|
|
675
|
+
return { month, timeZone, chats };
|
|
676
|
+
}
|
|
654
677
|
async usageAnalytics({ range, timeZone, settings }) {
|
|
655
678
|
return aggregateUsage(await this.usageHistory({ settings }), { range, timeZone, now: this.now() });
|
|
656
679
|
}
|