taskchef 5.1.0 → 5.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/SPEC.md CHANGED
@@ -196,10 +196,12 @@ For each assignment, `$taskchef-delegate`:
196
196
  marked entry with `threadId: null`, then prefers one native client-ID wait or
197
197
  resolution call with a 30-second timeout when Codex exposes one
198
198
  7. when no native operation is available, takes at most two recent-thread
199
- snapshots near 10 and 30 seconds after the provisional result, filters
200
- candidates by available host/project/time/worktree metadata, uses title only
201
- as an advisory ordering hint, and accepts only one thread whose structured
202
- delegated input starts with the exact marker
199
+ snapshots on a nominal 10- and 30-second schedule after the provisional
200
+ result; a late first checkpoint runs immediately, and the second starts at
201
+ least 20 seconds after the first actually started. It filters candidates by
202
+ available host/project/time/worktree metadata, uses title only as an advisory
203
+ ordering hint, and accepts only one thread whose structured delegated input
204
+ starts with the exact marker
203
205
  8. atomically fills the nullable thread ID after an exact match
204
206
  9. returns after recording or after reporting that bounded resolution was
205
207
  unresolved, without waiting for executor work completion.
@@ -209,11 +211,13 @@ Creation-time filtering allows five seconds of clock skew. Candidate reads run
209
211
  concurrently where the host permits and inspect only structured
210
212
  `codexDelegation.input`, never untrusted title, summary, preview, or plain-text
211
213
  marker echoes. A native resolver result is verified against the same structured
212
- marker before persistence. Zero exact matches time out unresolved; multiple
213
- exact matches are ambiguous. Snapshot, candidate-read, native-resolution, or
214
- task-resolution errors leave the already-recorded nullable entry intact. No
215
- snapshot, candidate read, marker verification, or task-resolution write starts
216
- after the 30-second deadline, so tool latency can reduce the number of attempts.
214
+ marker before persistence. Zero exact matches remain unresolved; multiple exact
215
+ matches are ambiguous. Snapshot, candidate-read, native-resolution, or
216
+ task-resolution errors leave the already-recorded nullable entry intact. The
217
+ fallback is bounded by two snapshots rather than an absolute wall-clock cutoff:
218
+ mandatory recording or tool latency can shift both attempts later, but cannot
219
+ erase an attempt or reduce the minimum 20-second interval between their start
220
+ times.
217
221
  A `clientThreadId`, `pendingWorktreeId`, or ID in the documented provisional
218
222
  `local:` namespace remains diagnostic context and is rejected from every path
219
223
  that could persist the canonical `threadId` field.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -82,10 +82,14 @@ for all deterministic workspace and task-record operations.
82
82
  ID, call it exactly once with a timeout of at most 30 seconds. Do not invent
83
83
  an operation or pass the provisional ID to tools that require `threadId`.
84
84
  - When no native operation is available, take at most two `list_threads`
85
- snapshots with limit 50, near 10 and 30 seconds after the provisional
86
- result. Count tool latency against the 30-second deadline. Do not start a
87
- snapshot, candidate read, marker verification, or task-resolution write
88
- after it.
85
+ snapshots with limit 50. The nominal schedule is 10 and 30 seconds after
86
+ the provisional result. Treat the first checkpoint as due, not expired: if
87
+ nullable recording or other required work finishes after 10 seconds, take
88
+ the first snapshot immediately. Start the second snapshot no sooner than
89
+ 20 seconds after the first snapshot actually started. Useful candidate
90
+ work counts toward that interval; when it finishes sooner, wait only the
91
+ remainder. Never suppress either snapshot merely because an earlier step
92
+ ran late, and never take a third snapshot.
89
93
  - Filter Codex candidates by the expected host, project, creation time
90
94
  (allow five seconds of clock skew), and worktree environment whenever
91
95
  those fields are present. Use the title only to prioritize reads; Codex
package/src/delegation.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
- export const THREAD_RESOLUTION_CHECKPOINTS_MS = Object.freeze([10_000, 29_000]);
3
+ export const THREAD_RESOLUTION_CHECKPOINTS_MS = Object.freeze([10_000, 30_000]);
4
4
  export const THREAD_RESOLUTION_TIMEOUT_MS = 30_000;
5
5
  export const THREAD_RESOLUTION_RECENT_LIMIT = 50;
6
6
  export const THREAD_RESOLUTION_CLOCK_SKEW_MS = 5_000;
@@ -179,6 +179,9 @@ function validateResolutionSchedule(checkpointsMs, timeoutMs) {
179
179
  if (!Array.isArray(checkpointsMs) || checkpointsMs.length === 0) {
180
180
  throw new Error("checkpointsMs must contain at least one checkpoint");
181
181
  }
182
+ if (checkpointsMs.length > 2) {
183
+ throw new Error("checkpointsMs must contain at most two checkpoints");
184
+ }
182
185
  if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
183
186
  throw new Error("timeoutMs must be positive");
184
187
  }
@@ -189,6 +192,9 @@ function validateResolutionSchedule(checkpointsMs, timeoutMs) {
189
192
  }
190
193
  previous = checkpoint;
191
194
  }
195
+ if (checkpointsMs.length === 2 && checkpointsMs[1] - checkpointsMs[0] < 20_000) {
196
+ throw new Error("checkpointsMs must keep two checkpoints at least 20000ms apart");
197
+ }
192
198
  }
193
199
 
