@pentoshi/clai 3.8.50 → 3.9.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.
Files changed (93) hide show
  1. package/dist/agent/compaction-summary.js +1 -1
  2. package/dist/agent/compaction-summary.js.map +1 -1
  3. package/dist/agent/context-manager.js +26 -3
  4. package/dist/agent/context-manager.js.map +1 -1
  5. package/dist/agent/plan-tool.js +134 -42
  6. package/dist/agent/plan-tool.js.map +1 -1
  7. package/dist/agent/responder-context.d.ts +10 -0
  8. package/dist/agent/responder-context.js +37 -1
  9. package/dist/agent/responder-context.js.map +1 -1
  10. package/dist/agent/runner.d.ts +3 -0
  11. package/dist/agent/runner.js +57 -14
  12. package/dist/agent/runner.js.map +1 -1
  13. package/dist/agent/session-policy.js +1 -0
  14. package/dist/agent/session-policy.js.map +1 -1
  15. package/dist/agent/task-evidence.js +7 -2
  16. package/dist/agent/task-evidence.js.map +1 -1
  17. package/dist/agent/task-plan.d.ts +4 -0
  18. package/dist/agent/task-plan.js +8 -0
  19. package/dist/agent/task-plan.js.map +1 -1
  20. package/dist/agent/task-sync.d.ts +0 -4
  21. package/dist/agent/task-sync.js +6 -11
  22. package/dist/agent/task-sync.js.map +1 -1
  23. package/dist/agent/tool-call-parser.js +7 -1
  24. package/dist/agent/tool-call-parser.js.map +1 -1
  25. package/dist/app/adapters/current-jobs-adapter.js +5 -0
  26. package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
  27. package/dist/app/adapters/quiet-meta-tools.js +1 -0
  28. package/dist/app/adapters/quiet-meta-tools.js.map +1 -1
  29. package/dist/app/controllers/job-controller.d.ts +4 -0
  30. package/dist/app/controllers/job-controller.js +12 -0
  31. package/dist/app/controllers/job-controller.js.map +1 -1
  32. package/dist/app/controllers/plan-controller.d.ts +1 -0
  33. package/dist/app/controllers/plan-controller.js +15 -0
  34. package/dist/app/controllers/plan-controller.js.map +1 -1
  35. package/dist/app/controllers/responder-settlement.d.ts +8 -0
  36. package/dist/app/controllers/responder-settlement.js +20 -0
  37. package/dist/app/controllers/responder-settlement.js.map +1 -0
  38. package/dist/app/controllers/session-controller.d.ts +3 -12
  39. package/dist/app/controllers/session-controller.js +72 -31
  40. package/dist/app/controllers/session-controller.js.map +1 -1
  41. package/dist/app/controllers/session-responder.d.ts +22 -25
  42. package/dist/app/controllers/session-responder.js +177 -258
  43. package/dist/app/controllers/session-responder.js.map +1 -1
  44. package/dist/app/controllers/turn-controller.d.ts +1 -0
  45. package/dist/app/controllers/turn-controller.js +1 -0
  46. package/dist/app/controllers/turn-controller.js.map +1 -1
  47. package/dist/app/ports/jobs-port.d.ts +5 -0
  48. package/dist/index.js +3 -3
  49. package/dist/index.js.map +1 -1
  50. package/dist/prompts/embedded.js +1 -1
  51. package/dist/prompts/embedded.js.map +1 -1
  52. package/dist/prompts/system.agent.md +4 -3
  53. package/dist/repl.js +14 -68
  54. package/dist/repl.js.map +1 -1
  55. package/dist/store/history-index.d.ts +43 -0
  56. package/dist/store/history-index.js +197 -0
  57. package/dist/store/history-index.js.map +1 -0
  58. package/dist/store/history.d.ts +6 -5
  59. package/dist/store/history.js +246 -205
  60. package/dist/store/history.js.map +1 -1
  61. package/dist/store/plan.d.ts +1 -1
  62. package/dist/store/plan.js +17 -8
  63. package/dist/store/plan.js.map +1 -1
  64. package/dist/store/responder-settlement.d.ts +3 -0
  65. package/dist/store/responder-settlement.js +60 -0
  66. package/dist/store/responder-settlement.js.map +1 -0
  67. package/dist/tools/definitions.js +40 -21
  68. package/dist/tools/definitions.js.map +1 -1
  69. package/dist/tools/jobs.d.ts +20 -6
  70. package/dist/tools/jobs.js +203 -52
  71. package/dist/tools/jobs.js.map +1 -1
  72. package/dist/tools/registry.js +5 -16
  73. package/dist/tools/registry.js.map +1 -1
  74. package/dist/tui-v2/app/commands/picker-commands.js +9 -10
  75. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
  76. package/dist/tui-v2/bootstrap/composition-root.d.ts +2 -1
  77. package/dist/tui-v2/bootstrap/composition-root.js +15 -3
  78. package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
  79. package/dist/tui-v2/components/jobs/jobs-panel.js +50 -27
  80. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
  81. package/dist/tui-v2/components/status/status-line.d.ts +2 -0
  82. package/dist/tui-v2/components/status/status-line.js +16 -0
  83. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  84. package/dist/tui-v2/state/transcript-hydrate.d.ts +8 -2
  85. package/dist/tui-v2/state/transcript-hydrate.js +99 -9
  86. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
  87. package/dist/tui-v2/state/transcript-struct.d.ts +0 -10
  88. package/dist/tui-v2/state/transcript-struct.js +4 -93
  89. package/dist/tui-v2/state/transcript-struct.js.map +1 -1
  90. package/dist/version.generated.d.ts +2 -2
  91. package/dist/version.generated.js +2 -2
  92. package/dist/version.generated.js.map +1 -1
  93. 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;
