taskchef 3.0.2 → 4.0.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: taskchef-report
3
- description: "Report the live state of Codex tasks recorded in a TaskChef task history. Use only when the user asks for status, outcomes, or a report about delegated work. Queries each relevant task once, never polls or waits, and never persists status or results."
3
+ description: "Report the live state of Codex tasks recorded in a TaskChef task history. Use only when the user asks for status, outcomes, or a report about delegated work. Queries each relevant task once, may resolve a nullable thread ID from one exact marker match, never polls or waits, and never persists status or results."
4
4
  ---
5
5
 
6
6
  # TaskChef Report
@@ -23,16 +23,26 @@ all deterministic task-log operations.
23
23
  `<plugin-root>/bin/taskchef.js task list --json --workspace <workspace>`
24
24
  once, then select matching entries. Ask the user if the match is ambiguous.
25
25
  - Use the full list only when the user asks for an overview of the task history.
26
- 2. Query every selected thread exactly once using immediate native snapshots,
27
- with no more than eight targets per call.
28
- 3. Summarize the live state and any reported outcome for each requested task.
26
+ 2. Separate entries whose `threadId` is `null`. For those entries, take one
27
+ `list_threads` snapshot with limit 50, filter by available project metadata,
28
+ and inspect candidate structured delegated inputs. Use title only to
29
+ prioritize candidates, never to exclude them. When exactly one candidate
30
+ starts with the task's exact marker, run
31
+ `<plugin-root>/bin/taskchef.js task resolve <task-id> --thread-id <thread-id> --json --workspace <workspace>`.
32
+ Do not resolve zero or multiple matches. Report unmatched entries as
33
+ recorded but unresolved and do not pass them to native thread tools.
34
+ 3. Query every resolved or previously durable thread exactly once using
35
+ immediate native snapshots, with no more than eight targets per call.
36
+ 4. Summarize the live state and any reported outcome for each requested task.
29
37
  Distinguish active work, requests for user input, completed work, and failed
30
38
  or partial attempts.
31
- 4. Treat each Codex task as the source of truth. The task log proves that
32
- TaskChef created the task, but it does not contain the task's current state.
33
- 5. Never update `tasks.jsonl`. Never persist status, results, transcripts,
34
- or hidden reasoning. Do not poll or wait for future activity.
39
+ 5. Treat each Codex task as the source of truth. The task log records what
40
+ TaskChef submitted, but it does not contain the task's current state.
41
+ 6. Never edit `tasks.jsonl` directly. Use `task resolve` only for one exact
42
+ marker match. Never persist status, results, transcripts, or hidden
43
+ reasoning. Do not poll or wait for future activity.
35
44
 
36
45
  If the task history is empty, say that TaskChef has not recorded any tasks. If
37
- a recorded task cannot be read, identify it by task ID and thread ID, then
38
- continue with the remaining entries.
46
+ a task has no durable thread ID, identify it by task ID and say that its marker
47
+ remains available for later recovery. If a recorded thread cannot be read,
48
+ identify it by task ID and thread ID, then continue with the remaining entries.
package/src/cli.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  readTask,
13
13
  recordTask,
14
14
  removeProject,
15
+ resolveTask,
15
16
  } from "./workspace.js";
16
17
 
