@pentoshi/clai 3.8.50 → 3.8.51

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.
Files changed (55) hide show
  1. package/dist/agent/runner.js +1 -1
  2. package/dist/agent/runner.js.map +1 -1
  3. package/dist/app/adapters/current-jobs-adapter.js +4 -0
  4. package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
  5. package/dist/app/controllers/job-controller.d.ts +4 -0
  6. package/dist/app/controllers/job-controller.js +12 -0
  7. package/dist/app/controllers/job-controller.js.map +1 -1
  8. package/dist/app/controllers/plan-controller.d.ts +1 -0
  9. package/dist/app/controllers/plan-controller.js +15 -0
  10. package/dist/app/controllers/plan-controller.js.map +1 -1
  11. package/dist/app/controllers/session-controller.d.ts +2 -0
  12. package/dist/app/controllers/session-controller.js +32 -10
  13. package/dist/app/controllers/session-controller.js.map +1 -1
  14. package/dist/app/controllers/session-responder.d.ts +19 -23
  15. package/dist/app/controllers/session-responder.js +157 -262
  16. package/dist/app/controllers/session-responder.js.map +1 -1
  17. package/dist/app/ports/jobs-port.d.ts +4 -0
  18. package/dist/index.js +3 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/repl.js +14 -68
  21. package/dist/repl.js.map +1 -1
  22. package/dist/store/history-index.d.ts +43 -0
  23. package/dist/store/history-index.js +197 -0
  24. package/dist/store/history-index.js.map +1 -0
  25. package/dist/store/history.d.ts +6 -5
  26. package/dist/store/history.js +246 -205
  27. package/dist/store/history.js.map +1 -1
  28. package/dist/store/plan.d.ts +1 -1
  29. package/dist/store/plan.js +17 -8
  30. package/dist/store/plan.js.map +1 -1
  31. package/dist/store/responder-settlement.d.ts +3 -0
  32. package/dist/store/responder-settlement.js +60 -0
  33. package/dist/store/responder-settlement.js.map +1 -0
  34. package/dist/tools/definitions.js +16 -20
  35. package/dist/tools/definitions.js.map +1 -1
  36. package/dist/tools/jobs.d.ts +18 -6
  37. package/dist/tools/jobs.js +195 -41
  38. package/dist/tools/jobs.js.map +1 -1
  39. package/dist/tools/registry.js +4 -16
  40. package/dist/tools/registry.js.map +1 -1
  41. package/dist/tui-v2/app/commands/picker-commands.js +9 -10
  42. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
  43. package/dist/tui-v2/bootstrap/composition-root.js +9 -0
  44. package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
  45. package/dist/tui-v2/components/jobs/jobs-panel.js +49 -26
  46. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
  47. package/dist/tui-v2/components/status/status-line.d.ts +2 -0
  48. package/dist/tui-v2/components/status/status-line.js +23 -2
  49. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  50. package/dist/tui-v2/state/transcript-hydrate.d.ts +8 -2
  51. package/dist/tui-v2/state/transcript-hydrate.js +99 -9
  52. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
  53. package/dist/version.generated.d.ts +2 -2
  54. package/dist/version.generated.js +2 -2
  55. package/package.json +1 -1
@@ -16,6 +16,8 @@ export interface JobLinkMetadata {
16
16
  parentTaskId?: string | undefined;
17
17
  wakeOnCompletion?: boolean | undefined;
18
18
  monitor?: JobMonitorMetadata | undefined;
19
+ /** Runtime listening lease that authorized this responder delegation. */
20
+ responderLeaseId?: string | undefined;
19
21
  /**
20
22
  * Opt-in delegation to the Responder: fire-and-continue, plan subtask +
21
23
  * auto-wake on completion, and inclusion in the Responder inbox/UI. When
@@ -59,7 +61,7 @@ export interface BackgroundJob extends JobLinkMetadata {
59
61
  target: string;
60
62
  expiresAt?: string | undefined;
61
63
  } | undefined;
62
- /** Optional execution deadline for finite durable jobs. */
64
+ /** Accepted when reading legacy registries but never armed or restored. */
63
65
  timeoutAt?: string | undefined;
64
66
  }
