taskchef 7.14.2 → 7.15.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 +10 -5
- package/docs/firstmate-taskchef-comparison.md +5 -5
- package/docs/spec.md +44 -29
- package/docs/workflows.md +38 -29
- package/package.json +1 -1
- package/skills/taskchef-delegate/SKILL.md +4 -2
- package/skills/taskchef-executor/SKILL.md +18 -12
- package/src/cli.js +8 -3
- package/src/dashboard/app.js +7 -7
- package/src/dashboard/state.js +61 -9
- package/src/dashboard.js +5 -0
- package/src/delegation.js +18 -3
- package/src/mcp.js +9 -2
- package/src/workspace.js +282 -109
package/src/dashboard/state.js
CHANGED
|
@@ -44,6 +44,7 @@ export function latestTurnPresentation(task) {
|
|
|
44
44
|
const requestSummary = turn?.requestSummary
|
|
45
45
|
?? (turn ? "Request not recorded by this TaskChef version." : task.title);
|
|
46
46
|
return {
|
|
47
|
+
turnRef: turn?.turnRef ?? turn?.turnId ?? task.turnRef ?? task.turnId ?? null,
|
|
47
48
|
turnId: turn?.turnId ?? task.turnId ?? null,
|
|
48
49
|
startedAt: turn?.startedAt ?? task.updatedAt ?? task.createdAt ?? null,
|
|
49
50
|
requestSummary,
|
|
@@ -70,11 +71,25 @@ export function mergeProjectedTurns(task, preservedTurns = []) {
|
|
|
70
71
|
if (!task.latestTurn) return preservedTurns;
|
|
71
72
|
const turns = [...preservedTurns];
|
|
72
73
|
const lastIndex = turns.length - 1;
|
|
73
|
-
|
|
74
|
+
const latestIdentity = task.latestTurn.turnRef ?? task.latestTurn.turnId;
|
|
75
|
+
const preservedIdentity = lastIndex >= 0
|
|
76
|
+
? (turns[lastIndex].turnRef ?? turns[lastIndex].turnId)
|
|
77
|
+
: null;
|
|
78
|
+
const migratedFallbackIdentity = lastIndex >= 0
|
|
79
|
+
&& preservedIdentity === null
|
|
80
|
+
&& latestIdentity !== null
|
|
81
|
+
&& turns[lastIndex].turnId == null
|
|
82
|
+
&& task.latestTurn.turnId == null
|
|
83
|
+
&& JSON.stringify({ ...turns[lastIndex], turnRef: null })
|
|
84
|
+
=== JSON.stringify({ ...task.latestTurn, turnRef: null });
|
|
85
|
+
if (
|
|
86
|
+
lastIndex >= 0
|
|
87
|
+
&& (preservedIdentity === latestIdentity || migratedFallbackIdentity)
|
|
88
|
+
) {
|
|
74
89
|
turns[lastIndex] = task.latestTurn;
|
|
75
90
|
} else {
|
|
76
91
|
if (
|
|
77
|
-
task.schemaVersion
|
|
92
|
+
task.schemaVersion >= 8
|
|
78
93
|
&& lastIndex >= 0
|
|
79
94
|
&& turns[lastIndex].result === null
|
|
80
95
|
) {
|
|
@@ -144,7 +159,30 @@ export function nextDateFilterRefreshDelay(tasks, filter, now = Date.now()) {
|
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
export function taskSignature(task) {
|
|
147
|
-
return JSON.stringify([
|
|
162
|
+
return JSON.stringify([
|
|
163
|
+
task.id,
|
|
164
|
+
task.turnRef ?? task.turnId ?? null,
|
|
165
|
+
task.turnId ?? null,
|
|
166
|
+
task.status ?? "unresolved",
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function signaturesDifferOnlyByMigratedFallback(previous, next) {
|
|
171
|
+
try {
|
|
172
|
+
const before = JSON.parse(previous);
|
|
173
|
+
const after = JSON.parse(next);
|
|
174
|
+
return Array.isArray(before)
|
|
175
|
+
&& Array.isArray(after)
|
|
176
|
+
&& before.length === after.length
|
|
177
|
+
&& before[1] === null
|
|
178
|
+
&& after[1] !== null
|
|
179
|
+
&& before[2] === null
|
|
180
|
+
&& after[2] === null
|
|
181
|
+
&& before[3] !== "working"
|
|
182
|
+
&& before.every((value, index) => index === 1 || value === after[index]);
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
148
186
|
}
|
|
149
187
|
|
|
150
188
|
export function findCurrentTask(tasks, taskId) {
|
|
@@ -162,14 +200,15 @@ function notificationIdentity(task, event) {
|
|
|
162
200
|
if (event === "created") {
|
|
163
201
|
return JSON.stringify([task.id, null, event, task.createdAt ?? null]);
|
|
164
202
|
}
|
|
165
|
-
return JSON.stringify([task.id, task.turnId ?? null, event]);
|
|
203
|
+
return JSON.stringify([task.id, task.turnRef ?? task.turnId ?? null, event]);
|
|
166
204
|
}
|
|
167
205
|
|
|
168
206
|
function eventTimestamp(task, event) {
|
|
169
207
|
if (event === "created") return task.createdAt ?? task.updatedAt ?? null;
|
|
170
208
|
if (
|
|
171
209
|
task.lastResult?.status === task.status
|
|
172
|
-
&& task.lastResult?.
|
|
210
|
+
&& (task.lastResult?.turnRef ?? task.lastResult?.turnId)
|
|
211
|
+
=== (task.turnRef ?? task.turnId)
|
|
173
212
|
) {
|
|
174
213
|
return task.lastResult.updatedAt;
|
|
175
214
|
}
|
|
@@ -180,7 +219,8 @@ function eventSummary(task, event) {
|
|
|
180
219
|
if (!["completed", "needs_input", "failed"].includes(event)) return null;
|
|
181
220
|
if (
|
|
182
221
|
task.lastResult?.status === task.status
|
|
183
|
-
&& task.lastResult?.
|
|
222
|
+
&& (task.lastResult?.turnRef ?? task.lastResult?.turnId)
|
|
223
|
+
=== (task.turnRef ?? task.turnId)
|
|
184
224
|
) {
|
|
185
225
|
return task.lastResult.summary;
|
|
186
226
|
}
|
|
@@ -195,6 +235,7 @@ export function notificationSnapshot(task, event = lifecycleEvent(task)) {
|
|
|
195
235
|
title: task.title,
|
|
196
236
|
status: created ? "working" : task.status,
|
|
197
237
|
event,
|
|
238
|
+
turnRef: created ? null : task.turnRef ?? task.turnId ?? null,
|
|
198
239
|
turnId: created ? null : task.turnId ?? null,
|
|
199
240
|
timestamp: eventTimestamp(task, event),
|
|
200
241
|
summary: eventSummary(task, event),
|
|
@@ -205,11 +246,16 @@ function resultNotificationSnapshot(task) {
|
|
|
205
246
|
const result = task.lastResult;
|
|
206
247
|
if (!result) return null;
|
|
207
248
|
return Object.freeze({
|
|
208
|
-
id: notificationIdentity({
|
|
249
|
+
id: notificationIdentity({
|
|
250
|
+
...task,
|
|
251
|
+
turnRef: result.turnRef ?? result.turnId,
|
|
252
|
+
turnId: result.turnId,
|
|
253
|
+
}, result.status),
|
|
209
254
|
taskId: task.id,
|
|
210
255
|
title: task.title,
|
|
211
256
|
status: result.status,
|
|
212
257
|
event: result.status,
|
|
258
|
+
turnRef: result.turnRef ?? result.turnId ?? null,
|
|
213
259
|
turnId: result.turnId ?? null,
|
|
214
260
|
timestamp: result.updatedAt,
|
|
215
261
|
summary: result.summary,
|
|
@@ -259,12 +305,18 @@ export function reconcileNotifications(
|
|
|
259
305
|
const candidates = [];
|
|
260
306
|
if (!signatures.has(task.id)) {
|
|
261
307
|
const resultNotification = resultNotificationSnapshot(task);
|
|
262
|
-
if (task.turnId || task.status !== "working" || resultNotification) {
|
|
308
|
+
if (task.turnRef || task.turnId || task.status !== "working" || resultNotification) {
|
|
263
309
|
candidates.push(notificationSnapshot(task));
|
|
264
310
|
if (resultNotification) candidates.push(resultNotification);
|
|
265
311
|
}
|
|
266
312
|
candidates.push(notificationSnapshot(task, "created"));
|
|
267
|
-
} else if (
|
|
313
|
+
} else if (
|
|
314
|
+
signatures.get(task.id) !== nextSignatures.get(task.id)
|
|
315
|
+
&& !signaturesDifferOnlyByMigratedFallback(
|
|
316
|
+
signatures.get(task.id),
|
|
317
|
+
nextSignatures.get(task.id),
|
|
318
|
+
)
|
|
319
|
+
) {
|
|
268
320
|
candidates.push(notificationSnapshot(task));
|
|
269
321
|
const resultNotification = resultNotificationSnapshot(task);
|
|
270
322
|
if (resultNotification) candidates.push(resultNotification);
|
package/src/dashboard.js
CHANGED
|
@@ -138,10 +138,13 @@ function assertDashboardTaskBounds(tasks, maximumTasks) {
|
|
|
138
138
|
boundedText(task.instruction, 250_000, `${name} instruction`);
|
|
139
139
|
boundedText(task.summary, 2_000, `${name} summary`);
|
|
140
140
|
boundedText(task.threadId, 512, `${name} thread ID`);
|
|
141
|
+
boundedText(task.turnRef, 512, `${name} turn ref`);
|
|
141
142
|
boundedText(task.turnId, 512, `${name} turn ID`);
|
|
142
143
|
boundedText(task.lastResult?.summary, 2_000, `${name} last result summary`);
|
|
144
|
+
boundedText(task.lastResult?.turnRef, 512, `${name} last result turn ref`);
|
|
143
145
|
boundedText(task.lastResult?.turnId, 512, `${name} last result turn ID`);
|
|
144
146
|
boundedText(task.latestTurn?.requestSummary, 1_000, `${name} latest request summary`);
|
|
147
|
+
boundedText(task.latestTurn?.turnRef, 512, `${name} latest turn ref`);
|
|
145
148
|
boundedText(task.latestTurn?.turnId, 512, `${name} latest turn ID`);
|
|
146
149
|
const turns = task.turns ?? [];
|
|
147
150
|
if (turns.length > 10_000) {
|
|
@@ -149,6 +152,7 @@ function assertDashboardTaskBounds(tasks, maximumTasks) {
|
|
|
149
152
|
}
|
|
150
153
|
for (const [turnIndex, turn] of turns.entries()) {
|
|
151
154
|
boundedText(turn.requestSummary, 1_000, `${name} turn ${turnIndex + 1} request summary`);
|
|
155
|
+
boundedText(turn.turnRef, 512, `${name} turn ${turnIndex + 1} turn ref`);
|
|
152
156
|
boundedText(turn.turnId, 512, `${name} turn ${turnIndex + 1} turn ID`);
|
|
153
157
|
boundedText(turn.result?.summary, 2_000, `${name} turn ${turnIndex + 1} result summary`);
|
|
154
158
|
}
|
|
@@ -158,6 +162,7 @@ function assertDashboardTaskBounds(tasks, maximumTasks) {
|
|
|
158
162
|
}
|
|
159
163
|
for (const [resultIndex, result] of results.entries()) {
|
|
160
164
|
boundedText(result.summary, 2_000, `${name} result ${resultIndex + 1} summary`);
|
|
165
|
+
boundedText(result.turnRef, 512, `${name} result ${resultIndex + 1} turn ref`);
|
|
161
166
|
boundedText(result.turnId, 512, `${name} result ${resultIndex + 1} turn ID`);
|
|
162
167
|
}
|
|
163
168
|
boundedText(task.project.name, 1_000, `${name} project name`);
|
package/src/delegation.js
CHANGED
|
@@ -70,17 +70,19 @@ function toolIdentifier(value) {
|
|
|
70
70
|
return normalized.length > 0 ? normalized : null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function attachCreationRecovery(error, taskId, resultReporting) {
|
|
73
|
+
function attachCreationRecovery(error, taskId, turnRef, resultReporting) {
|
|
74
74
|
const creationError = error instanceof Error ? error : new Error(String(error));
|
|
75
75
|
try {
|
|
76
76
|
Object.defineProperties(creationError, {
|
|
77
77
|
taskChefTaskId: { value: taskId, enumerable: true },
|
|
78
|
+
taskChefTurnRef: { value: turnRef, enumerable: true },
|
|
78
79
|
taskChefResultReporting: { value: resultReporting, enumerable: true },
|
|
79
80
|
});
|
|
80
81
|
return creationError;
|
|
81
82
|
} catch {
|
|
82
83
|
const wrapped = new Error(`Executor creation failed for recorded TaskChef task ${taskId}.`, { cause: creationError });
|
|
83
84
|
wrapped.taskChefTaskId = taskId;
|
|
85
|
+
wrapped.taskChefTurnRef = turnRef;
|
|
84
86
|
wrapped.taskChefResultReporting = resultReporting;
|
|
85
87
|
return wrapped;
|
|
86
88
|
}
|
|
@@ -238,16 +240,29 @@ export async function createAndRecordDelegation(input) {
|
|
|
238
240
|
try {
|
|
239
241
|
createResult = parseToolResult(await createThread({ prompt: prepared.instruction, title, target }), "create_thread result");
|
|
240
242
|
} catch (error) {
|
|
243
|
+
const creationFailureTurnRef = randomUUID();
|
|
241
244
|
let resultReporting = "unavailable";
|
|
242
245
|
if (reportRecordedResult !== null) {
|
|
243
246
|
try {
|
|
244
|
-
await reportRecordedResult({
|
|
247
|
+
await reportRecordedResult({
|
|
248
|
+
taskId: prepared.id,
|
|
249
|
+
threadId: null,
|
|
250
|
+
turnRef: creationFailureTurnRef,
|
|
251
|
+
turnId: null,
|
|
252
|
+
status: "failed",
|
|
253
|
+
summary: "Executor creation failed before the executor started.",
|
|
254
|
+
});
|
|
245
255
|
resultReporting = "recorded";
|
|
246
256
|
} catch {
|
|
247
257
|
resultReporting = "failed";
|
|
248
258
|
}
|
|
249
259
|
}
|
|
250
|
-
throw attachCreationRecovery(
|
|
260
|
+
throw attachCreationRecovery(
|
|
261
|
+
error,
|
|
262
|
+
prepared.id,
|
|
263
|
+
creationFailureTurnRef,
|
|
264
|
+
resultReporting,
|
|
265
|
+
);
|
|
251
266
|
}
|
|
252
267
|
|
|
253
268
|
const returnedThreadId = toolIdentifier(createResult.threadId);
|
package/src/mcp.js
CHANGED
|
@@ -22,7 +22,7 @@ const projectSchema = z.object({
|
|
|
22
22
|
|
|
23
23
|
const taskSchema = z.object({
|
|
24
24
|
schemaVersion: z.union([
|
|
25
|
-
z.literal(4), z.literal(5), z.literal(6), z.literal(7), z.literal(8),
|
|
25
|
+
z.literal(4), z.literal(5), z.literal(6), z.literal(7), z.literal(8), z.literal(9),
|
|
26
26
|
]),
|
|
27
27
|
id: z.string(),
|
|
28
28
|
project: projectSchema,
|
|
@@ -32,10 +32,12 @@ const taskSchema = z.object({
|
|
|
32
32
|
createdAt: z.string(),
|
|
33
33
|
status: z.enum(["working", "needs_input", "completed", "failed"]),
|
|
34
34
|
summary: z.string().nullable(),
|
|
35
|
+
turnRef: z.string().nullable(),
|
|
35
36
|
turnId: z.string().nullable(),
|
|
36
37
|
updatedAt: z.string(),
|
|
37
38
|
updatedBy: z.enum(["dispatcher", "mcp"]),
|
|
38
39
|
turns: z.array(z.object({
|
|
40
|
+
turnRef: z.string().nullable(),
|
|
39
41
|
turnId: z.string().nullable(),
|
|
40
42
|
requestSummary: z.string().nullable(),
|
|
41
43
|
startedAt: z.string(),
|
|
@@ -46,6 +48,7 @@ const taskSchema = z.object({
|
|
|
46
48
|
}).nullable(),
|
|
47
49
|
})),
|
|
48
50
|
latestTurn: z.object({
|
|
51
|
+
turnRef: z.string().nullable(),
|
|
49
52
|
turnId: z.string().nullable(),
|
|
50
53
|
requestSummary: z.string().nullable(),
|
|
51
54
|
startedAt: z.string(),
|
|
@@ -58,12 +61,14 @@ const taskSchema = z.object({
|
|
|
58
61
|
results: z.array(z.object({
|
|
59
62
|
status: z.enum(["needs_input", "completed", "failed"]),
|
|
60
63
|
summary: z.string(),
|
|
64
|
+
turnRef: z.string().nullable(),
|
|
61
65
|
turnId: z.string().nullable(),
|
|
62
66
|
updatedAt: z.string(),
|
|
63
67
|
})),
|
|
64
68
|
lastResult: z.object({
|
|
65
69
|
status: z.enum(["needs_input", "completed", "failed"]),
|
|
66
70
|
summary: z.string(),
|
|
71
|
+
turnRef: z.string().nullable(),
|
|
67
72
|
turnId: z.string().nullable(),
|
|
68
73
|
updatedAt: z.string(),
|
|
69
74
|
}).nullable(),
|
|
@@ -225,10 +230,11 @@ export function createTaskChefMcpServer({
|
|
|
225
230
|
{
|
|
226
231
|
title: "Report TaskChef state",
|
|
227
232
|
description:
|
|
228
|
-
"Report this self-linked executor turn's lifecycle state. Use
|
|
233
|
+
"Report this self-linked executor turn's lifecycle state. Use one stable turnRef for working and its terminal report; pass the native Codex turn ID as both turnRef and turnId when available, otherwise pass a client-generated UUID turnRef and null turnId. Preserve known repository context and delivered links. Exact retries are idempotent; stale or mismatched turnRefs are rejected.",
|
|
229
234
|
inputSchema: {
|
|
230
235
|
taskId: z.string().min(1),
|
|
231
236
|
threadId: z.string().min(1).nullable(),
|
|
237
|
+
turnRef: z.string().min(1).max(256).nullable().optional(),
|
|
232
238
|
turnId: z.string().min(1).max(256).nullable(),
|
|
233
239
|
status: z.enum(["working", "needs_input", "completed", "failed"]),
|
|
234
240
|
summary: z.string().min(1).max(2_000).nullable().optional(),
|
|
@@ -256,6 +262,7 @@ export function createTaskChefMcpServer({
|
|
|
256
262
|
inputSchema: {
|
|
257
263
|
taskId: z.string().min(1),
|
|
258
264
|
threadId: z.string().min(1).nullable(),
|
|
265
|
+
turnRef: z.string().min(1).max(256).nullable().optional(),
|
|
259
266
|
turnId: z.string().min(1).max(256).nullable(),
|
|
260
267
|
status: z.enum(["needs_input", "completed", "failed"]),
|
|
261
268
|
summary: z.string().min(1).max(2_000),
|