@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.
- package/dist/agent/compaction-summary.js +1 -1
- package/dist/agent/compaction-summary.js.map +1 -1
- package/dist/agent/context-manager.js +26 -3
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/plan-tool.js +134 -42
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/responder-context.d.ts +10 -0
- package/dist/agent/responder-context.js +37 -1
- package/dist/agent/responder-context.js.map +1 -1
- package/dist/agent/runner.d.ts +3 -0
- package/dist/agent/runner.js +57 -14
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-policy.js +1 -0
- package/dist/agent/session-policy.js.map +1 -1
- package/dist/agent/task-evidence.js +7 -2
- package/dist/agent/task-evidence.js.map +1 -1
- package/dist/agent/task-plan.d.ts +4 -0
- package/dist/agent/task-plan.js +8 -0
- package/dist/agent/task-plan.js.map +1 -1
- package/dist/agent/task-sync.d.ts +0 -4
- package/dist/agent/task-sync.js +6 -11
- package/dist/agent/task-sync.js.map +1 -1
- package/dist/agent/tool-call-parser.js +7 -1
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/app/adapters/current-jobs-adapter.js +5 -0
- package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
- package/dist/app/adapters/quiet-meta-tools.js +1 -0
- package/dist/app/adapters/quiet-meta-tools.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/responder-settlement.d.ts +8 -0
- package/dist/app/controllers/responder-settlement.js +20 -0
- package/dist/app/controllers/responder-settlement.js.map +1 -0
- package/dist/app/controllers/session-controller.d.ts +3 -12
- package/dist/app/controllers/session-controller.js +72 -31
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-responder.d.ts +22 -25
- package/dist/app/controllers/session-responder.js +177 -258
- package/dist/app/controllers/session-responder.js.map +1 -1
- package/dist/app/controllers/turn-controller.d.ts +1 -0
- package/dist/app/controllers/turn-controller.js +1 -0
- package/dist/app/controllers/turn-controller.js.map +1 -1
- package/dist/app/ports/jobs-port.d.ts +5 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/system.agent.md +4 -3
- 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 +40 -21
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/jobs.d.ts +20 -6
- package/dist/tools/jobs.js +203 -52
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/registry.js +5 -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.d.ts +2 -1
- package/dist/tui-v2/bootstrap/composition-root.js +15 -3
- package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
- package/dist/tui-v2/components/jobs/jobs-panel.js +50 -27
- 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 +16 -0
- 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/tui-v2/state/transcript-struct.d.ts +0 -10
- package/dist/tui-v2/state/transcript-struct.js +4 -93
- package/dist/tui-v2/state/transcript-struct.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/dist/version.generated.js.map +1 -1
- 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;
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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;
|
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. */
|
|
@@ -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
|
-
|
|
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
|
-
|
|
292
|
-
const timer = this.
|
|
297
|
+
clearAuthorizationTimer(id) {
|
|
298
|
+
const timer = this.authorizationTimers.get(id);
|
|
293
299
|
if (timer)
|
|
294
300
|
clearTimeout(timer);
|
|
295
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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))
|
|
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.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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.
|
|
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.
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
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);
|