taskchef 7.20.0 → 7.21.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/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
 
@@ -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.result === null) {
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
- && TERMINAL_STATUSES.has(task.latestTurn.result.status)
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[latestIndex - 1];
149
- const previousBoundary = existing?.boundaries?.[previousTurn.turnRef];
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
- turn.result !== null && TERMINAL_STATUSES.has(turn.result.status)
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 ? task.turns[index - 1] : null;
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 = previousTurn === null
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 = previousTurn === null || previousBoundary === null || delta?.totalTokens > 0;
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: !advanced
192
- ? "Cumulative usage did not advance beyond the preceding turn."
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 = await markCalculating(task);
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)