65
67
  export declare function formatJobElapsed(job: Pick<BackgroundJob, "startedAt" | "endedAt">, now?: number): string;
@@ -81,9 +83,12 @@ export interface ResponderNotification {
81
83
  wakeOnCompletion: boolean;
82
84
  responder: boolean;
83
85
  monitor?: JobMonitorMetadata | undefined;
86
+ responderLeaseId?: string | undefined;
84
87
  deliveredAt?: string | undefined;
85
88
  analyzedAt?: string | undefined;
86
89
  acknowledgedAt?: string | undefined;
90
+ archivedAt?: string | undefined;
91
+ settledAt?: string | undefined;
87
92
  }
88
93
  export type JobManagerChange = {
89
94
  type: "job";
@@ -113,7 +118,7 @@ export interface StartJobOptions extends JobLinkMetadata {
113
118
  ownerSessionId?: string | undefined;
114
119
  profile?: string | undefined;
115
120
  estimatedSeconds?: number | undefined;
116
- /** Stop this finite job after the selected duration. */
121
+ /** Legacy compatibility input. Durable jobs no longer have generic deadlines. */
117
122
  timeoutMs?: number | undefined;
118
123
  authorization?: {
119
124
  target: string;
@@ -127,7 +132,10 @@ export declare class JobManager {
127
132
  private processes;
128
133
  private writers;
129
134
  private abortControllers;
130
- private deadlineTimers;
135
+ private authorizationTimers;
136
+ private responderLeases;
137
+ private settlementTimers;
138
+ private settlementAttempts;
131
139
  private finalizations;
132
140
  private listeners;
133
141
  /**
@@ -147,7 +155,11 @@ export declare class JobManager {
147
155
  private emit;
148
156
  subscribe(listener: JobManagerListener): () => void;
149
157
  private matchesSession;
150
- private clearJobDeadline;
158
+ private clearAuthorizationTimer;
159
+ private scheduleAuthorizationExpiry;
160
+ activateResponderLease(sessionId: string): string;
161
+ getResponderLeaseId(sessionId: string | undefined): string | undefined;
162
+ releaseResponderLease(sessionId: string, leaseId?: string): void;
151
163
  private cloneReceipt;
152
164
  private notificationForJob;
153
165
  private ensureCompletionNotification;
@@ -155,8 +167,7 @@ export declare class JobManager {
155
167
  private finalizeJob;
156
168
  /** Reconcile a restored process that no longer has a ChildProcess close event. */
157
169
  private refreshJobLiveness;
158
- /** Arm or restore a persisted finite-job deadline. */
159
- private scheduleJobDeadline;
170
+ private scheduleTaskSettlement;
160
171
  /**
161
172
  * Register an in-flight tool for stall tracking only.
162
173
  * Never appears in shell.jobs and never touches the durable registry.
@@ -173,6 +184,7 @@ export declare class JobManager {
173
184
  getJob(id: string): BackgroundJob | undefined;
174
185
  getPendingNotifications(sessionId?: string): ResponderNotification[];
175
186
  pendingNotifications(sessionId?: string): ResponderNotification[];
187
+ claimNextResponderNotification(sessionId: string, leaseId: string): ResponderNotification | undefined;
176
188
  markDelivered(notificationId: string): boolean;
177
189
  markAnalyzed(notificationId: string): boolean;
178
190
  acknowledge(notificationId: string): boolean;
@@ -40,6 +40,8 @@ const TRANSIENT_V2_REGISTRY_FILE = "registry-v2.json";
40
40
  const MAX_DURABLE_TERMINAL_JOBS = 80;
41
41
  /** Drop terminal durable jobs older than this on load/list. */
42
42
  const TERMINAL_JOB_MAX_AGE_MS = 48 * 60 * 60 * 1000;
43
+ const ARCHIVED_UNSETTLED_MAX_AGE_MS = 24 * 60 * 60 * 1000;
44
+ const MAX_ARCHIVED_UNSETTLED_NOTIFICATIONS = 40;
43
45
  /** Max lines shell.jobs returns to the model (running first, then recent). */
44
46
  const LIST_JOBS_MAX_LINES = 40;
45
47
  /** Coalesce window for chatty stdout/stderr progress persistence + UI events. */
@@ -241,7 +243,10 @@ export class JobManager {
241
243
  processes = new Map();
242
244
  writers = new Map();
243
245
  abortControllers = new Map();
244
- deadlineTimers = new Map();
246
+ authorizationTimers = new Map();
247
+ responderLeases = new Map();
248
+ settlementTimers = new Map();
249
+ settlementAttempts = new Map();
245
250
  finalizations = new Map();
246
251
  listeners = new Set();
247
252
  /**
@@ -288,11 +293,60 @@ export class JobManager {
288
293
  return true;
289
294
  return job.ownerSessionId === sessionId;
290
295
  }
291
- clearJobDeadline(id) {
292
- const timer = this.deadlineTimers.get(id);
296
+ clearAuthorizationTimer(id) {
297
+ const timer = this.authorizationTimers.get(id);
293
298
  if (timer)
294
299
  clearTimeout(timer);
295
- this.deadlineTimers.delete(id);
300
+ this.authorizationTimers.delete(id);
301
+ }
302
+ scheduleAuthorizationExpiry(job) {
303
+ this.clearAuthorizationTimer(job.id);
304
+ const raw = job.authorization?.expiresAt;
305
+ if (!raw || !this.isLive(job))
306
+ return;
307
+ const expiresAt = Date.parse(raw);
308
+ if (!Number.isFinite(expiresAt))
309
+ return;
310
+ const timer = setTimeout(() => {
311
+ this.authorizationTimers.delete(job.id);
312
+ this.refreshJobLiveness(job);
313
+ if (this.isLive(job))
314
+ void this.stopJob(job.id, { graceMs: 1_000 });
315
+ }, Math.max(0, expiresAt - Date.now()));
316
+ timer.unref?.();
317
+ this.authorizationTimers.set(job.id, timer);
318
+ }
319
+ activateResponderLease(sessionId) {
320
+ const current = this.responderLeases.get(sessionId);
321
+ if (current)
322
+ return current;
323
+ const leaseId = randomUUID();
324
+ this.responderLeases.set(sessionId, leaseId);
325
+ this.emit({ type: "job", jobId: `responder:${sessionId}` });
326
+ return leaseId;
327
+ }
328
+ getResponderLeaseId(sessionId) {
329
+ return sessionId ? this.responderLeases.get(sessionId) : undefined;
330
+ }
331
+ releaseResponderLease(sessionId, leaseId) {
332
+ const current = this.responderLeases.get(sessionId);
333
+ if (!current || (leaseId && current !== leaseId))
334
+ return;
335
+ this.responderLeases.delete(sessionId);
336
+ const archivedAt = new Date().toISOString();
337
+ let changed = false;
338
+ for (const notification of this.notifications.values()) {
339
+ if (notification.ownerSessionId === sessionId &&
340
+ notification.responderLeaseId === current &&
341
+ !notification.acknowledgedAt &&
342
+ !notification.archivedAt) {
343
+ notification.archivedAt = archivedAt;
344
+ changed = true;
345
+ }
346
+ }
347
+ if (changed && !this.persistSync())
348
+ this.scheduleRegistryRetry();
349
+ this.emit({ type: "job", jobId: `responder:${sessionId}` });
296
350
  }
297
351
  cloneReceipt(receipt) {
298
352
  return { ...receipt, chunks: [...receipt.chunks] };
@@ -320,6 +374,9 @@ export class JobManager {
320
374
  JSON.stringify(existing.stderrArtifact) !==
321
375
  JSON.stringify(stderrArtifact)));
322
376
  const reopenSettlement = Boolean(existing?.acknowledgedAt && completionChanged);
377
+ const responderLeaseId = job.responderLeaseId ?? existing?.responderLeaseId;
378
+ const leaseIsActive = Boolean(responderLeaseId &&
379
+ this.responderLeases.get(job.ownerSessionId) === responderLeaseId);
323
380
  const notification = {
324
381
  id: existing?.id ??
325
382
  `completion:${createHash("sha256").update(`${job.ownerSessionId}\0${job.id}\0${job.startedAt}`).digest("hex").slice(0, 24)}`,
@@ -339,6 +396,7 @@ export class JobManager {
339
396
  ...(job.exitCode !== undefined ? { exitCode: job.exitCode } : {}),
340
397
  ...(job.signal !== undefined ? { signal: job.signal } : {}),
341
398
  ...(job.monitor !== undefined ? { monitor: job.monitor } : {}),
399
+ ...(responderLeaseId ? { responderLeaseId } : {}),
342
400
  ...(existing?.deliveredAt ? { deliveredAt: existing.deliveredAt } : {}),
343
401
  ...((existing?.analyzedAt ?? existing?.acknowledgedAt)
344
402
  ? { analyzedAt: existing?.analyzedAt ?? existing?.acknowledgedAt }
@@ -346,6 +404,12 @@ export class JobManager {
346
404
  ...(!reopenSettlement && existing?.acknowledgedAt
347
405
  ? { acknowledgedAt: existing.acknowledgedAt }
348
406
  : {}),
407
+ ...(existing?.settledAt ? { settledAt: existing.settledAt } : {}),
408
+ ...(!leaseIsActive
409
+ ? { archivedAt: existing?.archivedAt ?? endedAt }
410
+ : existing?.archivedAt
411
+ ? { archivedAt: existing.archivedAt }
412
+ : {}),
349
413
  };
350
414
  const updated = Boolean(existing && JSON.stringify(existing) !== JSON.stringify(notification));
351
415
  this.notifications.set(notification.id, notification);
@@ -366,6 +430,7 @@ export class JobManager {
366
430
  const correctsLost = job.status === "lost" && status !== "lost";
367
431
  if (this.isTerminalStatus(job.status) && !correctsLost) {
368
432
  const completion = this.ensureCompletionNotification(job);
433
+ this.scheduleTaskSettlement(job);
369
434
  if ((!completion.created && !completion.updated) ||
370
435
  !completion.notification) {
371
436
  return true;
@@ -382,7 +447,7 @@ export class JobManager {
382
447
  return persisted;
383
448
  }
384
449
  const finalization = (async () => {
385
- this.clearJobDeadline(job.id);
450
+ this.clearAuthorizationTimer(job.id);
386
451
  this.livenessMisses.delete(job.id);
387
452
  const streamsFlushed = await this.closeWriters(job.id);
388
453
  job.status = streamsFlushed ? status : "failed";
@@ -397,6 +462,7 @@ export class JobManager {
397
462
  this.abortControllers.delete(job.id);
398
463
  this.processes.delete(job.id);
399
464
  const completion = this.ensureCompletionNotification(job);
465
+ this.scheduleTaskSettlement(job);
400
466
  this.pruneTerminalJobs();
401
467
  const persisted = this.persistSync();
402
468
  if (!persisted)
@@ -453,8 +519,9 @@ export class JobManager {
453
519
  this.livenessMisses.delete(job.id);
454
520
  job.status = "lost";
455
521
  job.endedAt = new Date().toISOString();
456
- this.clearJobDeadline(job.id);
522
+ this.clearAuthorizationTimer(job.id);
457
523
  const completion = this.ensureCompletionNotification(job);
524
+ this.scheduleTaskSettlement(job);
458
525
  this.pruneTerminalJobs();
459
526
  if (!this.persistSync())
460
527
  this.scheduleRegistryRetry();
@@ -468,22 +535,54 @@ export class JobManager {
468
535
  });
469
536
  }
470
537
  }
471
- /** Arm or restore a persisted finite-job deadline. */
472
- scheduleJobDeadline(job) {
473
- this.clearJobDeadline(job.id);
474
- if (!job.timeoutAt || !this.isLive(job))
475
- return;
476
- const timeoutAt = Date.parse(job.timeoutAt);
477
- if (!Number.isFinite(timeoutAt))
538
+ scheduleTaskSettlement(job, delayMs = 0) {
539
+ if (!job.responder || !this.isTerminalStatus(job.status))
478
540
  return;
541
+ const existing = this.settlementTimers.get(job.id);
542
+ if (existing)
543
+ clearTimeout(existing);
479
544
  const timer = setTimeout(() => {
480
- this.deadlineTimers.delete(job.id);
481
- this.refreshJobLiveness(job);
482
- if (this.isLive(job))
483
- void this.stopJob(job.id, { graceMs: 1_000 });
484
- }, Math.max(0, timeoutAt - Date.now()));
545
+ this.settlementTimers.delete(job.id);
546
+ void import("../store/responder-settlement.js")
547
+ .then(({ settleResponderJob }) => settleResponderJob({ ...job }))
548
+ .then((result) => {
549
+ if (result === "applied" || result === "noop") {
550
+ this.settlementAttempts.delete(job.id);
551
+ const notification = this.notificationForJob(job.id);
552
+ if (notification && !notification.settledAt) {
553
+ notification.settledAt = new Date().toISOString();
554
+ if (!this.persistSync())
555
+ this.scheduleRegistryRetry();
556
+ this.emit({
557
+ type: "notification",
558
+ jobId: job.id,
559
+ notificationId: notification.id,
560
+ });
561
+ }
562
+ return;
563
+ }
564
+ const attempt = (this.settlementAttempts.get(job.id) ?? 0) + 1;
565
+ this.settlementAttempts.set(job.id, attempt);
566
+ if (attempt < 8) {
567
+ this.scheduleTaskSettlement(job, Math.min(5_000, 50 * 2 ** attempt));
568
+ }
569
+ else {
570
+ this.settlementAttempts.delete(job.id);
571
+ }
572
+ })
573
+ .catch(() => {
574
+ const attempt = (this.settlementAttempts.get(job.id) ?? 0) + 1;
575
+ this.settlementAttempts.set(job.id, attempt);
576
+ if (attempt < 8) {
577
+ this.scheduleTaskSettlement(job, Math.min(5_000, 50 * 2 ** attempt));
578
+ }
579
+ else {
580
+ this.settlementAttempts.delete(job.id);
581
+ }
582
+ });
583
+ }, delayMs);
485
584
  timer.unref?.();
486
- this.deadlineTimers.set(job.id, timer);
585
+ this.settlementTimers.set(job.id, timer);
487
586
  }
488
587
  /**
489
588
  * Register an in-flight tool for stall tracking only.
@@ -516,7 +615,7 @@ export class JobManager {
516
615
  this.abortControllers.delete(id);
517
616
  this.processes.delete(id);
518
617
  if (!this.isLive(job))
519
- this.clearJobDeadline(id);
618
+ this.clearAuthorizationTimer(id);
520
619
  if (!this.isDurable(job) && !this.isLive(job)) {
521
620
  this.jobs.delete(id);
522
621
  this.emit({ type: "job", jobId: id });
@@ -598,10 +697,8 @@ export class JobManager {
598
697
  ...(options?.wakeOnCompletion !== undefined ? { wakeOnCompletion: options.wakeOnCompletion } : {}),
599
698
  ...(options?.responder !== undefined ? { responder: options.responder } : {}),
600
699
  ...(monitor !== undefined ? { monitor } : {}),
601
- ...(options?.timeoutMs !== undefined
602
- ? {
603
- timeoutAt: new Date(Date.now() + Math.max(1_000, Math.floor(options.timeoutMs))).toISOString(),
604
- }
700
+ ...(options?.responderLeaseId
701
+ ? { responderLeaseId: options.responderLeaseId }
605
702
  : {}),
606
703
  ...(options?.authorization ? { authorization: options.authorization } : {}),
607
704
  };
@@ -710,12 +807,7 @@ export class JobManager {
710
807
  job.processIdentity = processIdentity(child.pid);
711
808
  this.processes.set(id, child);
712
809
  this.writers.set(id, { stdout, stderr });
713
- const expiresAt = options?.authorization?.expiresAt ? Date.parse(options.authorization.expiresAt) : Number.NaN;
714
- const expiryTimer = Number.isFinite(expiresAt) && expiresAt > Date.now()
715
- ? setTimeout(() => { void this.stopJob(id, { graceMs: 1_000 }); }, expiresAt - Date.now())
716
- : undefined;
717
- expiryTimer?.unref?.();
718
- this.scheduleJobDeadline(job);
810
+ this.scheduleAuthorizationExpiry(job);
719
811
  let lastProgressFlush = 0;
720
812
  let progressDirty = false;
721
813
  let progressTimer;
@@ -765,8 +857,6 @@ export class JobManager {
765
857
  child.stdout?.on("data", (chunk) => recordOutput(stdout, chunk));
766
858
  child.stderr?.on("data", (chunk) => recordOutput(stderr, chunk));
767
859
  child.on("close", (code, signal) => {
768
- if (expiryTimer)
769
- clearTimeout(expiryTimer);
770
860
  stopProgressFlush();
771
861
  const status = signal ? "killed" : code === 0 ? "exited" : "failed";
772
862
  void this.finalizeJob(job, status, {
@@ -775,8 +865,6 @@ export class JobManager {
775
865
  });
776
866
  });
777
867
  child.on("error", (error) => {
778
- if (expiryTimer)
779
- clearTimeout(expiryTimer);
780
868
  stopProgressFlush();
781
869
  const code = error.code ?? "UNKNOWN";
782
870
  try {
@@ -966,6 +1054,47 @@ export class JobManager {
966
1054
  pendingNotifications(sessionId) {
967
1055
  return this.getPendingNotifications(sessionId);
968
1056
  }
1057
+ claimNextResponderNotification(sessionId, leaseId) {
1058
+ if (this.responderLeases.get(sessionId) !== leaseId)
1059
+ return undefined;
1060
+ const archivedAt = new Date().toISOString();
1061
+ let archived = false;
1062
+ for (const notification of this.notifications.values()) {
1063
+ if (notification.ownerSessionId === sessionId &&
1064
+ !notification.acknowledgedAt &&
1065
+ notification.responderLeaseId !== leaseId &&
1066
+ !notification.archivedAt) {
1067
+ notification.archivedAt = archivedAt;
1068
+ archived = true;
1069
+ }
1070
+ }
1071
+ const notification = [...this.notifications.values()]
1072
+ .filter((candidate) => candidate.ownerSessionId === sessionId &&
1073
+ candidate.responderLeaseId === leaseId &&
1074
+ !candidate.archivedAt &&
1075
+ !candidate.analyzedAt &&
1076
+ !candidate.acknowledgedAt)
1077
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt))[0];
1078
+ if (!notification) {
1079
+ if (archived && !this.persistSync())
1080
+ this.scheduleRegistryRetry();
1081
+ return undefined;
1082
+ }
1083
+ const previous = notification.deliveredAt;
1084
+ notification.deliveredAt ??= new Date().toISOString();
1085
+ if (!this.persistSync()) {
1086
+ notification.deliveredAt = previous;
1087
+ return undefined;
1088
+ }
1089
+ if (archived)
1090
+ this.persistSync();
1091
+ this.emit({
1092
+ type: "notification",
1093
+ jobId: notification.jobId,
1094
+ notificationId: notification.id,
1095
+ });
1096
+ return notification;
1097
+ }
969
1098
  markDelivered(notificationId) {
970
1099
  const notification = this.notifications.get(notificationId);
971
1100
  if (!notification)
@@ -1048,6 +1177,7 @@ export class JobManager {
1048
1177
  if (metadata.responder !== undefined)
1049
1178
  job.responder = metadata.responder;
1050
1179
  const completion = this.ensureCompletionNotification(job);
1180
+ this.scheduleTaskSettlement(job);
1051
1181
  if (completion.notification) {
1052
1182
  if (metadata.taskId !== undefined)
1053
1183
  completion.notification.taskId = metadata.taskId;
@@ -1208,7 +1338,7 @@ export class JobManager {
1208
1338
  const childClose = child
1209
1339
  ? new Promise((resolve) => child.once("close", () => resolve()))
1210
1340
  : undefined;
1211
- this.clearJobDeadline(id);
1341
+ this.clearAuthorizationTimer(id);
1212
1342
  const previousStatus = job.status;
1213
1343
  const previousWakeOnCompletion = job.wakeOnCompletion;
1214
1344
  if (options?.suppressWake)
@@ -1217,8 +1347,7 @@ export class JobManager {
1217
1347
  if (!this.persistSync()) {
1218
1348
  job.status = previousStatus;
1219
1349
  job.wakeOnCompletion = previousWakeOnCompletion;
1220
- if (!job.timeoutAt || Date.parse(job.timeoutAt) > Date.now())
1221
- this.scheduleJobDeadline(job);
1350
+ this.scheduleAuthorizationExpiry(job);
1222
1351
  return { ok: false, output: `Failed to persist stop state for job "${id}"; no signal was sent.`, exitCode: 1 };
1223
1352
  }
1224
1353
  this.emit({ type: "job", jobId: id });
@@ -1243,8 +1372,7 @@ export class JobManager {
1243
1372
  };
1244
1373
  const restoreRunning = () => {
1245
1374
  job.status = "running";
1246
- if (!job.timeoutAt || Date.parse(job.timeoutAt) > Date.now())
1247
- this.scheduleJobDeadline(job);
1375
+ this.scheduleAuthorizationExpiry(job);
1248
1376
  this.persistSync();
1249
1377
  this.emit({ type: "job", jobId: id });
1250
1378
  };
@@ -1414,6 +1542,24 @@ export class JobManager {
1414
1542
  /** Drop stale terminal durable jobs and all leftover ephemeral rows. */
1415
1543
  pruneTerminalJobs() {
1416
1544
  const now = Date.now();
1545
+ const archivedUnsettled = [...this.notifications.values()]
1546
+ .filter((notification) => Boolean(notification.archivedAt) &&
1547
+ !notification.acknowledgedAt &&
1548
+ !notification.settledAt)
1549
+ .sort((left, right) => (right.archivedAt ?? right.createdAt).localeCompare(left.archivedAt ?? left.createdAt));
1550
+ for (const [index, notification] of archivedUnsettled.entries()) {
1551
+ const archivedAt = Date.parse(notification.archivedAt ?? notification.endedAt ?? notification.createdAt);
1552
+ const expired = Number.isFinite(archivedAt) &&
1553
+ now - archivedAt > ARCHIVED_UNSETTLED_MAX_AGE_MS;
1554
+ if (index < MAX_ARCHIVED_UNSETTLED_NOTIFICATIONS && !expired)
1555
+ continue;
1556
+ this.notifications.delete(notification.id);
1557
+ const timer = this.settlementTimers.get(notification.jobId);
1558
+ if (timer)
1559
+ clearTimeout(timer);
1560
+ this.settlementTimers.delete(notification.jobId);
1561
+ this.settlementAttempts.delete(notification.jobId);
1562
+ }
1417
1563
  for (const [id, notification] of this.notifications) {
1418
1564
  const job = this.jobs.get(notification.jobId);
1419
1565
  if (!job ||
@@ -1432,8 +1578,11 @@ export class JobManager {
1432
1578
  if (this.isLive(job))
1433
1579
  continue;
1434
1580
  const notification = this.notificationForJob(job.id);
1435
- if (notification && !notification.acknowledgedAt)
1581
+ if (notification &&
1582
+ !notification.acknowledgedAt &&
1583
+ !(notification.archivedAt && notification.settledAt)) {
1436
1584
  continue;
1585
+ }
1437
1586
  const ended = job.endedAt ? Date.parse(job.endedAt) : Date.parse(job.startedAt);
1438
1587
  if (Number.isFinite(ended) && now - ended > TERMINAL_JOB_MAX_AGE_MS) {
1439
1588
  this.jobs.delete(id);
@@ -1473,6 +1622,9 @@ export class JobManager {
1473
1622
  if (notifiedJobs.has(notification.jobId))
1474
1623
  continue;
1475
1624
  notifiedJobs.add(notification.jobId);
1625
+ if (!notification.acknowledgedAt && !notification.archivedAt) {
1626
+ notification.archivedAt = new Date().toISOString();
1627
+ }
1476
1628
  this.notifications.set(notification.id, notification);
1477
1629
  }
1478
1630
  }
@@ -1497,9 +1649,11 @@ export class JobManager {
1497
1649
  job.heartbeatAt = new Date().toISOString();
1498
1650
  }
1499
1651
  }
1652
+ delete job.timeoutAt;
1500
1653
  this.jobs.set(job.id, job);
1501
1654
  this.ensureCompletionNotification(job);
1502
- this.scheduleJobDeadline(job);
1655
+ this.scheduleAuthorizationExpiry(job);
1656
+ this.scheduleTaskSettlement(job);
1503
1657
  }
1504
1658
  for (const [id, notification] of this.notifications) {
1505
1659
  const job = this.jobs.get(notification.jobId);