@@ -124,10 +129,14 @@ export declare class JobManager {
124
129
  private readonly jobsDir;
125
130
  private jobs;
126
131
  private notifications;
132
+ private readonly claimedNotifications;
127
133
  private processes;
128
134
  private writers;
129
135
  private abortControllers;
130
- private deadlineTimers;
136
+ private authorizationTimers;
137
+ private responderLeases;
138
+ private settlementTimers;
139
+ private settlementAttempts;
131
140
  private finalizations;
132
141
  private listeners;
133
142
  /**
@@ -147,7 +156,11 @@ export declare class JobManager {
147
156
  private emit;
148
157
  subscribe(listener: JobManagerListener): () => void;
149
158
  private matchesSession;
150
- private clearJobDeadline;
159
+ private clearAuthorizationTimer;
160
+ private scheduleAuthorizationExpiry;
161
+ activateResponderLease(sessionId: string): string;
162
+ getResponderLeaseId(sessionId: string | undefined): string | undefined;
163
+ releaseResponderLease(sessionId: string, leaseId?: string): void;
151
164
  private cloneReceipt;
152
165
  private notificationForJob;
153
166
  private ensureCompletionNotification;
@@ -155,8 +168,7 @@ export declare class JobManager {
155
168
  private finalizeJob;
156
169
  /** Reconcile a restored process that no longer has a ChildProcess close event. */
157
170
  private refreshJobLiveness;
158
- /** Arm or restore a persisted finite-job deadline. */
159
- private scheduleJobDeadline;
171
+ private scheduleTaskSettlement;
160
172
  /**
161
173
  * Register an in-flight tool for stall tracking only.
162
174
  * Never appears in shell.jobs and never touches the durable registry.
@@ -173,6 +185,8 @@ export declare class JobManager {
173
185
  getJob(id: string): BackgroundJob | undefined;
174
186
  getPendingNotifications(sessionId?: string): ResponderNotification[];
175
187
  pendingNotifications(sessionId?: string): ResponderNotification[];
188
+ claimNextResponderNotification(sessionId: string, leaseId: string): ResponderNotification | undefined;
189
+ releaseResponderNotificationClaim(notificationId: string): void;
176
190
  markDelivered(notificationId: string): boolean;
177
191
  markAnalyzed(notificationId: string): boolean;
178
192
  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. */
@@ -238,10 +240,14 @@ export class JobManager {
238
240
  jobsDir;
239
241
  jobs = new Map();
240
242
  notifications = new Map();
243
+ claimedNotifications = new Set();
241
244
  processes = new Map();
242
245
  writers = new Map();
243
246
  abortControllers = new Map();
244
- deadlineTimers = new Map();
247
+ authorizationTimers = new Map();
248
+ responderLeases = new Map();
249
+ settlementTimers = new Map();
250
+ settlementAttempts = new Map();
245
251
  finalizations = new Map();
246
252
  listeners = new Set();
247
253
  /**
@@ -288,11 +294,64 @@ export class JobManager {
288
294
  return true;
289
295
  return job.ownerSessionId === sessionId;
290
296
  }
291
- clearJobDeadline(id) {
292
- const timer = this.deadlineTimers.get(id);
297
+ clearAuthorizationTimer(id) {
298
+ const timer = this.authorizationTimers.get(id);
293
299
  if (timer)
294
300
  clearTimeout(timer);
295
- this.deadlineTimers.delete(id);
301
+ this.authorizationTimers.delete(id);
302
+ }
303
+ scheduleAuthorizationExpiry(job) {
304
+ this.clearAuthorizationTimer(job.id);
305
+ const raw = job.authorization?.expiresAt;
306
+ if (!raw || !this.isLive(job))
307
+ return;
308
+ const expiresAt = Date.parse(raw);
309
+ if (!Number.isFinite(expiresAt))
310
+ return;
311
+ const timer = setTimeout(() => {
312
+ this.authorizationTimers.delete(job.id);
313
+ this.refreshJobLiveness(job);
314
+ if (this.isLive(job))
315
+ void this.stopJob(job.id, { graceMs: 1_000 });
316
+ }, Math.max(0, expiresAt - Date.now()));
317
+ timer.unref?.();
318
+ this.authorizationTimers.set(job.id, timer);
319
+ }
320
+ activateResponderLease(sessionId) {
321
+ const current = this.responderLeases.get(sessionId);
322
+ if (current)
323
+ return current;
324
+ const leaseId = randomUUID();
325
+ this.responderLeases.set(sessionId, leaseId);
326
+ this.emit({ type: "job", jobId: `responder:${sessionId}` });
327
+ return leaseId;
328
+ }
329
+ getResponderLeaseId(sessionId) {
330
+ return sessionId ? this.responderLeases.get(sessionId) : undefined;
331
+ }
332
+ releaseResponderLease(sessionId, leaseId) {
333
+ const current = this.responderLeases.get(sessionId);
334
+ if (!current || (leaseId && current !== leaseId))
335
+ return;
336
+ this.responderLeases.delete(sessionId);
337
+ const archivedAt = new Date().toISOString();
338
+ let changed = false;
339
+ for (const notification of this.notifications.values()) {
340
+ if (notification.ownerSessionId === sessionId &&
341
+ notification.responderLeaseId === current) {
342
+ this.claimedNotifications.delete(notification.id);
343
+ }
344
+ if (notification.ownerSessionId === sessionId &&
345
+ notification.responderLeaseId === current &&
346
+ !notification.acknowledgedAt &&
347
+ !notification.archivedAt) {
348
+ notification.archivedAt = archivedAt;
349
+ changed = true;
350
+ }
351
+ }
352
+ if (changed && !this.persistSync())
353
+ this.scheduleRegistryRetry();
354
+ this.emit({ type: "job", jobId: `responder:${sessionId}` });
296
355
  }
297
356
  cloneReceipt(receipt) {
298
357
  return { ...receipt, chunks: [...receipt.chunks] };
@@ -320,6 +379,9 @@ export class JobManager {
320
379
  JSON.stringify(existing.stderrArtifact) !==
321
380
  JSON.stringify(stderrArtifact)));
322
381
  const reopenSettlement = Boolean(existing?.acknowledgedAt && completionChanged);
382
+ const responderLeaseId = job.responderLeaseId ?? existing?.responderLeaseId;
383
+ const leaseIsActive = Boolean(responderLeaseId &&
384
+ this.responderLeases.get(job.ownerSessionId) === responderLeaseId);
323
385
  const notification = {
324
386
  id: existing?.id ??
325
387
  `completion:${createHash("sha256").update(`${job.ownerSessionId}\0${job.id}\0${job.startedAt}`).digest("hex").slice(0, 24)}`,
@@ -339,6 +401,7 @@ export class JobManager {
339
401
  ...(job.exitCode !== undefined ? { exitCode: job.exitCode } : {}),
340
402
  ...(job.signal !== undefined ? { signal: job.signal } : {}),
341
403
  ...(job.monitor !== undefined ? { monitor: job.monitor } : {}),
404
+ ...(responderLeaseId ? { responderLeaseId } : {}),
342
405
  ...(existing?.deliveredAt ? { deliveredAt: existing.deliveredAt } : {}),
343
406
  ...((existing?.analyzedAt ?? existing?.acknowledgedAt)
344
407
  ? { analyzedAt: existing?.analyzedAt ?? existing?.acknowledgedAt }
@@ -346,6 +409,12 @@ export class JobManager {
346
409
  ...(!reopenSettlement && existing?.acknowledgedAt
347
410
  ? { acknowledgedAt: existing.acknowledgedAt }
348
411
  : {}),
412
+ ...(existing?.settledAt ? { settledAt: existing.settledAt } : {}),
413
+ ...(!leaseIsActive
414
+ ? { archivedAt: existing?.archivedAt ?? endedAt }
415
+ : existing?.archivedAt
416
+ ? { archivedAt: existing.archivedAt }
417
+ : {}),
349
418
  };
350
419
  const updated = Boolean(existing && JSON.stringify(existing) !== JSON.stringify(notification));
351
420
  this.notifications.set(notification.id, notification);
@@ -366,6 +435,7 @@ export class JobManager {
366
435
  const correctsLost = job.status === "lost" && status !== "lost";
367
436
  if (this.isTerminalStatus(job.status) && !correctsLost) {
368
437
  const completion = this.ensureCompletionNotification(job);
438
+ this.scheduleTaskSettlement(job);
369
439
  if ((!completion.created && !completion.updated) ||
370
440
  !completion.notification) {
371
441
  return true;
@@ -382,7 +452,7 @@ export class JobManager {
382
452
  return persisted;
383
453
  }
384
454
  const finalization = (async () => {
385
- this.clearJobDeadline(job.id);
455
+ this.clearAuthorizationTimer(job.id);
386
456
  this.livenessMisses.delete(job.id);
387
457
  const streamsFlushed = await this.closeWriters(job.id);
388
458
  job.status = streamsFlushed ? status : "failed";
@@ -397,6 +467,7 @@ export class JobManager {
397
467
  this.abortControllers.delete(job.id);
398
468
  this.processes.delete(job.id);
399
469
  const completion = this.ensureCompletionNotification(job);
470
+ this.scheduleTaskSettlement(job);
400
471
  this.pruneTerminalJobs();
401
472
  const persisted = this.persistSync();
402
473
  if (!persisted)
@@ -453,8 +524,9 @@ export class JobManager {
453
524
  this.livenessMisses.delete(job.id);
454
525
  job.status = "lost";
455
526
  job.endedAt = new Date().toISOString();
456
- this.clearJobDeadline(job.id);
527
+ this.clearAuthorizationTimer(job.id);
457
528
  const completion = this.ensureCompletionNotification(job);
529
+ this.scheduleTaskSettlement(job);
458
530
  this.pruneTerminalJobs();
459
531
  if (!this.persistSync())
460
532
  this.scheduleRegistryRetry();
@@ -468,22 +540,54 @@ export class JobManager {
468
540
  });
469
541
  }
470
542
  }
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))
543
+ scheduleTaskSettlement(job, delayMs = 0) {
544
+ if (!job.responder || !this.isTerminalStatus(job.status))
478
545
  return;
546
+ const existing = this.settlementTimers.get(job.id);
547
+ if (existing)
548
+ clearTimeout(existing);
479
549
  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()));
550
+ this.settlementTimers.delete(job.id);
551
+ void import("../store/responder-settlement.js")
552
+ .then(({ settleResponderJob }) => settleResponderJob({ ...job }))
553
+ .then((result) => {
554
+ if (result === "applied" || result === "noop") {
555
+ this.settlementAttempts.delete(job.id);
556
+ const notification = this.notificationForJob(job.id);
557
+ if (notification && !notification.settledAt) {
558
+ notification.settledAt = new Date().toISOString();
559
+ if (!this.persistSync())
560
+ this.scheduleRegistryRetry();
561
+ this.emit({
562
+ type: "notification",
563
+ jobId: job.id,
564
+ notificationId: notification.id,
565
+ });
566
+ }
567
+ return;
568
+ }
569
+ const attempt = (this.settlementAttempts.get(job.id) ?? 0) + 1;
570
+ this.settlementAttempts.set(job.id, attempt);
571
+ if (attempt < 8) {
572
+ this.scheduleTaskSettlement(job, Math.min(5_000, 50 * 2 ** attempt));
573
+ }
574
+ else {
575
+ this.settlementAttempts.delete(job.id);
576
+ }
577
+ })
578
+ .catch(() => {
579
+ const attempt = (this.settlementAttempts.get(job.id) ?? 0) + 1;
580
+ this.settlementAttempts.set(job.id, attempt);
581
+ if (attempt < 8) {
582
+ this.scheduleTaskSettlement(job, Math.min(5_000, 50 * 2 ** attempt));
583
+ }
584
+ else {
585
+ this.settlementAttempts.delete(job.id);
586
+ }
587
+ });
588
+ }, delayMs);
485
589
  timer.unref?.();
486
- this.deadlineTimers.set(job.id, timer);
590
+ this.settlementTimers.set(job.id, timer);
487
591
  }
488
592
  /**
489
593
  * Register an in-flight tool for stall tracking only.
@@ -516,7 +620,7 @@ export class JobManager {
516
620
  this.abortControllers.delete(id);
517
621
  this.processes.delete(id);
518
622
  if (!this.isLive(job))
519
- this.clearJobDeadline(id);
623
+ this.clearAuthorizationTimer(id);
520
624
  if (!this.isDurable(job) && !this.isLive(job)) {
521
625
  this.jobs.delete(id);
522
626
  this.emit({ type: "job", jobId: id });
@@ -598,10 +702,8 @@ export class JobManager {
598
702
  ...(options?.wakeOnCompletion !== undefined ? { wakeOnCompletion: options.wakeOnCompletion } : {}),
599
703
  ...(options?.responder !== undefined ? { responder: options.responder } : {}),
600
704
  ...(monitor !== undefined ? { monitor } : {}),
601
- ...(options?.timeoutMs !== undefined
602
- ? {
603
- timeoutAt: new Date(Date.now() + Math.max(1_000, Math.floor(options.timeoutMs))).toISOString(),
604
- }
705
+ ...(options?.responderLeaseId
706
+ ? { responderLeaseId: options.responderLeaseId }
605
707
  : {}),
606
708
  ...(options?.authorization ? { authorization: options.authorization } : {}),
607
709
  };
@@ -710,12 +812,7 @@ export class JobManager {
710
812
  job.processIdentity = processIdentity(child.pid);
711
813
  this.processes.set(id, child);
712
814
  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);
815
+ this.scheduleAuthorizationExpiry(job);
719
816
  let lastProgressFlush = 0;
720
817
  let progressDirty = false;
721
818
  let progressTimer;
@@ -765,8 +862,6 @@ export class JobManager {
765
862
  child.stdout?.on("data", (chunk) => recordOutput(stdout, chunk));
766
863
  child.stderr?.on("data", (chunk) => recordOutput(stderr, chunk));
767
864
  child.on("close", (code, signal) => {
768
- if (expiryTimer)
769
- clearTimeout(expiryTimer);
770
865
  stopProgressFlush();
771
866
  const status = signal ? "killed" : code === 0 ? "exited" : "failed";
772
867
  void this.finalizeJob(job, status, {
@@ -775,8 +870,6 @@ export class JobManager {
775
870
  });
776
871
  });
777
872
  child.on("error", (error) => {
778
- if (expiryTimer)
779
- clearTimeout(expiryTimer);
780
873
  stopProgressFlush();
781
874
  const code = error.code ?? "UNKNOWN";
782
875
  try {
@@ -966,31 +1059,62 @@ export class JobManager {
966
1059
  pendingNotifications(sessionId) {
967
1060
  return this.getPendingNotifications(sessionId);
968
1061
  }
1062
+ claimNextResponderNotification(sessionId, leaseId) {
1063
+ if (this.responderLeases.get(sessionId) !== leaseId)
1064
+ return undefined;
1065
+ const archivedAt = new Date().toISOString();
1066
+ let archived = false;
1067
+ for (const notification of this.notifications.values()) {
1068
+ if (notification.ownerSessionId === sessionId &&
1069
+ !notification.acknowledgedAt &&
1070
+ notification.responderLeaseId !== leaseId &&
1071
+ !notification.archivedAt) {
1072
+ notification.archivedAt = archivedAt;
1073
+ archived = true;
1074
+ }
1075
+ }
1076
+ const notification = [...this.notifications.values()]
1077
+ .filter((candidate) => candidate.ownerSessionId === sessionId &&
1078
+ candidate.responderLeaseId === leaseId &&
1079
+ !candidate.archivedAt &&
1080
+ !candidate.deliveredAt &&
1081
+ !candidate.analyzedAt &&
1082
+ !candidate.acknowledgedAt &&
1083
+ !this.claimedNotifications.has(candidate.id))
1084
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt))[0];
1085
+ if (notification)
1086
+ this.claimedNotifications.add(notification.id);
1087
+ if (archived && !this.persistSync())
1088
+ this.scheduleRegistryRetry();
1089
+ return notification;
1090
+ }
1091
+ releaseResponderNotificationClaim(notificationId) {
1092
+ this.claimedNotifications.delete(notificationId);
1093
+ }
969
1094
  markDelivered(notificationId) {
970
1095
  const notification = this.notifications.get(notificationId);
971
1096
  if (!notification)
972
1097
  return false;
973
1098
  if (!notification.deliveredAt) {
974
- const deliveredAt = new Date().toISOString();
975
- notification.deliveredAt = deliveredAt;
1099
+ notification.deliveredAt = new Date().toISOString();
976
1100
  if (!this.persistSync()) {
977
1101
  notification.deliveredAt = undefined;
978
1102
  return false;
979
1103
  }
980
1104
  this.emit({ type: "notification", jobId: notification.jobId, notificationId });
981
1105
  }
1106
+ this.claimedNotifications.delete(notificationId);
982
1107
  return true;
983
1108
  }
984
1109
  markAnalyzed(notificationId) {
985
1110
  const notification = this.notifications.get(notificationId);
986
1111
  if (!notification?.deliveredAt)
987
1112
  return false;
1113
+ this.claimedNotifications.delete(notificationId);
988
1114
  if (!notification.analyzedAt) {
989
1115
  notification.analyzedAt = new Date().toISOString();
990
- if (!this.persistSync()) {
991
- notification.analyzedAt = undefined;
992
- return false;
993
- }
1116
+ if (!this.persistSync())
1117
+ this.scheduleRegistryRetry();
994
1118
  this.emit({
995
1119
  type: "notification",
996
1120
  jobId: notification.jobId,
@@ -1003,14 +1127,16 @@ export class JobManager {
1003
1127
  const notification = this.notifications.get(notificationId);
1004
1128
  if (!notification?.analyzedAt)
1005
1129
  return false;
1130
+ this.claimedNotifications.delete(notificationId);
1006
1131
  if (!notification.acknowledgedAt) {
1007
1132
  notification.acknowledgedAt = new Date().toISOString();
1008
- if (!this.persistSync()) {
1009
- notification.acknowledgedAt = undefined;
1010
- return false;
1133
+ const persisted = this.persistSync();
1134
+ if (!persisted)
1135
+ this.scheduleRegistryRetry();
1136
+ if (persisted) {
1137
+ this.pruneTerminalJobs();
1138
+ this.persistSync();
1011
1139
  }
1012
- this.pruneTerminalJobs();
1013
- this.persistSync();
1014
1140
  this.emit({ type: "notification", jobId: notification.jobId, notificationId });
1015
1141
  }
1016
1142
  return true;
@@ -1048,6 +1174,7 @@ export class JobManager {
1048
1174
  if (metadata.responder !== undefined)
1049
1175
  job.responder = metadata.responder;
1050
1176
  const completion = this.ensureCompletionNotification(job);
1177
+ this.scheduleTaskSettlement(job);
1051
1178
  if (completion.notification) {
1052
1179
  if (metadata.taskId !== undefined)
1053
1180
  completion.notification.taskId = metadata.taskId;
@@ -1208,7 +1335,7 @@ export class JobManager {
1208
1335
  const childClose = child
1209
1336
  ? new Promise((resolve) => child.once("close", () => resolve()))
1210
1337
  : undefined;
1211
- this.clearJobDeadline(id);
1338
+ this.clearAuthorizationTimer(id);
1212
1339
  const previousStatus = job.status;
1213
1340
  const previousWakeOnCompletion = job.wakeOnCompletion;
1214
1341
  if (options?.suppressWake)
@@ -1217,8 +1344,7 @@ export class JobManager {
1217
1344
  if (!this.persistSync()) {
1218
1345
  job.status = previousStatus;
1219
1346
  job.wakeOnCompletion = previousWakeOnCompletion;
1220
- if (!job.timeoutAt || Date.parse(job.timeoutAt) > Date.now())
1221
- this.scheduleJobDeadline(job);
1347
+ this.scheduleAuthorizationExpiry(job);
1222
1348
  return { ok: false, output: `Failed to persist stop state for job "${id}"; no signal was sent.`, exitCode: 1 };
1223
1349
  }
1224
1350
  this.emit({ type: "job", jobId: id });
@@ -1243,8 +1369,7 @@ export class JobManager {
1243
1369
  };
1244
1370
  const restoreRunning = () => {
1245
1371
  job.status = "running";
1246
- if (!job.timeoutAt || Date.parse(job.timeoutAt) > Date.now())
1247
- this.scheduleJobDeadline(job);
1372
+ this.scheduleAuthorizationExpiry(job);
1248
1373
  this.persistSync();
1249
1374
  this.emit({ type: "job", jobId: id });
1250
1375
  };
@@ -1414,6 +1539,24 @@ export class JobManager {
1414
1539
  /** Drop stale terminal durable jobs and all leftover ephemeral rows. */
1415
1540
  pruneTerminalJobs() {
1416
1541
  const now = Date.now();
1542
+ const archivedUnsettled = [...this.notifications.values()]
1543
+ .filter((notification) => Boolean(notification.archivedAt) &&
1544
+ !notification.acknowledgedAt &&
1545
+ !notification.settledAt)
1546
+ .sort((left, right) => (right.archivedAt ?? right.createdAt).localeCompare(left.archivedAt ?? left.createdAt));
1547
+ for (const [index, notification] of archivedUnsettled.entries()) {
1548
+ const archivedAt = Date.parse(notification.archivedAt ?? notification.endedAt ?? notification.createdAt);
1549
+ const expired = Number.isFinite(archivedAt) &&
1550
+ now - archivedAt > ARCHIVED_UNSETTLED_MAX_AGE_MS;
1551
+ if (index < MAX_ARCHIVED_UNSETTLED_NOTIFICATIONS && !expired)
1552
+ continue;
1553
+ this.notifications.delete(notification.id);
1554
+ const timer = this.settlementTimers.get(notification.jobId);
1555
+ if (timer)
1556
+ clearTimeout(timer);
1557
+ this.settlementTimers.delete(notification.jobId);
1558
+ this.settlementAttempts.delete(notification.jobId);
1559
+ }
1417
1560
  for (const [id, notification] of this.notifications) {
1418
1561
  const job = this.jobs.get(notification.jobId);
1419
1562
  if (!job ||
@@ -1432,8 +1575,11 @@ export class JobManager {
1432
1575
  if (this.isLive(job))
1433
1576
  continue;
1434
1577
  const notification = this.notificationForJob(job.id);
1435
- if (notification && !notification.acknowledgedAt)
1578
+ if (notification &&
1579
+ !notification.acknowledgedAt &&
1580
+ !(notification.archivedAt && notification.settledAt)) {
1436
1581
  continue;
1582
+ }
1437
1583
  const ended = job.endedAt ? Date.parse(job.endedAt) : Date.parse(job.startedAt);
1438
1584
  if (Number.isFinite(ended) && now - ended > TERMINAL_JOB_MAX_AGE_MS) {
1439
1585
  this.jobs.delete(id);
@@ -1473,6 +1619,9 @@ export class JobManager {
1473
1619
  if (notifiedJobs.has(notification.jobId))
1474
1620
  continue;
1475
1621
  notifiedJobs.add(notification.jobId);
1622
+ if (!notification.acknowledgedAt && !notification.archivedAt) {
1623
+ notification.archivedAt = new Date().toISOString();
1624
+ }
1476
1625
  this.notifications.set(notification.id, notification);
1477
1626
  }
1478
1627
  }
@@ -1497,9 +1646,11 @@ export class JobManager {
1497
1646
  job.heartbeatAt = new Date().toISOString();
1498
1647
  }
1499
1648
  }
1649
+ delete job.timeoutAt;
1500
1650
  this.jobs.set(job.id, job);
1501
1651
  this.ensureCompletionNotification(job);
1502
- this.scheduleJobDeadline(job);
1652
+ this.scheduleAuthorizationExpiry(job);
1653
+ this.scheduleTaskSettlement(job);
1503
1654
  }
1504
1655
  for (const [id, notification] of this.notifications) {
1505
1656
  const job = this.jobs.get(notification.jobId);