194
200
  async function inspectCandidates(candidates, {
@@ -365,7 +371,11 @@ export async function createAndRecordDelegation({
365
371
  return false;
366
372
  };
367
373
 
368
- const acceptResolvedThread = async (thread, resolution, attempt, { verified = false } = {}) => {
374
+ const acceptResolvedThread = async (thread, resolution, attempt, {
375
+ verified = false,
376
+ enforceDeadline = true,
377
+ } = {}) => {
378
+ const canContinue = enforceDeadline ? hasResolutionTime : () => true;
369
379
  const threadId = toolIdentifier(thread.id);
370
380
  if (
371
381
  threadId === null
@@ -381,18 +391,18 @@ export async function createAndRecordDelegation({
381
391
  return null;
382
392
  }
383
393
  const durableThread = { ...thread, id: threadId };
384
- if (!hasResolutionTime()) return null;
394
+ if (!canContinue()) return null;
385
395
  if (!verified) {
386
396
  const inspected = await inspectCandidates([durableThread], {
387
397
  readThread,
388
398
  taskId: prepared.id,
389
399
  attempt,
390
- canVerify: hasResolutionTime,
400
+ canVerify: canContinue,
391
401
  });
392
402
  discoveryErrors.push(...inspected.errors);
393
403
  if (inspected.exactMatches.length !== 1 || inspected.errors.length > 0) return null;
394
404
  }
395
- if (!hasResolutionTime()) return null;
405
+ if (!canContinue()) return null;
396
406
  try {
397
407
  await resolveRecordedTask({ id: prepared.id, threadId });
398
408
  } catch (error) {
@@ -470,12 +480,14 @@ export async function createAndRecordDelegation({
470
480
  let exactMatches = [];
471
481
  let ambiguousMatches = [];
472
482
  let attemptsMade = 0;
483
+ let previousAttemptStartedAt = resolutionStartedAt;
473
484
  for (let index = 0; index < checkpointsMs.length; index += 1) {
474
485
  const attempt = index + 1;
475
- const checkpointMs = checkpointsMs[index];
476
- const elapsedBeforeAttempt = now() - resolutionStartedAt;
477
- if (elapsedBeforeAttempt > timeoutMs) break;
478
- const remainingDelayMs = checkpointMs - elapsedBeforeAttempt;
486
+ const intervalMs = index === 0
487
+ ? checkpointsMs[0]
488
+ : checkpointsMs[index] - checkpointsMs[index - 1];
489
+ const dueAt = previousAttemptStartedAt + intervalMs;
490
+ const remainingDelayMs = dueAt - now();
479
491
  if (remainingDelayMs > 0) {
480
492
  try {
481
493
  await waitImpl(remainingDelayMs);
@@ -488,43 +500,36 @@ export async function createAndRecordDelegation({
488
500
  break;
489
501
  }
490
502
  }
491
- if (!hasResolutionTime()) break;
503
+ previousAttemptStartedAt = now();
492
504
  attemptsMade = attempt;
493
505
  let candidates = [];
494
506
  let attemptFailed = false;
495
507
  try {
496
508
  const snapshot = await listThreads({ limit: recentLimit });
497
- if (hasResolutionTime()) {
498
- candidates = filterThreadCandidates(snapshot, {
499
- excludedThreadIds: provisionalIds,
500
- hostId: expected.hostId ?? null,
501
- projectId: expected.projectId ?? target.projectId ?? null,
502
- title,
503
- createdAfter,
504
- environmentType: expected.environmentType ?? target.environment?.type ?? null,
505
- });
506
- }
509
+ candidates = filterThreadCandidates(snapshot, {
510
+ excludedThreadIds: provisionalIds,
511
+ hostId: expected.hostId ?? null,
512
+ projectId: expected.projectId ?? target.projectId ?? null,
513
+ title,
514
+ createdAfter,
515
+ environmentType: expected.environmentType ?? target.environment?.type ?? null,
516
+ });
507
517
  } catch (error) {
508
- if (hasResolutionTime()) {
509
- attemptFailed = true;
510
- discoveryErrors.push({
511
- attempt,
512
- operation: "listThreads",
513
- message: error instanceof Error ? error.message : String(error),
514
- });
515
- }
518
+ attemptFailed = true;
519
+ discoveryErrors.push({
520
+ attempt,
521
+ operation: "listThreads",
522
+ message: error instanceof Error ? error.message : String(error),
523
+ });
516
524
  }
517
- if (!hasResolutionTime()) break;
518
525
  const inspected = await inspectCandidates(candidates, {
519
526
  readThread,
520
527
  taskId: prepared.id,
521
528
  attempt,
522
- canVerify: hasResolutionTime,
523
529
  });
524
530
  exactMatches = inspected.exactMatches;
525
531
  discoveryErrors.push(...inspected.errors);
526
532
  if (inspected.errors.length > 0) attemptFailed = true;
527
- if (!hasResolutionTime()) break;
528
533
  if (exactMatches.length > 1) ambiguousMatches = exactMatches;
529
534
  if (
530
535
  exactMatches.length === 1
@@ -536,7 +541,7 @@ export async function createAndRecordDelegation({
536
541
  exactMatches[0],
537
542
  "discovered",
538
543
  attempt,
539
- { verified: true },
544
+ { verified: true, enforceDeadline: false },
540
545
  );
541
546
  if (resolved !== null) return resolved;
542
547
  }