@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.
- package/dist/agent/runner.js +1 -1
- package/dist/agent/runner.js.map +1 -1
- package/dist/app/adapters/current-jobs-adapter.js +4 -0
- package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
- package/dist/app/controllers/job-controller.d.ts +4 -0
- package/dist/app/controllers/job-controller.js +12 -0
- package/dist/app/controllers/job-controller.js.map +1 -1
- package/dist/app/controllers/plan-controller.d.ts +1 -0
- package/dist/app/controllers/plan-controller.js +15 -0
- package/dist/app/controllers/plan-controller.js.map +1 -1
- package/dist/app/controllers/session-controller.d.ts +2 -0
- package/dist/app/controllers/session-controller.js +32 -10
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-responder.d.ts +19 -23
- package/dist/app/controllers/session-responder.js +157 -262
- package/dist/app/controllers/session-responder.js.map +1 -1
- package/dist/app/ports/jobs-port.d.ts +4 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/repl.js +14 -68
- package/dist/repl.js.map +1 -1
- package/dist/store/history-index.d.ts +43 -0
- package/dist/store/history-index.js +197 -0
- package/dist/store/history-index.js.map +1 -0
- package/dist/store/history.d.ts +6 -5
- package/dist/store/history.js +246 -205
- package/dist/store/history.js.map +1 -1
- package/dist/store/plan.d.ts +1 -1
- package/dist/store/plan.js +17 -8
- package/dist/store/plan.js.map +1 -1
- package/dist/store/responder-settlement.d.ts +3 -0
- package/dist/store/responder-settlement.js +60 -0
- package/dist/store/responder-settlement.js.map +1 -0
- package/dist/tools/definitions.js +16 -20
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/jobs.d.ts +18 -6
- package/dist/tools/jobs.js +195 -41
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/registry.js +4 -16
- package/dist/tools/registry.js.map +1 -1
- package/dist/tui-v2/app/commands/picker-commands.js +9 -10
- package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
- package/dist/tui-v2/bootstrap/composition-root.js +9 -0
- package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
- package/dist/tui-v2/components/jobs/jobs-panel.js +49 -26
- package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.d.ts +2 -0
- package/dist/tui-v2/components/status/status-line.js +23 -2
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/state/transcript-hydrate.d.ts +8 -2
- package/dist/tui-v2/state/transcript-hydrate.js +99 -9
- package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +1 -1
package/dist/tools/jobs.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/tools/jobs.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
292
|
-
const timer = this.
|
|
296
|
+
clearAuthorizationTimer(id) {
|
|
297
|
+
const timer = this.authorizationTimers.get(id);
|
|
293
298
|
if (timer)
|
|
294
299
|
clearTimeout(timer);
|
|
295
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
472
|
-
|
|
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.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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.
|
|
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.
|
|
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?.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
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);
|