ptbk 0.119.0-67 → 0.119.0-70

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.
@@ -870,6 +870,8 @@ export declare class Agenda {
870
870
  * concurrent units; the loop never waits for one of them.
871
871
  */
872
872
  private iteration;
873
+ /** Waits for scheduled work or an internal retry after the current evaluation has finished. */
874
+ private waitForNextWork;
873
875
  /** Whether the last loop wait was skipped by the operator. */
874
876
  private waiterSkipped;
875
877
  /**
@@ -1031,15 +1031,19 @@ export class Agenda {
1031
1031
  }
1032
1032
  /** Tells waiting chats about finished work, once each, also after a restart. */
1033
1033
  async deliverTaskNotifications() {
1034
- const waiting = this.taskControlStore.waiting();
1034
+ // An active writer's source may be incomplete, or carry a completion stamp its check can still reject.
1035
+ const waiting = this.taskControlStore.waiting().filter((notification) => !this.ownership.ownerOf(notification.source));
1035
1036
  if (!waiting.length)
1036
1037
  return;
1037
1038
  const { tasks } = await this.readQueue();
1038
1039
  for (const notification of waiting) {
1040
+ // A run can acquire the source while the asynchronous queue scan is in progress.
1041
+ if (this.ownership.ownerOf(notification.source))
1042
+ continue;
1039
1043
  const task = tasks.find((candidate) => candidate.source.relativePath === notification.source);
1040
1044
  const result = this.recentResults.find((candidate) => nameKey(candidate.title) === nameKey(notification.task));
1041
1045
  const outcome = !task
1042
- ? 'its task no longer exists'
1046
+ ? 'no longer exists'
1043
1047
  : task.cycle > notification.cycle || task.status === 'done'
1044
1048
  ? `is done${result?.summary ? `: ${result.summary}` : ''}${result?.commit ? ` (result commit ${result.commit.slice(0, 8)})` : ''}`
1045
1049
  : task.status === 'cancelled'
@@ -2992,12 +2996,18 @@ export class Agenda {
2992
2996
  await this.runEngineUpdate();
2993
2997
  return;
2994
2998
  }
2999
+ await this.waitForNextWork(evaluated);
3000
+ }
3001
+ /** Waits for scheduled work or an internal retry after the current evaluation has finished. */
3002
+ async waitForNextWork(evaluated) {
3003
+ const waitingForTools = this.tools.waiting;
2995
3004
  const waiting = evaluated.filter((item) => item.readiness.kind === 'waiting-until').map((item) => item.readiness.until);
2996
- const earliest = (times) => times.filter((until) => until > Date.now()).reduce((first, until) => (first === undefined || until < first ? until : first), undefined);
2997
- const nextTime = earliest([...waiting, ...(this.nextKnownTime ? [this.nextKnownTime] : [])]);
3005
+ /** Internal deadlines can become due while evaluation runs; they still require a prompt next iteration. */
3006
+ const earliest = (times) => times.filter(Number.isFinite).reduce((first, until) => (first === undefined || until < first ? until : first), undefined);
3007
+ const nextTime = earliest([...waiting, ...(this.nextKnownTime ? [this.nextKnownTime] : [])].filter((until) => until > Date.now()));
2998
3008
  this.nextKnownTime = nextTime;
2999
3009
  // Internal retries (resumable runs, deferred claims, a failed brief's recheck) also wake the loop, without being
3000
- // presented as scheduled work.
3010
+ // presented as scheduled work. Overdue retries must not fall through to the ten-minute idle wait.
3001
3011
  const wakeAt = earliest([...(nextTime ? [nextTime] : []), ...this.resumeAfter.values(), ...this.claimDeferred.values(), ...this.recoveryDue.values(), ...(this.reportRecheck ? [Date.parse(this.reportRecheck.nextAt)] : []), ...(this.updater.wakeAt() ? [this.updater.wakeAt()] : [])]);
3002
3012
  const blocked = evaluated.filter((item) => ['blocked', 'needs-recovery', 'failed'].includes(item.readiness.kind));
3003
3013
  const reason = this.executing
@@ -3010,7 +3020,7 @@ export class Agenda {
3010
3020
  ? `No unfinished work. Waiting for tools: ${waitingForTools.reason}`
3011
3021
  : (this.idleReason ?? 'No unfinished work.');
3012
3022
  this.phase = 'idle';
3013
- await this.wait(wakeAt ? Math.min(Math.max(1_000, wakeAt - Date.now()), IDLE_RECHECK_MS) : IDLE_RECHECK_MS, { kind: nextTime ? 'schedule' : 'idle', reason, skippable: false });
3023
+ await this.wait(wakeAt !== undefined ? Math.min(Math.max(1_000, wakeAt - Date.now()), IDLE_RECHECK_MS) : IDLE_RECHECK_MS, { kind: nextTime ? 'schedule' : 'idle', reason, skippable: false });
3014
3024
  }
3015
3025
  /** Whether the last loop wait was skipped by the operator. */
3016
3026
  waiterSkipped = false;