taskchef 7.14.2 → 7.15.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.
@@ -61,13 +61,19 @@ const elements = {
61
61
  };
62
62
  let notificationDescriptionSerial = 0;
63
63
 
64
- function referenceLink(link, { compact = false } = {}) {
64
+ function referenceLink(link, { compact = false, typePrefix = false } = {}) {
65
65
  const anchor = document.createElement("a");
66
66
  anchor.className = compact ? "github-link github-link-compact" : "github-link";
67
67
  anchor.href = link.url;
68
68
  anchor.target = "_blank";
69
69
  anchor.rel = "noopener noreferrer";
70
- anchor.textContent = link.label ?? link.text;
70
+ const label = link.label ?? link.text;
71
+ const visibleLabel = typePrefix && link.type === "pull"
72
+ ? `PR ${label}`
73
+ : typePrefix && link.type === "issue"
74
+ ? `Issue ${label}`
75
+ : label;
76
+ anchor.textContent = visibleLabel;
71
77
  const githubKind = link.type === "issue"
72
78
  ? ", GitHub issue"
73
79
  : link.type === "pull"
@@ -75,7 +81,7 @@ function referenceLink(link, { compact = false } = {}) {
75
81
  : link.provider === "github" || link.owner ? " on GitHub" : "";
76
82
  anchor.setAttribute(
77
83
  "aria-label",
78
- `${link.label ?? link.text}${githubKind} (opens in a new tab)`,
84
+ `${visibleLabel}${githubKind} (opens in a new tab)`,
79
85
  );
80
86
  anchor.addEventListener("click", (event) => event.stopPropagation());
81
87
  return anchor;
@@ -87,7 +93,7 @@ function appendLinkedText(container, text, task) {
87
93
  taskRepository: task.relatedGitHubRepository,
88
94
  }).map((segment) => {
89
95
  if (segment.kind === "text") return document.createTextNode(segment.text);
90
- if (segment.kind === "link") return referenceLink(segment);
96
+ if (segment.kind === "link") return referenceLink(segment, { typePrefix: true });
91
97
  const ambiguous = document.createElement("span");
92
98
  ambiguous.className = "github-reference-ambiguous";
93
99
  ambiguous.textContent = segment.text;
@@ -100,7 +106,12 @@ function appendLinkedText(container, text, task) {
100
106
  }
101
107
 
102
108
  function relatedGitHubLinks(task, { compact = false } = {}) {
103
- const links = task.relatedGitHubLinks ?? [];
109
+ const seen = new Set();
110
+ const links = (task.relatedGitHubLinks ?? []).filter((link) => {
111
+ if (link.type === "repository" || seen.has(link.label)) return false;
112
+ seen.add(link.label);
113
+ return true;
114
+ });
104
115
  const container = document.createElement("nav");
105
116
  container.className = `github-links${compact ? " github-links-compact" : ""}`;
106
117
  container.setAttribute("aria-label", `Related GitHub links for ${task.title}`);
@@ -118,6 +129,26 @@ function relatedGitHubLinks(task, { compact = false } = {}) {
118
129
  return container;
119
130
  }
120
131
 
132
+ function renderProject(container, task) {
133
+ const children = [document.createTextNode(task.project.name)];
134
+ const repository = task.relatedGitHubRepository;
135
+ if (repository) {
136
+ const [owner, repositoryName] = repository.split("/");
137
+ children.push(
138
+ document.createTextNode(" "),
139
+ referenceLink({
140
+ label: repository,
141
+ owner,
142
+ provider: "github",
143
+ repository: repositoryName,
144
+ type: "repository",
145
+ url: `https://github.com/${repository}`,
146
+ }),
147
+ );
148
+ }
149
+ container.replaceChildren(...children);
150
+ }
151
+
121
152
  function timestampControl(value, { accessibleName, key, prefix = "" }) {
122
153
  if (!parsedTimestamp(value)) {
123
154
  const missing = document.createElement("span");
@@ -259,7 +290,7 @@ function notificationAnnouncement(notification) {
259
290
  notificationTitle(notification),
260
291
  notification.title,
261
292
  notification.summary,
262
- notification.turnId ? `Turn ${notification.turnId}` : null,
293
+ notification.turnRef ? `Turn ref ${notification.turnRef}` : null,
263
294
  formatRelativeTime(notification.timestamp),
264
295
  ].filter(Boolean).join(". ");
265
296
  }
@@ -302,7 +333,7 @@ function turnTimeline(task) {
302
333
  const turnStatus = presentation.status;
303
334
  status.className = `status status-${turnStatus}`;
304
335
  status.textContent = turnStatus.replaceAll("_", " ");
305
- const turnKey = turn.turnId ?? `no-turn:${index}`;
336
+ const turnKey = turn.turnRef ?? turn.turnId ?? `no-turn:${index}`;
306
337
  const timestamp = timestampControl(presentation.updatedAt, {
307
338
  accessibleName: `Turn updated time for ${turnStatus.replaceAll("_", " ")}`,
308
339
  key: `detail:${task.id}:turn:${turnKey}`,
@@ -324,9 +355,7 @@ function turnTimeline(task) {
324
355
  appendLinkedText(result, presentation.summary, task);
325
356
  const turnMetadata = document.createElement("p");
326
357
  turnMetadata.className = "result-history-turn";
327
- turnMetadata.textContent = turn.turnId
328
- ? `Turn ${turn.turnId}`
329
- : "No turn ID (creation failure)";
358
+ turnMetadata.textContent = `Turn ref ${turn.turnRef ?? "not recorded"}; Codex turn ${turn.turnId ?? "unavailable"}`;
330
359
  item.append(header, requestLabel, request, resultLabel, result, turnMetadata);
331
360
  return item;
332
361
  });
@@ -342,7 +371,7 @@ function renderDialog(task) {
342
371
  results: task.results ?? preservedResults ?? [],
343
372
  };
344
373
  state.selectedTask = detailedTask;
345
- elements.dialogProject.textContent = task.project.name;
374
+ renderProject(elements.dialogProject, task);
346
375
  elements.dialogTitle.textContent = task.title;
347
376
  elements.dialogRelatedLinks.replaceChildren(...relatedGitHubLinks(detailedTask).children);
348
377
  elements.dialogRelatedLinks.setAttribute(
@@ -357,9 +386,11 @@ function renderDialog(task) {
357
386
  elements.copyThreadId.disabled = !task.threadId;
358
387
  elements.dialogMetadata.replaceChildren(
359
388
  ...detailRow("Current status", taskStatusLabel(task)),
360
- ...detailRow("Current turn ID", task.turnId),
389
+ ...detailRow("Current turn ref", task.turnRef),
390
+ ...detailRow("Current Codex turn ID", task.turnId),
361
391
  ...detailRow("Last result status", task.lastResult?.status?.replaceAll("_", " ")),
362
- ...detailRow("Last result turn ID", task.lastResult?.turnId),
392
+ ...detailRow("Last result turn ref", task.lastResult?.turnRef),
393
+ ...detailRow("Last result Codex turn ID", task.lastResult?.turnId),
363
394
  ...detailRow("Last result updated", timestampControl(task.lastResult?.updatedAt, {
364
395
  accessibleName: `Last result updated time for ${task.title}`,
365
396
  key: `detail:${task.id}:last-result-updated`,
@@ -421,7 +452,7 @@ function taskCard(task) {
421
452
  heading.append(title, badge);
422
453
  const project = document.createElement("p");
423
454
  project.className = "task-project";
424
- project.textContent = task.project.name;
455
+ renderProject(project, task);
425
456
  const summary = document.createElement("p");
426
457
  summary.className = "task-summary";
427
458
  const latest = latestTurnPresentation(task);
@@ -323,7 +323,8 @@ export function taskGitHubProjection(task) {
323
323
  taskRepository,
324
324
  })) {
325
325
  if (segment.kind !== "link") continue;
326
- const key = segment.url;
326
+ if (segment.type === "repository") continue;
327
+ const key = relatedLinkLabel(segment);
327
328
  if (seen.has(key)) continue;
328
329
  if (links.length === MAX_RELATED_GITHUB_LINKS) {
329
330
  truncated = true;
@@ -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
- if (lastIndex >= 0 && turns[lastIndex].turnId === task.latestTurn.turnId) {
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 === 8
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([task.id, task.turnId ?? null, task.status ?? "unresolved"]);
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?.turnId === task.turnId
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?.turnId === task.turnId
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({ ...task, turnId: result.turnId }, result.status),
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 (signatures.get(task.id) !== nextSignatures.get(task.id)) {
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({ taskId: prepared.id, threadId: null, turnId: null, status: "failed", summary: "Executor creation failed before the executor started." });
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(error, prepared.id, resultReporting);
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 working before substantive work in a newly linked or follow-up turn, with summary omitted or null and a concise requestSummary. Preserve known repository context and delivered issues or pull requests as canonical GitHub URLs, including both child-repository and workspace pull requests when applicable. Before ending the same turn, report needs_input, completed, or failed with a concise semantic summary and requestSummary omitted. Exact retries are idempotent; stale or mismatched turns are rejected.",
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),