taskchef 7.20.0 → 7.21.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/README.md +26 -11
- package/docs/spec.md +57 -15
- package/docs/workflows.md +11 -9
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/dashboard/actions.js +81 -0
- package/src/dashboard/app.js +220 -13
- package/src/dashboard/index.html +13 -4
- package/src/dashboard/state.js +60 -3
- package/src/dashboard/styles.css +17 -4
- package/src/dashboard.js +116 -0
- package/src/mcp.js +19 -1
- package/src/usage-tracker.js +68 -13
- package/src/workspace.js +368 -20
package/src/mcp.js
CHANGED
|
@@ -23,9 +23,23 @@ const projectSchema = z.object({
|
|
|
23
23
|
description: z.string().optional(),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
const turnProvenanceSchema = z.union([
|
|
27
|
+
z.object({ kind: z.enum(["legacy", "mcp"]) }),
|
|
28
|
+
z.object({
|
|
29
|
+
kind: z.literal("dashboard_manual"),
|
|
30
|
+
actionId: z.string(),
|
|
31
|
+
fromStatus: z.enum(["working", "needs_input"]),
|
|
32
|
+
toStatus: z.enum(["completed", "failed"]),
|
|
33
|
+
expectedTurnRef: z.string().nullable(),
|
|
34
|
+
expectedThreadId: z.string().nullable(),
|
|
35
|
+
expectedUpdatedAt: z.string(),
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
|
|
26
39
|
const taskSchema = z.object({
|
|
27
40
|
schemaVersion: z.union([
|
|
28
41
|
z.literal(4), z.literal(5), z.literal(6), z.literal(7), z.literal(8), z.literal(9),
|
|
42
|
+
z.literal(10),
|
|
29
43
|
]),
|
|
30
44
|
id: z.string(),
|
|
31
45
|
project: projectSchema,
|
|
@@ -38,7 +52,7 @@ const taskSchema = z.object({
|
|
|
38
52
|
turnRef: z.string().nullable(),
|
|
39
53
|
turnId: z.string().nullable(),
|
|
40
54
|
updatedAt: z.string(),
|
|
41
|
-
updatedBy: z.enum(["dispatcher", "mcp"]),
|
|
55
|
+
updatedBy: z.enum(["dispatcher", "mcp", "dashboard"]),
|
|
42
56
|
turns: z.array(z.object({
|
|
43
57
|
turnRef: z.string().nullable(),
|
|
44
58
|
turnId: z.string().nullable(),
|
|
@@ -49,6 +63,7 @@ const taskSchema = z.object({
|
|
|
49
63
|
summary: z.string(),
|
|
50
64
|
updatedAt: z.string(),
|
|
51
65
|
}).nullable(),
|
|
66
|
+
provenance: turnProvenanceSchema.nullable(),
|
|
52
67
|
})),
|
|
53
68
|
latestTurn: z.object({
|
|
54
69
|
turnRef: z.string().nullable(),
|
|
@@ -60,6 +75,7 @@ const taskSchema = z.object({
|
|
|
60
75
|
summary: z.string(),
|
|
61
76
|
updatedAt: z.string(),
|
|
62
77
|
}).nullable(),
|
|
78
|
+
provenance: turnProvenanceSchema.nullable(),
|
|
63
79
|
}).nullable(),
|
|
64
80
|
results: z.array(z.object({
|
|
65
81
|
status: z.enum(["needs_input", "completed", "failed"]),
|
|
@@ -67,6 +83,7 @@ const taskSchema = z.object({
|
|
|
67
83
|
turnRef: z.string().nullable(),
|
|
68
84
|
turnId: z.string().nullable(),
|
|
69
85
|
updatedAt: z.string(),
|
|
86
|
+
provenance: turnProvenanceSchema.optional(),
|
|
70
87
|
})),
|
|
71
88
|
lastResult: z.object({
|
|
72
89
|
status: z.enum(["needs_input", "completed", "failed"]),
|
|
@@ -74,6 +91,7 @@ const taskSchema = z.object({
|
|
|
74
91
|
turnRef: z.string().nullable(),
|
|
75
92
|
turnId: z.string().nullable(),
|
|
76
93
|
updatedAt: z.string(),
|
|
94
|
+
provenance: turnProvenanceSchema.optional(),
|
|
77
95
|
}).nullable(),
|
|
78
96
|
});
|
|
79
97
|
|
package/src/usage-tracker.js
CHANGED
|
@@ -11,10 +11,16 @@ import {
|
|
|
11
11
|
|
|
12
12
|
const TERMINAL_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
13
13
|
const MAX_TRACKED_TURNS = 250;
|
|
14
|
+
const ADMINISTRATIVE_USAGE_REASON = "Administrative action; no Codex usage boundary.";
|
|
15
|
+
|
|
16
|
+
function isAdministrativeTurn(turn) {
|
|
17
|
+
return turn?.provenance?.kind === "dashboard_manual";
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
function hasTerminalLatestTurn(task) {
|
|
16
21
|
return task.latestTurn !== null
|
|
17
22
|
&& task.latestTurn !== undefined
|
|
23
|
+
&& !isAdministrativeTurn(task.latestTurn)
|
|
18
24
|
&& task.latestTurn.result !== null
|
|
19
25
|
&& TERMINAL_STATUSES.has(task.latestTurn.result.status);
|
|
20
26
|
}
|
|
@@ -56,7 +62,13 @@ function calculatingTask(task, existing = null, now = new Date().toISOString())
|
|
|
56
62
|
)));
|
|
57
63
|
const generation = lifecycleGeneration(task);
|
|
58
64
|
for (const turn of recentTurns) {
|
|
59
|
-
if (turn
|
|
65
|
+
if (isAdministrativeTurn(turn)) {
|
|
66
|
+
turns[turn.turnRef] = {
|
|
67
|
+
status: "unavailable",
|
|
68
|
+
reason: ADMINISTRATIVE_USAGE_REASON,
|
|
69
|
+
updatedAt: now,
|
|
70
|
+
};
|
|
71
|
+
} else if (turn.result === null) {
|
|
60
72
|
turns[turn.turnRef] = { status: "calculating", updatedAt: now };
|
|
61
73
|
} else if (turn.result.status === "interrupted") {
|
|
62
74
|
turns[turn.turnRef] = {
|
|
@@ -80,7 +92,7 @@ function calculatingTask(task, existing = null, now = new Date().toISOString())
|
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
if (task.latestTurn?.result
|
|
83
|
-
&&
|
|
95
|
+
&& hasTerminalLatestTurn(task)
|
|
84
96
|
&& !preserveAvailable) {
|
|
85
97
|
turns[task.latestTurn.turnRef] = { status: "calculating", updatedAt: now };
|
|
86
98
|
}
|
|
@@ -103,6 +115,19 @@ function calculatingTask(task, existing = null, now = new Date().toISOString())
|
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
function administrativeTask(task, existing = null, now = new Date().toISOString()) {
|
|
119
|
+
const usage = calculatingTask(task, existing, now);
|
|
120
|
+
const hasTaskProjection = existing?.task !== null && existing?.task !== undefined;
|
|
121
|
+
return {
|
|
122
|
+
...usage,
|
|
123
|
+
status: hasTaskProjection ? "available" : "unavailable",
|
|
124
|
+
...(hasTaskProjection ? {} : { reason: ADMINISTRATIVE_USAGE_REASON }),
|
|
125
|
+
updatedAt: now,
|
|
126
|
+
retryAfter: null,
|
|
127
|
+
task: existing?.task ?? null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
106
131
|
async function updateStore(workspace, taskId, transform) {
|
|
107
132
|
const release = await acquireWorkspaceLock(workspace);
|
|
108
133
|
try {
|
|
@@ -145,8 +170,13 @@ function snapshotSupersedes(current, incoming) {
|
|
|
145
170
|
function candidateHasAdvanced(task, existing, snapshot) {
|
|
146
171
|
const latestIndex = task.turns.findIndex((turn) => turn.turnRef === task.latestTurn?.turnRef);
|
|
147
172
|
if (latestIndex <= 0) return true;
|
|
148
|
-
const previousTurn = task.turns
|
|
149
|
-
|
|
173
|
+
const previousTurn = task.turns
|
|
174
|
+
.slice(0, latestIndex)
|
|
175
|
+
.reverse()
|
|
176
|
+
.find((turn) => !isAdministrativeTurn(turn));
|
|
177
|
+
const previousBoundary = previousTurn
|
|
178
|
+
? existing.boundaries[previousTurn.turnRef] ?? null
|
|
179
|
+
: null;
|
|
150
180
|
if (!previousBoundary) return true;
|
|
151
181
|
const delta = usageDelta(snapshot, previousBoundary);
|
|
152
182
|
return delta !== null && delta.totalTokens > 0;
|
|
@@ -162,9 +192,18 @@ function reconcileRecord(task, existing, snapshot, { boundaryReliable = true } =
|
|
|
162
192
|
: []
|
|
163
193
|
)));
|
|
164
194
|
const terminalTurns = recentTurns.filter((turn) => (
|
|
165
|
-
|
|
195
|
+
!isAdministrativeTurn(turn)
|
|
196
|
+
&& turn.result !== null
|
|
197
|
+
&& TERMINAL_STATUSES.has(turn.result.status)
|
|
166
198
|
));
|
|
167
199
|
const generation = lifecycleGeneration(task);
|
|
200
|
+
for (const turn of recentTurns.filter(isAdministrativeTurn)) {
|
|
201
|
+
turns[turn.turnRef] = {
|
|
202
|
+
status: "unavailable",
|
|
203
|
+
reason: ADMINISTRATIVE_USAGE_REASON,
|
|
204
|
+
updatedAt: now,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
168
207
|
for (const turn of terminalTurns) {
|
|
169
208
|
if (!turns[turn.turnRef]) {
|
|
170
209
|
turns[turn.turnRef] = {
|
|
@@ -178,21 +217,23 @@ function reconcileRecord(task, existing, snapshot, { boundaryReliable = true } =
|
|
|
178
217
|
const latest = terminalTurns.at(-1);
|
|
179
218
|
if (latest && latest.turnRef === task.latestTurn?.turnRef && boundaryReliable) {
|
|
180
219
|
const index = task.turns.findIndex((turn) => turn.turnRef === latest.turnRef);
|
|
181
|
-
const previousTurn = index > 0
|
|
220
|
+
const previousTurn = index > 0
|
|
221
|
+
? task.turns.slice(0, index).reverse().find((turn) => !isAdministrativeTurn(turn)) ?? null
|
|
222
|
+
: null;
|
|
182
223
|
const previousBoundary = previousTurn ? boundaries[previousTurn.turnRef] ?? null : null;
|
|
183
|
-
const delta =
|
|
224
|
+
const delta = index === 0
|
|
184
225
|
? (existing?.zeroBaselineTurnRef === latest.turnRef ? usageDelta(snapshot, null) : null)
|
|
185
226
|
: (previousBoundary !== null ? usageDelta(snapshot, previousBoundary) : null);
|
|
186
|
-
const advanced =
|
|
227
|
+
const advanced = index === 0 || (delta !== null && delta.totalTokens > 0);
|
|
187
228
|
if (advanced) boundaries[latest.turnRef] = snapshot;
|
|
188
229
|
turns[latest.turnRef] = delta === null || !advanced
|
|
189
230
|
? {
|
|
190
231
|
status: "unavailable",
|
|
191
|
-
reason:
|
|
192
|
-
?
|
|
193
|
-
: (previousTurn === null
|
|
232
|
+
reason: delta === null
|
|
233
|
+
? (previousTurn === null
|
|
194
234
|
? "No live zero-token boundary was recorded for this historical turn."
|
|
195
|
-
: "The preceding turn has no reliable cumulative boundary.")
|
|
235
|
+
: "The preceding turn has no reliable cumulative boundary.")
|
|
236
|
+
: "Cumulative usage did not advance beyond the preceding turn.",
|
|
196
237
|
updatedAt: now,
|
|
197
238
|
}
|
|
198
239
|
: {
|
|
@@ -245,6 +286,9 @@ export function createUsageTracker({
|
|
|
245
286
|
const markCalculating = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
246
287
|
generationIsOlder(task, existing) ? existing : calculatingTask(task, existing)
|
|
247
288
|
));
|
|
289
|
+
const markAdministrative = async (task) => updateStore(await root(), task.id, (existing) => (
|
|
290
|
+
generationIsOlder(task, existing) ? existing : administrativeTask(task, existing)
|
|
291
|
+
));
|
|
248
292
|
|
|
249
293
|
const reconcile = async (task, {
|
|
250
294
|
finalAttempt = false,
|
|
@@ -347,7 +391,9 @@ export function createUsageTracker({
|
|
|
347
391
|
active.cancelled = true;
|
|
348
392
|
jobs.delete(task.id);
|
|
349
393
|
}
|
|
350
|
-
const usage =
|
|
394
|
+
const usage = isAdministrativeTurn(task.latestTurn)
|
|
395
|
+
? await markAdministrative(task)
|
|
396
|
+
: await markCalculating(task);
|
|
351
397
|
if (hasTerminalLatestTurn(task)) schedule(task);
|
|
352
398
|
return usage;
|
|
353
399
|
});
|
|
@@ -378,6 +424,15 @@ export function createUsageTracker({
|
|
|
378
424
|
active.cancelled = true;
|
|
379
425
|
jobs.delete(task.id);
|
|
380
426
|
}
|
|
427
|
+
if (isAdministrativeTurn(task.latestTurn)) {
|
|
428
|
+
const latestUsage = usage?.turns?.[task.latestTurn.turnRef];
|
|
429
|
+
if (usage?.generationTurnRef === task.latestTurn.turnRef
|
|
430
|
+
&& latestUsage?.status === "unavailable"
|
|
431
|
+
&& latestUsage.reason === ADMINISTRATIVE_USAGE_REASON) return usage;
|
|
432
|
+
const administrative = administrativeTask(task, usage);
|
|
433
|
+
void markAdministrative(task).catch(() => {});
|
|
434
|
+
return administrative;
|
|
435
|
+
}
|
|
381
436
|
if (!usage || usage.threadId !== task.threadId) {
|
|
382
437
|
const calculating = calculatingTask(task);
|
|
383
438
|
void markCalculating(task)
|