taskchef 7.14.1 → 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 +23 -18
- package/src/dashboard/github-links.js +160 -24
- 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/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
turnPresentation,
|
|
17
17
|
} from "./state.js";
|
|
18
18
|
import { openTaskFromControl } from "./actions.js";
|
|
19
|
-
import {
|
|
19
|
+
import { referenceSegments } from "./github-links.js";
|
|
20
20
|
import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
21
21
|
|
|
22
22
|
const state = {
|
|
@@ -61,28 +61,33 @@ const elements = {
|
|
|
61
61
|
};
|
|
62
62
|
let notificationDescriptionSerial = 0;
|
|
63
63
|
|
|
64
|
-
function
|
|
64
|
+
function referenceLink(link, { compact = 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
70
|
anchor.textContent = link.label ?? link.text;
|
|
71
|
+
const githubKind = link.type === "issue"
|
|
72
|
+
? ", GitHub issue"
|
|
73
|
+
: link.type === "pull"
|
|
74
|
+
? ", GitHub pull request"
|
|
75
|
+
: link.provider === "github" || link.owner ? " on GitHub" : "";
|
|
71
76
|
anchor.setAttribute(
|
|
72
77
|
"aria-label",
|
|
73
|
-
`${link.label ?? link.text}
|
|
78
|
+
`${link.label ?? link.text}${githubKind} (opens in a new tab)`,
|
|
74
79
|
);
|
|
75
80
|
anchor.addEventListener("click", (event) => event.stopPropagation());
|
|
76
81
|
return anchor;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
function
|
|
80
|
-
const children =
|
|
84
|
+
function appendLinkedText(container, text, task) {
|
|
85
|
+
const children = referenceSegments(text, {
|
|
81
86
|
projectRepositories: task.project?.githubRepos,
|
|
82
87
|
taskRepository: task.relatedGitHubRepository,
|
|
83
88
|
}).map((segment) => {
|
|
84
89
|
if (segment.kind === "text") return document.createTextNode(segment.text);
|
|
85
|
-
if (segment.kind === "link") return
|
|
90
|
+
if (segment.kind === "link") return referenceLink(segment);
|
|
86
91
|
const ambiguous = document.createElement("span");
|
|
87
92
|
ambiguous.className = "github-reference-ambiguous";
|
|
88
93
|
ambiguous.textContent = segment.text;
|
|
@@ -99,7 +104,7 @@ function relatedGitHubLinks(task, { compact = false } = {}) {
|
|
|
99
104
|
const container = document.createElement("nav");
|
|
100
105
|
container.className = `github-links${compact ? " github-links-compact" : ""}`;
|
|
101
106
|
container.setAttribute("aria-label", `Related GitHub links for ${task.title}`);
|
|
102
|
-
const children = links.map((link) =>
|
|
107
|
+
const children = links.map((link) => referenceLink(link, { compact }));
|
|
103
108
|
if (task.relatedGitHubLinksTruncated) {
|
|
104
109
|
const more = document.createElement("span");
|
|
105
110
|
more.className = "github-links-more";
|
|
@@ -254,7 +259,7 @@ function notificationAnnouncement(notification) {
|
|
|
254
259
|
notificationTitle(notification),
|
|
255
260
|
notification.title,
|
|
256
261
|
notification.summary,
|
|
257
|
-
notification.
|
|
262
|
+
notification.turnRef ? `Turn ref ${notification.turnRef}` : null,
|
|
258
263
|
formatRelativeTime(notification.timestamp),
|
|
259
264
|
].filter(Boolean).join(". ");
|
|
260
265
|
}
|
|
@@ -297,7 +302,7 @@ function turnTimeline(task) {
|
|
|
297
302
|
const turnStatus = presentation.status;
|
|
298
303
|
status.className = `status status-${turnStatus}`;
|
|
299
304
|
status.textContent = turnStatus.replaceAll("_", " ");
|
|
300
|
-
const turnKey = turn.turnId ?? `no-turn:${index}`;
|
|
305
|
+
const turnKey = turn.turnRef ?? turn.turnId ?? `no-turn:${index}`;
|
|
301
306
|
const timestamp = timestampControl(presentation.updatedAt, {
|
|
302
307
|
accessibleName: `Turn updated time for ${turnStatus.replaceAll("_", " ")}`,
|
|
303
308
|
key: `detail:${task.id}:turn:${turnKey}`,
|
|
@@ -307,7 +312,7 @@ function turnTimeline(task) {
|
|
|
307
312
|
requestLabel.textContent = "Request";
|
|
308
313
|
const request = document.createElement("p");
|
|
309
314
|
request.className = "preserve-lines";
|
|
310
|
-
|
|
315
|
+
appendLinkedText(
|
|
311
316
|
request,
|
|
312
317
|
turn.requestSummary ?? "Request not recorded by this TaskChef version.",
|
|
313
318
|
task,
|
|
@@ -316,12 +321,10 @@ function turnTimeline(task) {
|
|
|
316
321
|
resultLabel.textContent = "Result";
|
|
317
322
|
const result = document.createElement("p");
|
|
318
323
|
result.className = "preserve-lines";
|
|
319
|
-
|
|
324
|
+
appendLinkedText(result, presentation.summary, task);
|
|
320
325
|
const turnMetadata = document.createElement("p");
|
|
321
326
|
turnMetadata.className = "result-history-turn";
|
|
322
|
-
turnMetadata.textContent = turn.turnId
|
|
323
|
-
? `Turn ${turn.turnId}`
|
|
324
|
-
: "No turn ID (creation failure)";
|
|
327
|
+
turnMetadata.textContent = `Turn ref ${turn.turnRef ?? "not recorded"}; Codex turn ${turn.turnId ?? "unavailable"}`;
|
|
325
328
|
item.append(header, requestLabel, request, resultLabel, result, turnMetadata);
|
|
326
329
|
return item;
|
|
327
330
|
});
|
|
@@ -352,9 +355,11 @@ function renderDialog(task) {
|
|
|
352
355
|
elements.copyThreadId.disabled = !task.threadId;
|
|
353
356
|
elements.dialogMetadata.replaceChildren(
|
|
354
357
|
...detailRow("Current status", taskStatusLabel(task)),
|
|
355
|
-
...detailRow("Current turn
|
|
358
|
+
...detailRow("Current turn ref", task.turnRef),
|
|
359
|
+
...detailRow("Current Codex turn ID", task.turnId),
|
|
356
360
|
...detailRow("Last result status", task.lastResult?.status?.replaceAll("_", " ")),
|
|
357
|
-
...detailRow("Last result turn
|
|
361
|
+
...detailRow("Last result turn ref", task.lastResult?.turnRef),
|
|
362
|
+
...detailRow("Last result Codex turn ID", task.lastResult?.turnId),
|
|
358
363
|
...detailRow("Last result updated", timestampControl(task.lastResult?.updatedAt, {
|
|
359
364
|
accessibleName: `Last result updated time for ${task.title}`,
|
|
360
365
|
key: `detail:${task.id}:last-result-updated`,
|
|
@@ -424,12 +429,12 @@ function taskCard(task) {
|
|
|
424
429
|
requestLabel.textContent = "Request";
|
|
425
430
|
const request = document.createElement("span");
|
|
426
431
|
request.className = "preserve-lines";
|
|
427
|
-
request
|
|
432
|
+
appendLinkedText(request, latest.requestSummary, task);
|
|
428
433
|
const resultLabel = document.createElement("strong");
|
|
429
434
|
resultLabel.textContent = "Result";
|
|
430
435
|
const result = document.createElement("span");
|
|
431
436
|
result.className = "preserve-lines";
|
|
432
|
-
result
|
|
437
|
+
appendLinkedText(result, latest.resultSummary, task);
|
|
433
438
|
summary.replaceChildren(requestLabel, request, resultLabel, result);
|
|
434
439
|
const relatedLinks = relatedGitHubLinks(task, { compact: true });
|
|
435
440
|
const time = timestampControl(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const GITHUB_REPOSITORY = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+$/;
|
|
2
|
-
const REFERENCE_PATTERN = /https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)\/(issues|pull)\/([1-9]\d*)(?![A-Za-z0-9_])|([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)#([1-9]\d*)(?![A-Za-z0-9_])|#([1-9]\d*)(?![A-Za-z0-9_])|https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9_-])?)\/?(?=$|[\s),.;:!?'"\]}>])/gi;
|
|
2
|
+
const REFERENCE_PATTERN = /https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)\/(issues|pull)\/([1-9]\d*)(?![A-Za-z0-9_])|([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)#([1-9]\d*)(?![A-Za-z0-9_])|#([1-9]\d*)(?![A-Za-z0-9_])|https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9_-])?)\/?(?=$|[\s#),.;:!?'"\]}>])/gi;
|
|
3
|
+
const ABSOLUTE_HTTP_PATTERN = /https?:\/\/[^\s<>"'`]+/gi;
|
|
3
4
|
export const MAX_RELATED_GITHUB_LINKS = 20;
|
|
4
5
|
|
|
5
6
|
function canonicalRepository(owner, repository) {
|
|
@@ -23,10 +24,35 @@ export function normalizeGitHubRepository(value) {
|
|
|
23
24
|
|
|
24
25
|
function rawReferences(text) {
|
|
25
26
|
const value = String(text ?? "");
|
|
27
|
+
const absoluteRanges = absoluteHttpCandidates(value);
|
|
26
28
|
const references = [];
|
|
27
29
|
for (const match of value.matchAll(REFERENCE_PATTERN)) {
|
|
28
30
|
const start = match.index;
|
|
29
31
|
const preceding = start > 0 ? value[start - 1] : "";
|
|
32
|
+
let explicitAbsoluteUrl = null;
|
|
33
|
+
const containingAbsoluteUrl = absoluteRanges.find((range) => (
|
|
34
|
+
start >= range.start && start < range.end
|
|
35
|
+
));
|
|
36
|
+
if (containingAbsoluteUrl) {
|
|
37
|
+
const explicitGitHubUrl = Boolean(match[1] || match[9]);
|
|
38
|
+
let absoluteHost = null;
|
|
39
|
+
try {
|
|
40
|
+
absoluteHost = new URL(containingAbsoluteUrl.text).hostname.toLowerCase();
|
|
41
|
+
} catch {
|
|
42
|
+
// Invalid absolute URL tokens still block nested shorthand recognition.
|
|
43
|
+
}
|
|
44
|
+
const completeGitHubUrl = explicitGitHubUrl
|
|
45
|
+
&& start === containingAbsoluteUrl.start
|
|
46
|
+
&& (absoluteHost === "github.com" || absoluteHost === "www.github.com");
|
|
47
|
+
if (!completeGitHubUrl) continue;
|
|
48
|
+
const safeUrl = safeAbsoluteHttpUrl(containingAbsoluteUrl.text);
|
|
49
|
+
if (!safeUrl) continue;
|
|
50
|
+
explicitAbsoluteUrl = {
|
|
51
|
+
end: containingAbsoluteUrl.end,
|
|
52
|
+
suffix: containingAbsoluteUrl.text.slice(match[0].length),
|
|
53
|
+
text: containingAbsoluteUrl.text,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
30
56
|
if (match[8] && /[\w&#/]/.test(preceding)) continue;
|
|
31
57
|
if ((match[5] || match[1] || match[9]) && /[\w/]/.test(preceding)) continue;
|
|
32
58
|
|
|
@@ -45,14 +71,15 @@ function rawReferences(text) {
|
|
|
45
71
|
[owner, repository] = normalized.split("/");
|
|
46
72
|
}
|
|
47
73
|
references.push({
|
|
48
|
-
end: start + match[0].length,
|
|
74
|
+
end: explicitAbsoluteUrl?.end ?? start + match[0].length,
|
|
49
75
|
explicit,
|
|
50
76
|
number,
|
|
51
77
|
owner,
|
|
52
78
|
repository,
|
|
53
79
|
start,
|
|
54
|
-
text: match[0],
|
|
80
|
+
text: explicitAbsoluteUrl?.text ?? match[0],
|
|
55
81
|
type,
|
|
82
|
+
urlSuffix: explicitAbsoluteUrl?.suffix ?? "",
|
|
56
83
|
});
|
|
57
84
|
}
|
|
58
85
|
return references;
|
|
@@ -83,26 +110,111 @@ function resolvedReference(reference, repositoryContext) {
|
|
|
83
110
|
if (reference.type === "repository") {
|
|
84
111
|
return {
|
|
85
112
|
kind: "link",
|
|
113
|
+
label: `${owner}/${repositoryName}`,
|
|
86
114
|
number: null,
|
|
87
115
|
owner,
|
|
116
|
+
provider: "github",
|
|
88
117
|
repository: repositoryName,
|
|
89
118
|
text: reference.text,
|
|
90
119
|
type: reference.type,
|
|
91
|
-
url: `https://github.com/${owner}/${repositoryName}`,
|
|
120
|
+
url: `https://github.com/${owner}/${repositoryName}${reference.urlSuffix}`,
|
|
92
121
|
};
|
|
93
122
|
}
|
|
94
123
|
const path = reference.type === "pull" ? "pull" : "issues";
|
|
95
124
|
return {
|
|
96
125
|
kind: "link",
|
|
126
|
+
label: `${owner}/${repositoryName}#${reference.number}`,
|
|
97
127
|
number: reference.number,
|
|
98
128
|
owner,
|
|
129
|
+
provider: "github",
|
|
99
130
|
repository: repositoryName,
|
|
100
131
|
text: reference.text,
|
|
101
132
|
type: reference.type,
|
|
102
|
-
url: `https://github.com/${owner}/${repositoryName}/${path}/${reference.number}`,
|
|
133
|
+
url: `https://github.com/${owner}/${repositoryName}/${path}/${reference.number}${reference.urlSuffix}`,
|
|
103
134
|
};
|
|
104
135
|
}
|
|
105
136
|
|
|
137
|
+
function trimAbsoluteUrl(candidate) {
|
|
138
|
+
let trimmed = candidate;
|
|
139
|
+
let previous;
|
|
140
|
+
do {
|
|
141
|
+
previous = trimmed;
|
|
142
|
+
trimmed = trimmed.replace(/[.,;:!?]+$/u, "");
|
|
143
|
+
for (const [opening, closing] of [["(", ")"], ["[", "]"], ["{", "}"]]) {
|
|
144
|
+
while (trimmed.endsWith(closing)) {
|
|
145
|
+
const openings = [...trimmed].filter((character) => character === opening).length;
|
|
146
|
+
const closings = [...trimmed].filter((character) => character === closing).length;
|
|
147
|
+
if (closings <= openings) break;
|
|
148
|
+
trimmed = trimmed.slice(0, -1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} while (trimmed !== previous);
|
|
152
|
+
return trimmed;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function absoluteHttpCandidates(text) {
|
|
156
|
+
const candidates = [];
|
|
157
|
+
for (const match of String(text ?? "").matchAll(ABSOLUTE_HTTP_PATTERN)) {
|
|
158
|
+
const candidate = trimAbsoluteUrl(match[0]);
|
|
159
|
+
if (!candidate) continue;
|
|
160
|
+
candidates.push({
|
|
161
|
+
end: match.index + candidate.length,
|
|
162
|
+
start: match.index,
|
|
163
|
+
text: candidate,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return candidates;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasDotPathSegment(candidate) {
|
|
170
|
+
const authorityAndPath = candidate.slice(candidate.indexOf("//") + 2).split(/[?#]/u, 1)[0];
|
|
171
|
+
const pathStart = authorityAndPath.indexOf("/");
|
|
172
|
+
if (pathStart < 0) return false;
|
|
173
|
+
return authorityAndPath.slice(pathStart).split("/").some((segment) => {
|
|
174
|
+
try {
|
|
175
|
+
const decoded = decodeURIComponent(segment);
|
|
176
|
+
return decoded === "." || decoded === "..";
|
|
177
|
+
} catch {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function safeAbsoluteHttpUrl(candidate) {
|
|
184
|
+
try {
|
|
185
|
+
const url = new URL(candidate);
|
|
186
|
+
if (
|
|
187
|
+
!url.hostname
|
|
188
|
+
|| url.username
|
|
189
|
+
|| url.password
|
|
190
|
+
|| candidate.includes("\\")
|
|
191
|
+
|| hasDotPathSegment(candidate)
|
|
192
|
+
) return null;
|
|
193
|
+
return url.href;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function rawAbsoluteHttpReferences(text) {
|
|
200
|
+
const references = [];
|
|
201
|
+
for (const candidate of absoluteHttpCandidates(text)) {
|
|
202
|
+
const url = safeAbsoluteHttpUrl(candidate.text);
|
|
203
|
+
if (!url) continue;
|
|
204
|
+
references.push({
|
|
205
|
+
end: candidate.end,
|
|
206
|
+
kind: "link",
|
|
207
|
+
label: candidate.text,
|
|
208
|
+
provider: "web",
|
|
209
|
+
start: candidate.start,
|
|
210
|
+
text: candidate.text,
|
|
211
|
+
type: "url",
|
|
212
|
+
url,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return references;
|
|
216
|
+
}
|
|
217
|
+
|
|
106
218
|
export function githubReferenceSegments(text, {
|
|
107
219
|
projectRepositories = [],
|
|
108
220
|
taskRepository = null,
|
|
@@ -134,6 +246,45 @@ export function githubReferenceSegments(text, {
|
|
|
134
246
|
return segments;
|
|
135
247
|
}
|
|
136
248
|
|
|
249
|
+
export function referenceSegments(text, options = {}) {
|
|
250
|
+
const value = String(text ?? "");
|
|
251
|
+
const githubSegments = githubReferenceSegments(value, options);
|
|
252
|
+
const references = [];
|
|
253
|
+
let cursor = 0;
|
|
254
|
+
for (const segment of githubSegments) {
|
|
255
|
+
const start = value.indexOf(segment.text, cursor);
|
|
256
|
+
if (start < 0) continue;
|
|
257
|
+
if (segment.kind !== "text") {
|
|
258
|
+
references.push({ end: start + segment.text.length, segment, start });
|
|
259
|
+
}
|
|
260
|
+
cursor = start + segment.text.length;
|
|
261
|
+
}
|
|
262
|
+
const absoluteReferences = rawAbsoluteHttpReferences(value).filter((reference) => (
|
|
263
|
+
!references.some(({ end, start }) => reference.start < end && reference.end > start)
|
|
264
|
+
));
|
|
265
|
+
if (absoluteReferences.length === 0) return githubSegments;
|
|
266
|
+
|
|
267
|
+
references.push(...absoluteReferences.map((segment) => ({
|
|
268
|
+
end: segment.end,
|
|
269
|
+
segment,
|
|
270
|
+
start: segment.start,
|
|
271
|
+
})));
|
|
272
|
+
references.sort((left, right) => left.start - right.start);
|
|
273
|
+
|
|
274
|
+
const segments = [];
|
|
275
|
+
let offset = 0;
|
|
276
|
+
for (const reference of references) {
|
|
277
|
+
if (reference.start < offset) continue;
|
|
278
|
+
if (reference.start > offset) {
|
|
279
|
+
segments.push({ kind: "text", text: value.slice(offset, reference.start) });
|
|
280
|
+
}
|
|
281
|
+
segments.push(reference.segment);
|
|
282
|
+
offset = reference.end;
|
|
283
|
+
}
|
|
284
|
+
if (offset < value.length) segments.push({ kind: "text", text: value.slice(offset) });
|
|
285
|
+
return segments;
|
|
286
|
+
}
|
|
287
|
+
|
|
137
288
|
function taskTexts(task) {
|
|
138
289
|
const texts = [task.instruction];
|
|
139
290
|
for (const turn of task.turns ?? []) {
|
|
@@ -146,16 +297,9 @@ function taskTexts(task) {
|
|
|
146
297
|
return texts.filter((text) => typeof text === "string" && text.length > 0);
|
|
147
298
|
}
|
|
148
299
|
|
|
149
|
-
function relatedLinkLabel(link
|
|
300
|
+
function relatedLinkLabel(link) {
|
|
150
301
|
if (link.type === "repository") return `${link.owner}/${link.repository}`;
|
|
151
|
-
|
|
152
|
-
? `PR #${link.number}`
|
|
153
|
-
: link.type === "issue"
|
|
154
|
-
? `Issue #${link.number}`
|
|
155
|
-
: `#${link.number}`;
|
|
156
|
-
if (!includeRepository) return reference;
|
|
157
|
-
const repository = includeOwner ? `${link.owner}/${link.repository}` : link.repository;
|
|
158
|
-
return `${repository} ${reference}`;
|
|
302
|
+
return `${link.owner}/${link.repository}#${link.number}`;
|
|
159
303
|
}
|
|
160
304
|
|
|
161
305
|
export function taskGitHubProjection(task) {
|
|
@@ -179,7 +323,7 @@ export function taskGitHubProjection(task) {
|
|
|
179
323
|
taskRepository,
|
|
180
324
|
})) {
|
|
181
325
|
if (segment.kind !== "link") continue;
|
|
182
|
-
const key = segment.url
|
|
326
|
+
const key = segment.url;
|
|
183
327
|
if (seen.has(key)) continue;
|
|
184
328
|
if (links.length === MAX_RELATED_GITHUB_LINKS) {
|
|
185
329
|
truncated = true;
|
|
@@ -189,17 +333,9 @@ export function taskGitHubProjection(task) {
|
|
|
189
333
|
links.push(segment);
|
|
190
334
|
}
|
|
191
335
|
}
|
|
192
|
-
const repositories = new Set(links.map(({ owner, repository }) => `${owner}/${repository}`));
|
|
193
|
-
const repositoryNames = new Map();
|
|
194
|
-
for (const link of links) {
|
|
195
|
-
const owners = repositoryNames.get(link.repository) ?? new Set();
|
|
196
|
-
owners.add(link.owner);
|
|
197
|
-
repositoryNames.set(link.repository, owners);
|
|
198
|
-
}
|
|
199
|
-
const includeRepository = repositories.size > 1;
|
|
200
336
|
return {
|
|
201
337
|
relatedGitHubLinks: links.map((link) => ({
|
|
202
|
-
label: relatedLinkLabel(link
|
|
338
|
+
label: relatedLinkLabel(link),
|
|
203
339
|
number: link.number,
|
|
204
340
|
owner: link.owner,
|
|
205
341
|
repository: link.repository,
|
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);
|