taskchef 3.0.2 → 3.0.3
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/BACKLOG.md +15 -0
- package/README.md +28 -7
- package/SPEC.md +79 -33
- package/index.js +18 -0
- package/package.json +1 -1
- package/skills/taskchef-bootstrap/SKILL.md +2 -3
- package/skills/taskchef-delegate/SKILL.md +75 -11
- package/skills/taskchef-report/SKILL.md +20 -10
- package/src/cli.js +19 -1
- package/src/delegation.js +555 -0
- package/src/workspace.js +53 -172
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const THREAD_RESOLUTION_CHECKPOINTS_MS = Object.freeze([10_000, 29_000]);
|
|
4
|
+
export const THREAD_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
5
|
+
export const THREAD_RESOLUTION_RECENT_LIMIT = 50;
|
|
6
|
+
export const THREAD_RESOLUTION_CLOCK_SKEW_MS = 5_000;
|
|
7
|
+
|
|
8
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
9
|
+
|
|
10
|
+
function requireObject(value, name) {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12
|
+
throw new Error(`${name} must be an object`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requireString(value, name) {
|
|
18
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
19
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requireUuid(value, name = "taskId") {
|
|
25
|
+
requireString(value, name);
|
|
26
|
+
if (!UUID_PATTERN.test(value)) throw new Error(`${name} must be a lowercase full UUID`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseToolResult(value, name) {
|
|
31
|
+
if (typeof value !== "string") return requireObject(value, name);
|
|
32
|
+
try {
|
|
33
|
+
return requireObject(JSON.parse(value), name);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof SyntaxError) throw new Error(`${name} must contain valid JSON`);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function timestampMilliseconds(value) {
|
|
41
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
42
|
+
return value < 1_000_000_000_000 ? value * 1_000 : value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toolIdentifier(value) {
|
|
46
|
+
if (typeof value !== "string") return null;
|
|
47
|
+
const normalized = value.trim();
|
|
48
|
+
return normalized.length > 0 ? normalized : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isProvisionalThreadId(value) {
|
|
52
|
+
const id = toolIdentifier(value);
|
|
53
|
+
return id !== null && id.startsWith("local:");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function normalizeDurableThreadId(value, name = "threadId") {
|
|
57
|
+
const id = toolIdentifier(value);
|
|
58
|
+
if (id === null) throw new Error(`${name} must be a non-empty string`);
|
|
59
|
+
if (isProvisionalThreadId(id)) {
|
|
60
|
+
throw new Error(`${name} must be a durable thread ID, not a provisional local ID`);
|
|
61
|
+
}
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function wait(delayMs) {
|
|
66
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function taskChefMarker(taskId) {
|
|
70
|
+
return `# taskchef_id=${requireUuid(taskId)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseTaskChefMarker(instruction) {
|
|
74
|
+
if (typeof instruction !== "string") return null;
|
|
75
|
+
const firstLine = instruction.split(/\r?\n/, 1)[0];
|
|
76
|
+
const match = firstLine.match(/^# taskchef_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/);
|
|
77
|
+
return match?.[1] ?? null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
81
|
+
requireString(instruction, "instruction");
|
|
82
|
+
if (parseTaskChefMarker(instruction) !== null) {
|
|
83
|
+
throw new Error("instruction already contains a TaskChef marker");
|
|
84
|
+
}
|
|
85
|
+
const id = requireUuid(taskId);
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
instruction: `${taskChefMarker(id)}\n\n${instruction}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function listThreadEntries(result) {
|
|
93
|
+
const parsed = parseToolResult(result, "list_threads result");
|
|
94
|
+
const entries = [...(parsed.pinnedThreads ?? []), ...(parsed.threads ?? [])];
|
|
95
|
+
const unique = new Map();
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const id = toolIdentifier(entry?.id);
|
|
98
|
+
if (id !== null && !unique.has(id)) {
|
|
99
|
+
unique.set(id, { ...entry, id });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [...unique.values()];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function structuredDelegatedInputs(result) {
|
|
106
|
+
const parsed = parseToolResult(result, "read_thread result");
|
|
107
|
+
const inputs = [];
|
|
108
|
+
for (const turn of parsed.turns ?? []) {
|
|
109
|
+
for (const item of turn?.items ?? []) {
|
|
110
|
+
if (item?.type !== "userMessage") continue;
|
|
111
|
+
for (const part of item.content ?? []) {
|
|
112
|
+
if (typeof part?.codexDelegation?.input === "string") {
|
|
113
|
+
inputs.push(part.codexDelegation.input);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return inputs;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function hasExactTaskChefMarker(result, taskId) {
|
|
122
|
+
const marker = taskChefMarker(taskId);
|
|
123
|
+
return structuredDelegatedInputs(result).some(
|
|
124
|
+
(input) => input.split(/\r?\n/, 1)[0] === marker,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function filterThreadCandidates(result, {
|
|
129
|
+
baselineThreadIds = new Set(),
|
|
130
|
+
excludedThreadIds = new Set(),
|
|
131
|
+
hostId = null,
|
|
132
|
+
projectId = null,
|
|
133
|
+
title = null,
|
|
134
|
+
createdAfter = null,
|
|
135
|
+
environmentType = null,
|
|
136
|
+
} = {}) {
|
|
137
|
+
const minimumCreatedAt = createdAfter === null
|
|
138
|
+
? null
|
|
139
|
+
: timestampMilliseconds(createdAfter) - THREAD_RESOLUTION_CLOCK_SKEW_MS;
|
|
140
|
+
const candidates = listThreadEntries(result).filter((thread) => {
|
|
141
|
+
if (
|
|
142
|
+
baselineThreadIds.has(thread.id)
|
|
143
|
+
|| excludedThreadIds.has(thread.id)
|
|
144
|
+
|| isProvisionalThreadId(thread.id)
|
|
145
|
+
|| thread.kind !== "codex"
|
|
146
|
+
) return false;
|
|
147
|
+
if (hostId !== null && thread.hostId !== undefined && thread.hostId !== hostId) return false;
|
|
148
|
+
if (projectId !== null && thread.projectId !== undefined && thread.projectId !== projectId) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const candidateEnvironmentType = thread.environment?.type
|
|
152
|
+
?? thread.environmentType
|
|
153
|
+
?? thread.backing?.environment?.type;
|
|
154
|
+
if (
|
|
155
|
+
environmentType !== null &&
|
|
156
|
+
candidateEnvironmentType !== undefined &&
|
|
157
|
+
candidateEnvironmentType !== environmentType
|
|
158
|
+
) return false;
|
|
159
|
+
const candidateTime = timestampMilliseconds(thread.createdAt ?? thread.updatedAt);
|
|
160
|
+
if (minimumCreatedAt !== null && candidateTime !== null && candidateTime < minimumCreatedAt) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
});
|
|
165
|
+
if (title === null) return candidates;
|
|
166
|
+
return candidates.sort((left, right) =>
|
|
167
|
+
Number(right.title === title) - Number(left.title === title));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateResolutionSchedule(checkpointsMs, timeoutMs) {
|
|
171
|
+
if (!Array.isArray(checkpointsMs) || checkpointsMs.length === 0) {
|
|
172
|
+
throw new Error("checkpointsMs must contain at least one checkpoint");
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
|
|
175
|
+
throw new Error("timeoutMs must be positive");
|
|
176
|
+
}
|
|
177
|
+
let previous = 0;
|
|
178
|
+
for (const checkpoint of checkpointsMs) {
|
|
179
|
+
if (!Number.isInteger(checkpoint) || checkpoint <= previous || checkpoint > timeoutMs) {
|
|
180
|
+
throw new Error("checkpointsMs must be strictly increasing positive integers within timeoutMs");
|
|
181
|
+
}
|
|
182
|
+
previous = checkpoint;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function inspectCandidates(candidates, {
|
|
187
|
+
readThread,
|
|
188
|
+
taskId,
|
|
189
|
+
attempt,
|
|
190
|
+
canVerify = () => true,
|
|
191
|
+
}) {
|
|
192
|
+
const inspected = await Promise.all(candidates.map(async (candidate) => {
|
|
193
|
+
try {
|
|
194
|
+
const readResult = await readThread({
|
|
195
|
+
threadId: candidate.id,
|
|
196
|
+
...(candidate.hostId ? { hostId: candidate.hostId } : {}),
|
|
197
|
+
turnLimit: 1,
|
|
198
|
+
includeOutputs: false,
|
|
199
|
+
});
|
|
200
|
+
if (!canVerify()) {
|
|
201
|
+
return { candidate, matches: false, error: null };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
candidate,
|
|
205
|
+
matches: hasExactTaskChefMarker(readResult, taskId),
|
|
206
|
+
error: null,
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
candidate,
|
|
211
|
+
matches: false,
|
|
212
|
+
error: {
|
|
213
|
+
attempt,
|
|
214
|
+
operation: "readThread",
|
|
215
|
+
threadId: candidate.id,
|
|
216
|
+
message: error instanceof Error ? error.message : String(error),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
221
|
+
return {
|
|
222
|
+
exactMatches: inspected.filter((item) => item.matches).map((item) => item.candidate),
|
|
223
|
+
errors: inspected.flatMap((item) => item.error === null ? [] : [item.error]),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function createAndRecordDelegation({
|
|
228
|
+
project,
|
|
229
|
+
title,
|
|
230
|
+
instruction,
|
|
231
|
+
target,
|
|
232
|
+
expected = {},
|
|
233
|
+
createThread,
|
|
234
|
+
listThreads,
|
|
235
|
+
readThread,
|
|
236
|
+
recordTask,
|
|
237
|
+
resolveRecordedTask = null,
|
|
238
|
+
resolveProvisionalThread = null,
|
|
239
|
+
taskId = randomUUID(),
|
|
240
|
+
checkpointsMs = THREAD_RESOLUTION_CHECKPOINTS_MS,
|
|
241
|
+
timeoutMs = THREAD_RESOLUTION_TIMEOUT_MS,
|
|
242
|
+
recentLimit = THREAD_RESOLUTION_RECENT_LIMIT,
|
|
243
|
+
now = Date.now,
|
|
244
|
+
waitImpl = wait,
|
|
245
|
+
}) {
|
|
246
|
+
requireString(project, "project");
|
|
247
|
+
requireString(title, "title");
|
|
248
|
+
requireObject(target, "target");
|
|
249
|
+
for (const [value, name] of [
|
|
250
|
+
[createThread, "createThread"],
|
|
251
|
+
[listThreads, "listThreads"],
|
|
252
|
+
[readThread, "readThread"],
|
|
253
|
+
[recordTask, "recordTask"],
|
|
254
|
+
]) {
|
|
255
|
+
if (typeof value !== "function") throw new Error(`${name} must be a function`);
|
|
256
|
+
}
|
|
257
|
+
if (resolveProvisionalThread !== null && typeof resolveProvisionalThread !== "function") {
|
|
258
|
+
throw new Error("resolveProvisionalThread must be a function or null");
|
|
259
|
+
}
|
|
260
|
+
if (resolveRecordedTask !== null && typeof resolveRecordedTask !== "function") {
|
|
261
|
+
throw new Error("resolveRecordedTask must be a function or null");
|
|
262
|
+
}
|
|
263
|
+
validateResolutionSchedule(checkpointsMs, timeoutMs);
|
|
264
|
+
|
|
265
|
+
const prepared = prepareDelegation(instruction, { taskId });
|
|
266
|
+
const discoveryErrors = [];
|
|
267
|
+
const createdAfter = now();
|
|
268
|
+
const createResult = parseToolResult(await createThread({
|
|
269
|
+
prompt: prepared.instruction,
|
|
270
|
+
title,
|
|
271
|
+
target,
|
|
272
|
+
}), "create_thread result");
|
|
273
|
+
const clientThreadId = toolIdentifier(createResult.clientThreadId);
|
|
274
|
+
const pendingWorktreeId = toolIdentifier(createResult.pendingWorktreeId);
|
|
275
|
+
const returnedThreadId = toolIdentifier(createResult.threadId);
|
|
276
|
+
const returnedProvisionalId = isProvisionalThreadId(returnedThreadId)
|
|
277
|
+
? returnedThreadId
|
|
278
|
+
: null;
|
|
279
|
+
const provisional = clientThreadId ?? pendingWorktreeId ?? returnedProvisionalId;
|
|
280
|
+
const resolutionStartedAt = provisional === null ? null : now();
|
|
281
|
+
const provisionalIds = new Set([
|
|
282
|
+
clientThreadId,
|
|
283
|
+
pendingWorktreeId,
|
|
284
|
+
returnedProvisionalId,
|
|
285
|
+
].filter(Boolean));
|
|
286
|
+
const durableThreadId = !isProvisionalThreadId(returnedThreadId)
|
|
287
|
+
&& !provisionalIds.has(returnedThreadId)
|
|
288
|
+
? returnedThreadId
|
|
289
|
+
: null;
|
|
290
|
+
|
|
291
|
+
if (durableThreadId !== null) {
|
|
292
|
+
await recordTask({
|
|
293
|
+
id: prepared.id,
|
|
294
|
+
project,
|
|
295
|
+
title,
|
|
296
|
+
instruction: prepared.instruction,
|
|
297
|
+
threadId: durableThreadId,
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
status: "recorded",
|
|
301
|
+
resolution: "immediate",
|
|
302
|
+
...prepared,
|
|
303
|
+
threadId: durableThreadId,
|
|
304
|
+
hostId: createResult.hostId ?? expected.hostId ?? null,
|
|
305
|
+
provisional,
|
|
306
|
+
attempts: 0,
|
|
307
|
+
discoveryErrors,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (provisional === null) {
|
|
312
|
+
await recordTask({
|
|
313
|
+
id: prepared.id,
|
|
314
|
+
project,
|
|
315
|
+
title,
|
|
316
|
+
instruction: prepared.instruction,
|
|
317
|
+
threadId: null,
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
status: "recorded-unresolved",
|
|
321
|
+
reason: "create-returned-no-thread-identifier",
|
|
322
|
+
...prepared,
|
|
323
|
+
threadId: null,
|
|
324
|
+
provisional: null,
|
|
325
|
+
attempts: 0,
|
|
326
|
+
matchingThreadIds: [],
|
|
327
|
+
discoveryErrors,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await recordTask({
|
|
332
|
+
id: prepared.id,
|
|
333
|
+
project,
|
|
334
|
+
title,
|
|
335
|
+
instruction: prepared.instruction,
|
|
336
|
+
threadId: null,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (resolveRecordedTask === null) {
|
|
340
|
+
return {
|
|
341
|
+
status: "recorded-unresolved",
|
|
342
|
+
reason: "task-resolution-unavailable",
|
|
343
|
+
...prepared,
|
|
344
|
+
threadId: null,
|
|
345
|
+
provisional,
|
|
346
|
+
attempts: 0,
|
|
347
|
+
matchingThreadIds: [],
|
|
348
|
+
discoveryErrors,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const resolutionDeadline = resolutionStartedAt + timeoutMs;
|
|
353
|
+
let deadlineExpired = false;
|
|
354
|
+
const hasResolutionTime = () => {
|
|
355
|
+
if (now() < resolutionDeadline) return true;
|
|
356
|
+
deadlineExpired = true;
|
|
357
|
+
return false;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const acceptResolvedThread = async (thread, resolution, attempt, { verified = false } = {}) => {
|
|
361
|
+
const threadId = toolIdentifier(thread.id);
|
|
362
|
+
if (
|
|
363
|
+
threadId === null
|
|
364
|
+
|| provisionalIds.has(threadId)
|
|
365
|
+
|| isProvisionalThreadId(threadId)
|
|
366
|
+
) {
|
|
367
|
+
discoveryErrors.push({
|
|
368
|
+
attempt,
|
|
369
|
+
operation: "validateThreadId",
|
|
370
|
+
...(threadId === null ? {} : { threadId }),
|
|
371
|
+
message: "resolved threadId is not a durable identifier",
|
|
372
|
+
});
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const durableThread = { ...thread, id: threadId };
|
|
376
|
+
if (!hasResolutionTime()) return null;
|
|
377
|
+
if (!verified) {
|
|
378
|
+
const inspected = await inspectCandidates([durableThread], {
|
|
379
|
+
readThread,
|
|
380
|
+
taskId: prepared.id,
|
|
381
|
+
attempt,
|
|
382
|
+
canVerify: hasResolutionTime,
|
|
383
|
+
});
|
|
384
|
+
discoveryErrors.push(...inspected.errors);
|
|
385
|
+
if (inspected.exactMatches.length !== 1 || inspected.errors.length > 0) return null;
|
|
386
|
+
}
|
|
387
|
+
if (!hasResolutionTime()) return null;
|
|
388
|
+
try {
|
|
389
|
+
await resolveRecordedTask({ id: prepared.id, threadId });
|
|
390
|
+
} catch (error) {
|
|
391
|
+
discoveryErrors.push({
|
|
392
|
+
attempt,
|
|
393
|
+
operation: "resolveRecordedTask",
|
|
394
|
+
threadId,
|
|
395
|
+
message: error instanceof Error ? error.message : String(error),
|
|
396
|
+
});
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
status: "recorded",
|
|
401
|
+
resolution,
|
|
402
|
+
...prepared,
|
|
403
|
+
threadId,
|
|
404
|
+
hostId: durableThread.hostId ?? expected.hostId ?? null,
|
|
405
|
+
provisional,
|
|
406
|
+
attempts: attempt,
|
|
407
|
+
discoveryErrors,
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
if (resolveProvisionalThread !== null) {
|
|
412
|
+
let nativeResult = null;
|
|
413
|
+
let nativeAttempts = 0;
|
|
414
|
+
const remainingTimeoutMs = timeoutMs - (now() - resolutionStartedAt);
|
|
415
|
+
if (remainingTimeoutMs > 0) {
|
|
416
|
+
nativeAttempts = 1;
|
|
417
|
+
try {
|
|
418
|
+
const nativeResponse = await resolveProvisionalThread({
|
|
419
|
+
provisionalId: provisional,
|
|
420
|
+
...(clientThreadId ? { clientThreadId } : {}),
|
|
421
|
+
...(pendingWorktreeId ? { pendingWorktreeId } : {}),
|
|
422
|
+
timeoutMs: remainingTimeoutMs,
|
|
423
|
+
});
|
|
424
|
+
if (hasResolutionTime()) {
|
|
425
|
+
nativeResult = parseToolResult(nativeResponse, "provisional thread resolver result");
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (hasResolutionTime()) {
|
|
429
|
+
discoveryErrors.push({
|
|
430
|
+
attempt: 1,
|
|
431
|
+
operation: "resolveProvisionalThread",
|
|
432
|
+
message: error instanceof Error ? error.message : String(error),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
hasResolutionTime();
|
|
438
|
+
const nativeThreadId = toolIdentifier(nativeResult?.threadId);
|
|
439
|
+
if (nativeThreadId !== null) {
|
|
440
|
+
const resolved = await acceptResolvedThread({
|
|
441
|
+
id: nativeThreadId,
|
|
442
|
+
hostId: nativeResult.hostId ?? expected.hostId ?? null,
|
|
443
|
+
}, "native", 1);
|
|
444
|
+
if (resolved !== null) return resolved;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
status: "recorded-unresolved",
|
|
448
|
+
reason: discoveryErrors.length > 0
|
|
449
|
+
? "thread-discovery-error"
|
|
450
|
+
: deadlineExpired
|
|
451
|
+
? "resolution-deadline-exhausted"
|
|
452
|
+
: "native-resolution-unresolved",
|
|
453
|
+
...prepared,
|
|
454
|
+
threadId: null,
|
|
455
|
+
provisional,
|
|
456
|
+
attempts: nativeAttempts,
|
|
457
|
+
matchingThreadIds: [],
|
|
458
|
+
discoveryErrors,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let exactMatches = [];
|
|
463
|
+
let ambiguousMatches = [];
|
|
464
|
+
let attemptsMade = 0;
|
|
465
|
+
for (let index = 0; index < checkpointsMs.length; index += 1) {
|
|
466
|
+
const attempt = index + 1;
|
|
467
|
+
const checkpointMs = checkpointsMs[index];
|
|
468
|
+
const elapsedBeforeAttempt = now() - resolutionStartedAt;
|
|
469
|
+
if (elapsedBeforeAttempt > timeoutMs) break;
|
|
470
|
+
const remainingDelayMs = checkpointMs - elapsedBeforeAttempt;
|
|
471
|
+
if (remainingDelayMs > 0) {
|
|
472
|
+
try {
|
|
473
|
+
await waitImpl(remainingDelayMs);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
discoveryErrors.push({
|
|
476
|
+
attempt,
|
|
477
|
+
operation: "wait",
|
|
478
|
+
message: error instanceof Error ? error.message : String(error),
|
|
479
|
+
});
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!hasResolutionTime()) break;
|
|
484
|
+
attemptsMade = attempt;
|
|
485
|
+
let candidates = [];
|
|
486
|
+
let attemptFailed = false;
|
|
487
|
+
try {
|
|
488
|
+
const snapshot = await listThreads({ limit: recentLimit });
|
|
489
|
+
if (hasResolutionTime()) {
|
|
490
|
+
candidates = filterThreadCandidates(snapshot, {
|
|
491
|
+
excludedThreadIds: provisionalIds,
|
|
492
|
+
hostId: expected.hostId ?? null,
|
|
493
|
+
projectId: expected.projectId ?? target.projectId ?? null,
|
|
494
|
+
title,
|
|
495
|
+
createdAfter,
|
|
496
|
+
environmentType: expected.environmentType ?? target.environment?.type ?? null,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (hasResolutionTime()) {
|
|
501
|
+
attemptFailed = true;
|
|
502
|
+
discoveryErrors.push({
|
|
503
|
+
attempt,
|
|
504
|
+
operation: "listThreads",
|
|
505
|
+
message: error instanceof Error ? error.message : String(error),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!hasResolutionTime()) break;
|
|
510
|
+
const inspected = await inspectCandidates(candidates, {
|
|
511
|
+
readThread,
|
|
512
|
+
taskId: prepared.id,
|
|
513
|
+
attempt,
|
|
514
|
+
canVerify: hasResolutionTime,
|
|
515
|
+
});
|
|
516
|
+
exactMatches = inspected.exactMatches;
|
|
517
|
+
discoveryErrors.push(...inspected.errors);
|
|
518
|
+
if (inspected.errors.length > 0) attemptFailed = true;
|
|
519
|
+
if (!hasResolutionTime()) break;
|
|
520
|
+
if (exactMatches.length > 1) ambiguousMatches = exactMatches;
|
|
521
|
+
if (
|
|
522
|
+
exactMatches.length === 1
|
|
523
|
+
&& ambiguousMatches.length === 0
|
|
524
|
+
&& !attemptFailed
|
|
525
|
+
&& discoveryErrors.length === 0
|
|
526
|
+
) {
|
|
527
|
+
const resolved = await acceptResolvedThread(
|
|
528
|
+
exactMatches[0],
|
|
529
|
+
"discovered",
|
|
530
|
+
attempt,
|
|
531
|
+
{ verified: true },
|
|
532
|
+
);
|
|
533
|
+
if (resolved !== null) return resolved;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const reason = ambiguousMatches.length > 1
|
|
538
|
+
? "multiple-exact-marker-matches"
|
|
539
|
+
: discoveryErrors.length > 0
|
|
540
|
+
? "thread-discovery-error"
|
|
541
|
+
: deadlineExpired
|
|
542
|
+
? "resolution-deadline-exhausted"
|
|
543
|
+
: "no-exact-marker-match";
|
|
544
|
+
return {
|
|
545
|
+
status: "recorded-unresolved",
|
|
546
|
+
reason,
|
|
547
|
+
...prepared,
|
|
548
|
+
threadId: null,
|
|
549
|
+
provisional,
|
|
550
|
+
attempts: attemptsMade,
|
|
551
|
+
discoveryErrors,
|
|
552
|
+
matchingThreadIds: (ambiguousMatches.length > 1 ? ambiguousMatches : exactMatches)
|
|
553
|
+
.map((thread) => thread.id),
|
|
554
|
+
};
|
|
555
|
+
}
|