17
18
  async function readStdin() {
@@ -36,6 +37,14 @@ function option(args, name, fallback) {
36
37
  return args[index + 1];
37
38
  }
38
39
 
40
+ function options(args, name) {
41
+ const values = [];
42
+ for (let index = 0; index < args.length; index += 1) {
43
+ if (args[index] === name) values.push(args[index + 1]);
44
+ }
45
+ return values;
46
+ }
47
+
39
48
  function validateCommandArgs(
40
49
  args,
41
50
  startIndex,
@@ -92,7 +101,6 @@ async function initialize(args) {
92
101
  `Workspace: ${value.workspace}`,
93
102
  `Configuration: ${value.config.action}`,
94
103
  `Task log: ${value.tasks.action}`,
95
- `Legacy tasks: ${value.legacyTasks.action}`,
96
104
  `Instructions: ${value.instructions.action}`,
97
105
  `Legacy skill links removed: ${value.legacySkills.removed.length}`,
98
106
  ].join("\n"));
@@ -113,6 +121,7 @@ async function projectAdd(args) {
113
121
  validateCommandArgs(args, 3, {
114
122
  values: ["--workspace", "--name", "--description", "--github-repo"],
115
123
  switches: ["--json", "--no-github"],
124
+ repeatable: ["--github-repo"],
116
125
  });
117
126
  if (args.includes("--no-github") && args.includes("--github-repo")) {
118
127
  throw new Error("--no-github and --github-repo cannot be used together");
@@ -122,8 +131,8 @@ async function projectAdd(args) {
122
131
  const description = option(args, "--description", null);
123
132
  if (name !== null) input.name = name;
124
133
  if (description !== null) input.description = description;
125
- if (args.includes("--no-github")) input.githubRepo = null;
126
- else if (args.includes("--github-repo")) input.githubRepo = option(args, "--github-repo");
134
+ if (args.includes("--no-github")) input.githubRepos = [];
135
+ else if (args.includes("--github-repo")) input.githubRepos = options(args, "--github-repo");
127
136
  const project = await addProject(workspaceRoot(args), input);
128
137
  print(project, args, (value) => `Added ${value.name}: ${value.path}`);
129
138
  return 0;
@@ -152,10 +161,11 @@ async function projectList(args) {
152
161
  validateCommandArgs(args, 2, { values: ["--workspace"], switches: ["--json"] });
153
162
  const projects = await listProjects(workspaceRoot(args));
154
163
  print({ projectCount: projects.length, projects }, args, (value) => table(
155
- ["NAME", "KIND", "PATH"],
164
+ ["NAME", "KIND", "GITHUB REPOSITORIES", "PATH"],
156
165
  value.projects.map((project) => [
157
166
  project.name,
158
167
  project.isGitRepository ? "git" : "folder",
168
+ project.githubRepos.join(", ") || "-",
159
169
  project.path,
160
170
  ]),
161
171
  ));
@@ -180,6 +190,22 @@ async function taskRecord(args) {
180
190
  return 0;
181
191
  }
182
192
 
193
+ async function taskResolve(args) {
194
+ if (!args[2] || args[2].startsWith("--")) throw new Error("task resolve requires a task ID");
195
+ validateCommandArgs(args, 3, {
196
+ values: ["--thread-id", "--workspace"],
197
+ switches: ["--json"],
198
+ });
199
+ if (!args.includes("--thread-id")) throw new Error("task resolve requires --thread-id");
200
+ const task = await resolveTask(
201
+ workspaceRoot(args),
202
+ args[2],
203
+ option(args, "--thread-id"),
204
+ );
205
+ print(task, args, (value) => `Resolved ${value.id}: ${value.threadId}`);
206
+ return 0;
207
+ }
208
+
183
209
  async function taskShow(args) {
184
210
  validateCommandArgs(args, 3, { values: ["--workspace"], switches: ["--json"] });
185
211
  print(await readTask(workspaceRoot(args), args[2]), args);
@@ -224,11 +250,12 @@ Usage:
224
250
  taskchef help
225
251
  taskchef doctor [--json] [--workspace <path>]
226
252
  taskchef workspace init [--json] [--workspace <path>]
227
- taskchef project add <path> [--name <name>] [--description <text>] [--github-repo <url> | --no-github] [--json] [--workspace <path>]
253
+ taskchef project add <path> [--name <name>] [--description <text>] [--github-repo <url> ... | --no-github] [--json] [--workspace <path>]
228
254
  taskchef project import [<file> | -] [--replace] [--json] [--workspace <path>]
229
255
  taskchef project list [--json] [--workspace <path>]
230
256
  taskchef project remove <name> [--json] [--workspace <path>]
231
257
  taskchef task record [--json] [--workspace <path>]
258
+ taskchef task resolve <task-id> --thread-id <thread-id> [--json] [--workspace <path>]
232
259
  taskchef task show <task-id> [--json] [--workspace <path>]
233
260
  taskchef task list [--project <name-or-path>] [--json] [--workspace <path>]
234
261
  taskchef task summary [--json] [--workspace <path>]
@@ -250,6 +277,7 @@ export async function runCli(args) {
250
277
  if (args[0] === "project" && args[1] === "list") return projectList(args);
251
278
  if (args[0] === "project" && args[1] === "remove") return projectRemove(args);
252
279
  if (args[0] === "task" && args[1] === "record") return taskRecord(args);
280
+ if (args[0] === "task" && args[1] === "resolve") return taskResolve(args);
253
281
  if (args[0] === "task" && args[1] === "show" && args[2]) return taskShow(args);
254
282
  if (args[0] === "task" && args[1] === "list") return taskList(args);
255
283
  if (args[0] === "task" && args[1] === "summary") return taskSummary(args);
@@ -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
+ }