@gajae-code/coding-agent 0.12.6 → 0.12.7
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/CHANGELOG.md +3 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/session/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +4 -5
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- package/package.json +8 -8
- package/src/async/job-manager.ts +430 -46
- package/src/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/bus/chat-daemon-control.ts +3 -5
- package/src/sdk/bus/index.ts +8 -324
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/session/agent-session.ts +15 -178
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-storage.ts +5 -104
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +21 -12
- package/src/session/streaming-output.ts +40 -198
- package/src/task/index.ts +230 -64
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- package/src/utils/artifact-id.ts +0 -6
package/src/async/job-manager.ts
CHANGED
|
@@ -16,6 +16,7 @@ const MAX_DEAD_LETTERED_DELIVERIES = 50;
|
|
|
16
16
|
|
|
17
17
|
export interface AsyncJob {
|
|
18
18
|
id: string;
|
|
19
|
+
readonly generation: string;
|
|
19
20
|
type: "bash" | "task";
|
|
20
21
|
status: "running" | "completed" | "failed" | "cancelled" | "paused";
|
|
21
22
|
startTime: number;
|
|
@@ -61,6 +62,8 @@ export interface AsyncJobMetadata {
|
|
|
61
62
|
agentSource: AgentSource;
|
|
62
63
|
description?: string;
|
|
63
64
|
assignment?: string;
|
|
65
|
+
duplicateIdentity?: string;
|
|
66
|
+
duplicateDisposition?: "warned" | "superseded";
|
|
64
67
|
};
|
|
65
68
|
/** True when this bash job was started by the `monitor` tool (vs plain async bash). */
|
|
66
69
|
monitor?: boolean;
|
|
@@ -166,6 +169,11 @@ export interface SubagentRecord {
|
|
|
166
169
|
modelFellBack?: boolean;
|
|
167
170
|
/** True when the effective subagent provider is in fast mode. */
|
|
168
171
|
fastMode?: boolean;
|
|
172
|
+
duplicateIdentity?: string;
|
|
173
|
+
duplicateDisposition?: "warned" | "superseded";
|
|
174
|
+
terminalGeneration?: string;
|
|
175
|
+
/** Generation of currentJobId, preventing stale ID reuse from mutating this record. */
|
|
176
|
+
currentJobGeneration?: string;
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
/** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
|
|
@@ -225,6 +233,8 @@ export interface AsyncJobDisposeDiagnostics {
|
|
|
225
233
|
|
|
226
234
|
interface AsyncJobDelivery {
|
|
227
235
|
jobId: string;
|
|
236
|
+
generation: string;
|
|
237
|
+
job: AsyncJob;
|
|
228
238
|
text: string;
|
|
229
239
|
originalBytes?: number;
|
|
230
240
|
truncated?: boolean;
|
|
@@ -237,6 +247,7 @@ interface AsyncJobDelivery {
|
|
|
237
247
|
|
|
238
248
|
interface DeadLetteredDelivery {
|
|
239
249
|
jobId: string;
|
|
250
|
+
generation: string;
|
|
240
251
|
attempt: number;
|
|
241
252
|
lastError?: string;
|
|
242
253
|
}
|
|
@@ -289,6 +300,51 @@ export interface AsyncJobFilter {
|
|
|
289
300
|
ownerId?: string;
|
|
290
301
|
}
|
|
291
302
|
|
|
303
|
+
export type AsyncJobWaitCondition = "all_terminal" | "any_terminal";
|
|
304
|
+
export type AsyncJobWaitOutcome = "completed" | "timed_out_wait" | "interrupted";
|
|
305
|
+
|
|
306
|
+
export interface AsyncJobWaitTarget {
|
|
307
|
+
targetId: string;
|
|
308
|
+
jobId: string | null;
|
|
309
|
+
subagentId?: string;
|
|
310
|
+
generation: string;
|
|
311
|
+
ownerId?: string;
|
|
312
|
+
initialStatus: AsyncJob["status"] | "queued" | "not_found";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface AsyncJobWaitResult {
|
|
316
|
+
outcome: AsyncJobWaitOutcome;
|
|
317
|
+
condition: AsyncJobWaitCondition;
|
|
318
|
+
terminalJobIds: string[];
|
|
319
|
+
pendingJobIds: string[];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface AsyncJobWaitHandle {
|
|
323
|
+
readonly token: string;
|
|
324
|
+
readonly result: Promise<AsyncJobWaitResult>;
|
|
325
|
+
acknowledge(targetIds?: readonly string[]): { acknowledged: boolean; jobIds: string[] };
|
|
326
|
+
close(): void;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface TerminalEvent {
|
|
330
|
+
generation: string;
|
|
331
|
+
jobId: string | null;
|
|
332
|
+
subagentId?: string;
|
|
333
|
+
ownerId?: string;
|
|
334
|
+
status: "completed" | "failed" | "cancelled";
|
|
335
|
+
createdAt: number;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface TerminalWaitState {
|
|
339
|
+
token: string;
|
|
340
|
+
targets: AsyncJobWaitTarget[];
|
|
341
|
+
condition: AsyncJobWaitCondition;
|
|
342
|
+
resolve: (result: AsyncJobWaitResult) => void;
|
|
343
|
+
settled: boolean;
|
|
344
|
+
acknowledged: boolean;
|
|
345
|
+
terminalGenerations: Set<string>;
|
|
346
|
+
}
|
|
347
|
+
|
|
292
348
|
function sliceTextFromUtf8ByteOffset(text: string, offsetBytes: number): string {
|
|
293
349
|
if (offsetBytes <= 0) return text;
|
|
294
350
|
let consumedBytes = 0;
|
|
@@ -387,6 +443,7 @@ export class AsyncJobManager {
|
|
|
387
443
|
readonly #deliveries: AsyncJobDelivery[] = [];
|
|
388
444
|
readonly #inFlightDeliveries: AsyncJobDelivery[] = [];
|
|
389
445
|
readonly #suppressedDeliveries = new Set<string>();
|
|
446
|
+
readonly #deliveryAckOwners = new Map<string, string>();
|
|
390
447
|
readonly #watchedJobs = new Set<string>();
|
|
391
448
|
readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
|
|
392
449
|
readonly #outputState = new Map<string, AsyncJobOutputState>();
|
|
@@ -401,6 +458,12 @@ export class AsyncJobManager {
|
|
|
401
458
|
#deliveryLoop: Promise<void> | undefined;
|
|
402
459
|
#disposed = false;
|
|
403
460
|
readonly #subagentRecords = new Map<string, SubagentRecord>();
|
|
461
|
+
readonly #terminalEvents = new Map<string, TerminalEvent>();
|
|
462
|
+
readonly #waitGenerationAliases = new Map<string, string>();
|
|
463
|
+
readonly #terminalWaits = new Map<string, TerminalWaitState>();
|
|
464
|
+
#waitSeq = 0;
|
|
465
|
+
readonly #publishedTerminalGenerations = new Set<string>();
|
|
466
|
+
#jobGenerationSeq = 0;
|
|
404
467
|
readonly #liveHandles = new Map<string, SubagentLiveHandle>();
|
|
405
468
|
readonly #subagentProgress = new Map<string, AgentProgress>();
|
|
406
469
|
readonly #resumeQueue: ResumeQueueEntry[] = [];
|
|
@@ -427,6 +490,272 @@ export class AsyncJobManager {
|
|
|
427
490
|
*/
|
|
428
491
|
readonly #changeListeners = new Set<() => void>();
|
|
429
492
|
|
|
493
|
+
#pruneTerminalEvents(): void {
|
|
494
|
+
const cutoff = Date.now() - Math.max(this.#retentionMs, 300_000);
|
|
495
|
+
for (const [generation, event] of this.#terminalEvents) {
|
|
496
|
+
if (event.createdAt >= cutoff) continue;
|
|
497
|
+
this.#terminalEvents.delete(generation);
|
|
498
|
+
this.#publishedTerminalGenerations.delete(generation);
|
|
499
|
+
this.#deliveryAckOwners.delete(generation);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#eventForTarget(target: AsyncJobWaitTarget): TerminalEvent | undefined {
|
|
504
|
+
this.#pruneTerminalEvents();
|
|
505
|
+
const aliasedGeneration = this.#waitGenerationAliases.get(target.generation);
|
|
506
|
+
if (aliasedGeneration) return this.#terminalEvents.get(aliasedGeneration);
|
|
507
|
+
const job = target.jobId ? this.#jobs.get(target.jobId) : undefined;
|
|
508
|
+
if (job) {
|
|
509
|
+
if (job.generation !== target.generation) return undefined;
|
|
510
|
+
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
511
|
+
return {
|
|
512
|
+
generation: job.generation,
|
|
513
|
+
jobId: job.id,
|
|
514
|
+
subagentId: target.subagentId,
|
|
515
|
+
ownerId: job.ownerId,
|
|
516
|
+
status: job.status,
|
|
517
|
+
createdAt: job.endTime ?? Date.now(),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
return this.#terminalEvents.get(target.generation);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#resultForWait(state: TerminalWaitState, outcome: AsyncJobWaitOutcome): AsyncJobWaitResult {
|
|
526
|
+
const terminalJobIds: string[] = [];
|
|
527
|
+
const pendingJobIds: string[] = [];
|
|
528
|
+
for (const target of state.targets) {
|
|
529
|
+
if (this.#eventForTarget(target)) terminalJobIds.push(target.targetId);
|
|
530
|
+
else pendingJobIds.push(target.targetId);
|
|
531
|
+
}
|
|
532
|
+
return { outcome, condition: state.condition, terminalJobIds, pendingJobIds };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#maybeResolveWait(state: TerminalWaitState): void {
|
|
536
|
+
if (state.settled) return;
|
|
537
|
+
const terminalCount = state.targets.filter(target => this.#eventForTarget(target)).length;
|
|
538
|
+
if (state.condition === "all_terminal" ? terminalCount !== state.targets.length : terminalCount === 0) return;
|
|
539
|
+
state.settled = true;
|
|
540
|
+
this.#terminalWaits.delete(state.token);
|
|
541
|
+
state.resolve(this.#resultForWait(state, "completed"));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#publishQueuedTerminal(subagentId: string, generation: string, status: "completed" | "failed" | "cancelled"): void {
|
|
545
|
+
if (this.#publishedTerminalGenerations.has(generation)) return;
|
|
546
|
+
this.#publishedTerminalGenerations.add(generation);
|
|
547
|
+
const record = this.#subagentRecords.get(subagentId);
|
|
548
|
+
if (record) {
|
|
549
|
+
record.currentJobId = null;
|
|
550
|
+
record.terminalGeneration = generation;
|
|
551
|
+
record.status = status;
|
|
552
|
+
record.queued = undefined;
|
|
553
|
+
}
|
|
554
|
+
this.#terminalEvents.set(generation, {
|
|
555
|
+
generation,
|
|
556
|
+
jobId: null,
|
|
557
|
+
subagentId,
|
|
558
|
+
ownerId: record?.ownerId,
|
|
559
|
+
status,
|
|
560
|
+
createdAt: Date.now(),
|
|
561
|
+
});
|
|
562
|
+
for (const state of this.#terminalWaits.values()) {
|
|
563
|
+
if (
|
|
564
|
+
state.targets.some(
|
|
565
|
+
target =>
|
|
566
|
+
target.generation === generation || this.#waitGenerationAliases.get(target.generation) === generation,
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
this.#maybeResolveWait(state);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#publishTerminal(job: AsyncJob): void {
|
|
574
|
+
if (job.status !== "completed" && job.status !== "failed" && job.status !== "cancelled") return;
|
|
575
|
+
if (this.#publishedTerminalGenerations.has(job.generation)) return;
|
|
576
|
+
this.#publishedTerminalGenerations.add(job.generation);
|
|
577
|
+
const record = Array.from(this.#subagentRecords.values()).find(
|
|
578
|
+
item => item.currentJobId === job.id && item.currentJobGeneration === job.generation,
|
|
579
|
+
);
|
|
580
|
+
if (record) record.terminalGeneration = job.generation;
|
|
581
|
+
this.#terminalEvents.set(job.generation, {
|
|
582
|
+
generation: job.generation,
|
|
583
|
+
jobId: job.id,
|
|
584
|
+
subagentId: record?.subagentId,
|
|
585
|
+
ownerId: job.ownerId,
|
|
586
|
+
status: job.status,
|
|
587
|
+
createdAt: Date.now(),
|
|
588
|
+
});
|
|
589
|
+
for (const state of this.#terminalWaits.values()) {
|
|
590
|
+
if (
|
|
591
|
+
state.targets.some(
|
|
592
|
+
target =>
|
|
593
|
+
target.generation === job.generation ||
|
|
594
|
+
target.jobId === job.id ||
|
|
595
|
+
this.#waitGenerationAliases.get(target.generation) === job.generation,
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
this.#maybeResolveWait(state);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
resolveSubagentWaitTarget(id: string, filter?: AsyncJobFilter): AsyncJobWaitTarget | undefined {
|
|
603
|
+
const targetId = id.trim();
|
|
604
|
+
if (!targetId) return undefined;
|
|
605
|
+
this.#pruneTerminalEvents();
|
|
606
|
+
const record = this.getSubagentRecord(targetId, filter);
|
|
607
|
+
if (record) {
|
|
608
|
+
if (filter?.ownerId && record.ownerId !== filter.ownerId) return undefined;
|
|
609
|
+
if (record.status === "queued" && record.queued?.seq !== undefined) {
|
|
610
|
+
return {
|
|
611
|
+
targetId,
|
|
612
|
+
jobId: null,
|
|
613
|
+
subagentId: record.subagentId,
|
|
614
|
+
generation: `queued:${record.subagentId}:${record.queued.seq}`,
|
|
615
|
+
ownerId: record.ownerId,
|
|
616
|
+
initialStatus: "queued",
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (record.terminalGeneration && this.#terminalEvents.has(record.terminalGeneration)) {
|
|
620
|
+
const generation = record.terminalGeneration;
|
|
621
|
+
const event = this.#terminalEvents.get(generation);
|
|
622
|
+
const current = record.currentJobId ? this.#jobs.get(record.currentJobId) : undefined;
|
|
623
|
+
if (!current || current.generation !== record.currentJobGeneration || current.generation !== generation) {
|
|
624
|
+
return {
|
|
625
|
+
targetId,
|
|
626
|
+
jobId: event?.jobId ?? null,
|
|
627
|
+
subagentId: record.subagentId,
|
|
628
|
+
generation,
|
|
629
|
+
ownerId: record.ownerId,
|
|
630
|
+
initialStatus: record.status,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (record.currentJobId) {
|
|
635
|
+
const job = this.#jobs.get(record.currentJobId);
|
|
636
|
+
if (job && record.currentJobGeneration === job.generation)
|
|
637
|
+
return {
|
|
638
|
+
targetId,
|
|
639
|
+
jobId: record.currentJobId,
|
|
640
|
+
subagentId: record.subagentId,
|
|
641
|
+
generation: job.generation,
|
|
642
|
+
ownerId: record.ownerId,
|
|
643
|
+
initialStatus: job.status,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (record.terminalGeneration && this.#terminalEvents.has(record.terminalGeneration)) {
|
|
647
|
+
const generation = record.terminalGeneration;
|
|
648
|
+
return {
|
|
649
|
+
targetId,
|
|
650
|
+
jobId: generation.startsWith("queued:") ? null : generation,
|
|
651
|
+
subagentId: record.subagentId,
|
|
652
|
+
generation,
|
|
653
|
+
ownerId: record.ownerId,
|
|
654
|
+
initialStatus: record.status,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
const job = this.#jobs.get(targetId);
|
|
660
|
+
if (job && (!filter?.ownerId || job.ownerId === filter.ownerId))
|
|
661
|
+
return {
|
|
662
|
+
targetId,
|
|
663
|
+
jobId: job.id,
|
|
664
|
+
subagentId: job.metadata?.subagent?.id,
|
|
665
|
+
generation: job.generation,
|
|
666
|
+
ownerId: job.ownerId,
|
|
667
|
+
initialStatus: job.status,
|
|
668
|
+
};
|
|
669
|
+
const metadataJobs = Array.from(this.#jobs.values()).filter(
|
|
670
|
+
candidate =>
|
|
671
|
+
candidate.metadata?.subagent?.id === targetId && (!filter?.ownerId || candidate.ownerId === filter.ownerId),
|
|
672
|
+
);
|
|
673
|
+
const metadataJob = metadataJobs.sort((a, b) => b.startTime - a.startTime)[0];
|
|
674
|
+
if (metadataJob)
|
|
675
|
+
return {
|
|
676
|
+
targetId,
|
|
677
|
+
jobId: metadataJob.id,
|
|
678
|
+
subagentId: metadataJob.metadata?.subagent?.id,
|
|
679
|
+
generation: metadataJob.generation,
|
|
680
|
+
ownerId: metadataJob.ownerId,
|
|
681
|
+
initialStatus: metadataJob.status,
|
|
682
|
+
};
|
|
683
|
+
const event = this.#terminalEvents.get(targetId);
|
|
684
|
+
if (event && (!filter?.ownerId || event.ownerId === filter.ownerId))
|
|
685
|
+
return {
|
|
686
|
+
targetId,
|
|
687
|
+
jobId: event.jobId,
|
|
688
|
+
subagentId: event.subagentId,
|
|
689
|
+
generation: event.generation,
|
|
690
|
+
ownerId: event.ownerId,
|
|
691
|
+
initialStatus: event.status,
|
|
692
|
+
};
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
subscribeTerminalWait(
|
|
697
|
+
targets: readonly AsyncJobWaitTarget[],
|
|
698
|
+
condition: AsyncJobWaitCondition = "all_terminal",
|
|
699
|
+
): AsyncJobWaitHandle {
|
|
700
|
+
const deduped: AsyncJobWaitTarget[] = [];
|
|
701
|
+
const seen = new Set<string>();
|
|
702
|
+
for (const target of targets) {
|
|
703
|
+
if (seen.has(target.generation)) continue;
|
|
704
|
+
seen.add(target.generation);
|
|
705
|
+
deduped.push(target);
|
|
706
|
+
}
|
|
707
|
+
let resolve!: (result: AsyncJobWaitResult) => void;
|
|
708
|
+
const result = new Promise<AsyncJobWaitResult>(resolver => {
|
|
709
|
+
resolve = resolver;
|
|
710
|
+
});
|
|
711
|
+
const state: TerminalWaitState = {
|
|
712
|
+
token: `wait_${++this.#waitSeq}`,
|
|
713
|
+
targets: deduped,
|
|
714
|
+
condition,
|
|
715
|
+
resolve,
|
|
716
|
+
settled: false,
|
|
717
|
+
acknowledged: false,
|
|
718
|
+
terminalGenerations: new Set(),
|
|
719
|
+
};
|
|
720
|
+
const handle: AsyncJobWaitHandle = {
|
|
721
|
+
token: state.token,
|
|
722
|
+
result,
|
|
723
|
+
acknowledge: (targetIds?: readonly string[]) => {
|
|
724
|
+
if (state.acknowledged) return { acknowledged: false, jobIds: [] };
|
|
725
|
+
state.acknowledged = true;
|
|
726
|
+
const allowed = targetIds ? new Set(targetIds) : undefined;
|
|
727
|
+
const ids: string[] = [];
|
|
728
|
+
for (const target of deduped) {
|
|
729
|
+
if (allowed && !allowed.has(target.targetId)) continue;
|
|
730
|
+
const event = this.#eventForTarget(target);
|
|
731
|
+
if (!event) continue;
|
|
732
|
+
ids.push(event.jobId ?? event.generation);
|
|
733
|
+
const owner = this.#deliveryAckOwners.get(event.generation);
|
|
734
|
+
if (owner && owner !== state.token) continue;
|
|
735
|
+
this.#deliveryAckOwners.set(event.generation, state.token);
|
|
736
|
+
this.#suppressedDeliveries.add(event.generation);
|
|
737
|
+
}
|
|
738
|
+
this.#deliveries.splice(
|
|
739
|
+
0,
|
|
740
|
+
this.#deliveries.length,
|
|
741
|
+
...this.#deliveries.filter(
|
|
742
|
+
delivery => !this.#isDeliveryAcknowledged(delivery.jobId, delivery.generation),
|
|
743
|
+
),
|
|
744
|
+
);
|
|
745
|
+
return { acknowledged: ids.length > 0, jobIds: ids };
|
|
746
|
+
},
|
|
747
|
+
close: () => {
|
|
748
|
+
if (state.settled) return;
|
|
749
|
+
state.settled = true;
|
|
750
|
+
this.#terminalWaits.delete(state.token);
|
|
751
|
+
resolve(this.#resultForWait(state, "interrupted"));
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
this.#terminalWaits.set(state.token, state);
|
|
755
|
+
this.#maybeResolveWait(state);
|
|
756
|
+
return handle;
|
|
757
|
+
}
|
|
758
|
+
|
|
430
759
|
#filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
|
|
431
760
|
const ownerId = filter?.ownerId;
|
|
432
761
|
if (!ownerId) return Array.from(jobs);
|
|
@@ -491,12 +820,12 @@ export class AsyncJobManager {
|
|
|
491
820
|
|
|
492
821
|
this.#expireMonitorTombstones();
|
|
493
822
|
const id = this.#resolveJobId(options?.id);
|
|
494
|
-
this.#suppressedDeliveries.delete(id);
|
|
495
823
|
const abortController = new AbortController();
|
|
496
824
|
const startTime = Date.now();
|
|
497
825
|
|
|
498
826
|
const job: AsyncJob = {
|
|
499
827
|
id,
|
|
828
|
+
generation: `job:${++this.#jobGenerationSeq}`,
|
|
500
829
|
type,
|
|
501
830
|
status: "running",
|
|
502
831
|
startTime,
|
|
@@ -527,7 +856,8 @@ export class AsyncJobManager {
|
|
|
527
856
|
|
|
528
857
|
if (job.status === "cancelled") {
|
|
529
858
|
job.resultText = outcome.kind === "paused" ? outcome.note : outcome.text;
|
|
530
|
-
this.#markRecordTerminal(id, "cancelled");
|
|
859
|
+
this.#markRecordTerminal(id, "cancelled", job.generation);
|
|
860
|
+
this.#publishTerminal(job);
|
|
531
861
|
this.#runLifecycle(id, "terminal", job);
|
|
532
862
|
this.#scheduleEviction(id);
|
|
533
863
|
this.#drainResumeQueue();
|
|
@@ -540,7 +870,7 @@ export class AsyncJobManager {
|
|
|
540
870
|
job.status = "paused";
|
|
541
871
|
this.#freezeEndTime(job);
|
|
542
872
|
if (outcome.note) job.resultText = outcome.note;
|
|
543
|
-
this.#markRecordPaused(id);
|
|
873
|
+
this.#markRecordPaused(id, job.generation);
|
|
544
874
|
this.#notifyChange();
|
|
545
875
|
this.#drainResumeQueue();
|
|
546
876
|
return;
|
|
@@ -550,7 +880,8 @@ export class AsyncJobManager {
|
|
|
550
880
|
job.setupFailureSummary = outcome.setupFailureSummary;
|
|
551
881
|
this.#freezeEndTime(job);
|
|
552
882
|
job.errorText = outcome.text;
|
|
553
|
-
this.#markRecordTerminal(id, "failed");
|
|
883
|
+
this.#markRecordTerminal(id, "failed", job.generation);
|
|
884
|
+
this.#publishTerminal(job);
|
|
554
885
|
this.#enqueueDelivery(id, outcome.text);
|
|
555
886
|
this.#runLifecycle(id, "terminal", job);
|
|
556
887
|
this.#scheduleEviction(id);
|
|
@@ -561,7 +892,8 @@ export class AsyncJobManager {
|
|
|
561
892
|
job.status = "completed";
|
|
562
893
|
this.#freezeEndTime(job);
|
|
563
894
|
job.resultText = outcome.text;
|
|
564
|
-
this.#markRecordTerminal(id, "completed");
|
|
895
|
+
this.#markRecordTerminal(id, "completed", job.generation);
|
|
896
|
+
this.#publishTerminal(job);
|
|
565
897
|
this.#enqueueDelivery(id, outcome.text);
|
|
566
898
|
this.#runLifecycle(id, "terminal", job);
|
|
567
899
|
this.#scheduleEviction(id);
|
|
@@ -569,7 +901,8 @@ export class AsyncJobManager {
|
|
|
569
901
|
} catch (error) {
|
|
570
902
|
if (job.status === "cancelled") {
|
|
571
903
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
572
|
-
this.#markRecordTerminal(id, "cancelled");
|
|
904
|
+
this.#markRecordTerminal(id, "cancelled", job.generation);
|
|
905
|
+
this.#publishTerminal(job);
|
|
573
906
|
this.#runLifecycle(id, "terminal", job);
|
|
574
907
|
this.#scheduleEviction(id);
|
|
575
908
|
this.#drainResumeQueue();
|
|
@@ -579,7 +912,8 @@ export class AsyncJobManager {
|
|
|
579
912
|
job.status = "failed";
|
|
580
913
|
this.#freezeEndTime(job);
|
|
581
914
|
job.errorText = errorText;
|
|
582
|
-
this.#markRecordTerminal(id, "failed");
|
|
915
|
+
this.#markRecordTerminal(id, "failed", job.generation);
|
|
916
|
+
this.#publishTerminal(job);
|
|
583
917
|
this.#enqueueDelivery(id, errorText);
|
|
584
918
|
this.#runLifecycle(id, "terminal", job);
|
|
585
919
|
this.#scheduleEviction(id);
|
|
@@ -605,7 +939,9 @@ export class AsyncJobManager {
|
|
|
605
939
|
// Paused jobs have no running promise to abort; transition directly.
|
|
606
940
|
// The session file is kept, so the record stays resumable by id.
|
|
607
941
|
job.status = "cancelled";
|
|
608
|
-
this.#markRecordTerminal(id, "cancelled");
|
|
942
|
+
this.#markRecordTerminal(id, "cancelled", job.generation);
|
|
943
|
+
this.#freezeEndTime(job);
|
|
944
|
+
this.#publishTerminal(job);
|
|
609
945
|
this.#runLifecycle(id, "cancel");
|
|
610
946
|
this.#scheduleEviction(id);
|
|
611
947
|
this.#drainResumeQueue();
|
|
@@ -614,7 +950,8 @@ export class AsyncJobManager {
|
|
|
614
950
|
if (job.status !== "running") return false;
|
|
615
951
|
job.status = "cancelled";
|
|
616
952
|
this.#freezeEndTime(job);
|
|
617
|
-
this.#markRecordTerminal(id, "cancelled");
|
|
953
|
+
this.#markRecordTerminal(id, "cancelled", job.generation);
|
|
954
|
+
this.#publishTerminal(job);
|
|
618
955
|
this.#runLifecycle(id, "cancel");
|
|
619
956
|
job.abortController.abort();
|
|
620
957
|
this.#notifyChange();
|
|
@@ -700,6 +1037,8 @@ export class AsyncJobManager {
|
|
|
700
1037
|
|
|
701
1038
|
/** Register or replace the canonical record for a subagent. */
|
|
702
1039
|
registerSubagentRecord(record: SubagentRecord): void {
|
|
1040
|
+
const currentJob = record.currentJobId ? this.#jobs.get(record.currentJobId) : undefined;
|
|
1041
|
+
if (currentJob && record.currentJobGeneration === undefined) record.currentJobGeneration = currentJob.generation;
|
|
703
1042
|
this.#subagentRecords.set(record.subagentId, record);
|
|
704
1043
|
this.#notifyChange();
|
|
705
1044
|
}
|
|
@@ -1061,15 +1400,17 @@ export class AsyncJobManager {
|
|
|
1061
1400
|
if (outcome === "release") this.#ensureDeliveryLoop();
|
|
1062
1401
|
}
|
|
1063
1402
|
|
|
1064
|
-
#recordByJobId(jobId: string): SubagentRecord | undefined {
|
|
1403
|
+
#recordByJobId(jobId: string, expectedGeneration?: string): SubagentRecord | undefined {
|
|
1065
1404
|
for (const rec of this.#subagentRecords.values()) {
|
|
1066
|
-
if (rec.currentJobId
|
|
1405
|
+
if (rec.currentJobId !== jobId) continue;
|
|
1406
|
+
if (expectedGeneration !== undefined && rec.currentJobGeneration !== expectedGeneration) continue;
|
|
1407
|
+
return rec;
|
|
1067
1408
|
}
|
|
1068
1409
|
return undefined;
|
|
1069
1410
|
}
|
|
1070
1411
|
|
|
1071
|
-
#markRecordPaused(jobId: string): void {
|
|
1072
|
-
const rec = this.#recordByJobId(jobId);
|
|
1412
|
+
#markRecordPaused(jobId: string, generation?: string): void {
|
|
1413
|
+
const rec = this.#recordByJobId(jobId, generation);
|
|
1073
1414
|
if (rec) {
|
|
1074
1415
|
rec.status = "paused";
|
|
1075
1416
|
this.#liveHandles.delete(rec.subagentId);
|
|
@@ -1077,16 +1418,16 @@ export class AsyncJobManager {
|
|
|
1077
1418
|
}
|
|
1078
1419
|
}
|
|
1079
1420
|
|
|
1080
|
-
#purgeTerminalSubagentStateForJob(jobId: string): void {
|
|
1081
|
-
const rec = this.#recordByJobId(jobId);
|
|
1421
|
+
#purgeTerminalSubagentStateForJob(jobId: string, generation?: string): void {
|
|
1422
|
+
const rec = this.#recordByJobId(jobId, generation);
|
|
1082
1423
|
if (!rec) return;
|
|
1083
1424
|
if (rec.status === "paused" || rec.status === "queued") return;
|
|
1084
1425
|
this.#liveHandles.delete(rec.subagentId);
|
|
1085
1426
|
this.#subagentProgress.delete(rec.subagentId);
|
|
1086
1427
|
}
|
|
1087
1428
|
|
|
1088
|
-
#markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
|
|
1089
|
-
const rec = this.#recordByJobId(jobId);
|
|
1429
|
+
#markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled", generation?: string): void {
|
|
1430
|
+
const rec = this.#recordByJobId(jobId, generation);
|
|
1090
1431
|
if (!rec) return;
|
|
1091
1432
|
rec.status = status;
|
|
1092
1433
|
this.#liveHandles.delete(rec.subagentId);
|
|
@@ -1163,6 +1504,7 @@ export class AsyncJobManager {
|
|
|
1163
1504
|
return { ok: false, status: rec.status, reason: "owner_shutdown_in_progress" };
|
|
1164
1505
|
}
|
|
1165
1506
|
const prevJobId = rec.currentJobId;
|
|
1507
|
+
const queuedGeneration = rec.queued?.seq !== undefined ? `queued:${rec.subagentId}:${rec.queued.seq}` : undefined;
|
|
1166
1508
|
// Clear any retained progress from the previous run so a resumed subagent
|
|
1167
1509
|
// never renders the prior run's tool/output as live before it emits again.
|
|
1168
1510
|
this.#subagentProgress.delete(rec.subagentId);
|
|
@@ -1170,9 +1512,13 @@ export class AsyncJobManager {
|
|
|
1170
1512
|
const newJobId = runner?.(rec.subagentId, message, descriptor);
|
|
1171
1513
|
if (!newJobId) return { ok: false, reason: "resume_failed" };
|
|
1172
1514
|
if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
|
|
1515
|
+
rec.terminalGeneration = undefined;
|
|
1173
1516
|
rec.currentJobId = newJobId;
|
|
1517
|
+
rec.currentJobGeneration = this.#jobs.get(newJobId)?.generation;
|
|
1174
1518
|
rec.status = this.#jobs.get(newJobId)?.status ?? "running";
|
|
1175
1519
|
rec.queued = undefined;
|
|
1520
|
+
if (queuedGeneration)
|
|
1521
|
+
this.#waitGenerationAliases.set(queuedGeneration, this.#jobs.get(newJobId)?.generation ?? newJobId);
|
|
1176
1522
|
this.#notifyChange();
|
|
1177
1523
|
return { ok: true, status: rec.status, jobId: newJobId };
|
|
1178
1524
|
}
|
|
@@ -1195,9 +1541,14 @@ export class AsyncJobManager {
|
|
|
1195
1541
|
}
|
|
1196
1542
|
try {
|
|
1197
1543
|
const result = this.#startResume(rec, entry.message, this.#descriptorForRecord(rec));
|
|
1198
|
-
if (result.
|
|
1199
|
-
|
|
1200
|
-
|
|
1544
|
+
if (!result.ok) {
|
|
1545
|
+
if (result.reason === "owner_shutdown_in_progress") {
|
|
1546
|
+
index += 1;
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
const queuedSeq = rec.queued?.seq;
|
|
1550
|
+
if (queuedSeq !== undefined)
|
|
1551
|
+
this.#publishQueuedTerminal(rec.subagentId, `queued:${rec.subagentId}:${queuedSeq}`, "failed");
|
|
1201
1552
|
}
|
|
1202
1553
|
this.#resumeQueue.splice(index, 1);
|
|
1203
1554
|
} catch (error) {
|
|
@@ -1205,7 +1556,10 @@ export class AsyncJobManager {
|
|
|
1205
1556
|
index += 1;
|
|
1206
1557
|
continue;
|
|
1207
1558
|
}
|
|
1208
|
-
|
|
1559
|
+
const queuedSeq = rec.queued?.seq;
|
|
1560
|
+
if (queuedSeq !== undefined)
|
|
1561
|
+
this.#publishQueuedTerminal(rec.subagentId, `queued:${rec.subagentId}:${queuedSeq}`, "failed");
|
|
1562
|
+
this.#resumeQueue.splice(index, 1);
|
|
1209
1563
|
}
|
|
1210
1564
|
}
|
|
1211
1565
|
}
|
|
@@ -1220,6 +1574,7 @@ export class AsyncJobManager {
|
|
|
1220
1574
|
const job = currentJobId ? this.#jobs.get(currentJobId) : undefined;
|
|
1221
1575
|
const shouldScheduleEviction = job?.status === "paused";
|
|
1222
1576
|
if (shouldScheduleEviction) job.status = "cancelled";
|
|
1577
|
+
if (shouldScheduleEviction && job) this.#publishTerminal(job);
|
|
1223
1578
|
rec.status = "cancelled";
|
|
1224
1579
|
this.#liveHandles.delete(rec.subagentId);
|
|
1225
1580
|
this.#subagentProgress.delete(rec.subagentId);
|
|
@@ -1230,8 +1585,11 @@ export class AsyncJobManager {
|
|
|
1230
1585
|
}
|
|
1231
1586
|
if (rec.status === "queued") {
|
|
1232
1587
|
const idx = this.#resumeQueue.findIndex(e => e.subagentId === rec.subagentId);
|
|
1588
|
+
const queuedSeq = rec.queued?.seq;
|
|
1233
1589
|
if (idx !== -1) this.#resumeQueue.splice(idx, 1);
|
|
1234
1590
|
rec.status = "cancelled";
|
|
1591
|
+
if (queuedSeq !== undefined)
|
|
1592
|
+
this.#publishQueuedTerminal(rec.subagentId, `queued:${rec.subagentId}:${queuedSeq}`, "cancelled");
|
|
1235
1593
|
rec.queued = undefined;
|
|
1236
1594
|
this.#subagentProgress.delete(rec.subagentId);
|
|
1237
1595
|
this.#notifyChange();
|
|
@@ -1464,16 +1822,18 @@ export class AsyncJobManager {
|
|
|
1464
1822
|
acknowledgeDeliveries(jobIds: string[]): number {
|
|
1465
1823
|
const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
|
|
1466
1824
|
if (uniqueJobIds.length === 0) return 0;
|
|
1467
|
-
|
|
1468
1825
|
for (const jobId of uniqueJobIds) {
|
|
1469
|
-
this.#
|
|
1826
|
+
const currentJob = this.#jobs.get(jobId);
|
|
1827
|
+
if (currentJob) this.#suppressedDeliveries.add(currentJob.generation);
|
|
1828
|
+
for (const delivery of [...this.#deliveries, ...this.#inFlightDeliveries]) {
|
|
1829
|
+
if (delivery.jobId === jobId) this.#suppressedDeliveries.add(delivery.generation);
|
|
1830
|
+
}
|
|
1470
1831
|
}
|
|
1471
|
-
|
|
1472
1832
|
const before = this.#deliveries.length;
|
|
1473
1833
|
this.#deliveries.splice(
|
|
1474
1834
|
0,
|
|
1475
1835
|
this.#deliveries.length,
|
|
1476
|
-
...this.#deliveries.filter(delivery => !this.#isDeliveryAcknowledged(delivery.jobId)),
|
|
1836
|
+
...this.#deliveries.filter(delivery => !this.#isDeliveryAcknowledged(delivery.jobId, delivery.generation)),
|
|
1477
1837
|
);
|
|
1478
1838
|
return before - this.#deliveries.length;
|
|
1479
1839
|
}
|
|
@@ -1635,6 +1995,8 @@ export class AsyncJobManager {
|
|
|
1635
1995
|
this.#deadLetteredDeliveries.clear();
|
|
1636
1996
|
this.#deadLetteredDeliveryOwners.clear();
|
|
1637
1997
|
this.#suppressedDeliveries.clear();
|
|
1998
|
+
this.#deliveryAckOwners.clear();
|
|
1999
|
+
this.#waitGenerationAliases.clear();
|
|
1638
2000
|
this.#watchedJobs.clear();
|
|
1639
2001
|
this.#outputState.clear();
|
|
1640
2002
|
this.#ownerCleanups.clear();
|
|
@@ -1700,12 +2062,14 @@ export class AsyncJobManager {
|
|
|
1700
2062
|
this.#recordMonitorTombstone(jobId);
|
|
1701
2063
|
this.#runLifecycle(jobId, "evict");
|
|
1702
2064
|
this.#purgeTerminalSubagentStateForJob(jobId);
|
|
2065
|
+
const job = this.#jobs.get(jobId);
|
|
1703
2066
|
this.#jobs.delete(jobId);
|
|
1704
2067
|
this.#lifecycles.delete(jobId);
|
|
1705
2068
|
this.#lifecyclePhases.delete(jobId);
|
|
1706
2069
|
this.#deadLetteredDeliveries.delete(jobId);
|
|
1707
2070
|
this.#deadLetteredDeliveryOwners.delete(jobId);
|
|
1708
2071
|
this.#suppressedDeliveries.delete(jobId);
|
|
2072
|
+
if (job) this.#publishedTerminalGenerations.delete(job.generation);
|
|
1709
2073
|
this.#watchedJobs.delete(jobId);
|
|
1710
2074
|
this.#outputState.delete(jobId);
|
|
1711
2075
|
}
|
|
@@ -1719,17 +2083,21 @@ export class AsyncJobManager {
|
|
|
1719
2083
|
|
|
1720
2084
|
#filterDeliveries(filter?: AsyncJobFilter): AsyncJobDelivery[] {
|
|
1721
2085
|
const ownerId = filter?.ownerId;
|
|
1722
|
-
if (!ownerId)
|
|
2086
|
+
if (!ownerId)
|
|
2087
|
+
return this.#deliveries.filter(delivery => !this.isDeliverySuppressed(delivery.jobId, delivery.generation));
|
|
1723
2088
|
return this.#deliveries.filter(
|
|
1724
|
-
delivery => delivery.ownerId === ownerId && !this.isDeliverySuppressed(delivery.jobId),
|
|
2089
|
+
delivery => delivery.ownerId === ownerId && !this.isDeliverySuppressed(delivery.jobId, delivery.generation),
|
|
1725
2090
|
);
|
|
1726
2091
|
}
|
|
1727
2092
|
|
|
1728
2093
|
#filterInFlightDeliveries(filter?: AsyncJobFilter): AsyncJobDelivery[] {
|
|
1729
2094
|
const ownerId = filter?.ownerId;
|
|
1730
|
-
if (!ownerId)
|
|
2095
|
+
if (!ownerId)
|
|
2096
|
+
return this.#inFlightDeliveries.filter(
|
|
2097
|
+
delivery => !this.isDeliverySuppressed(delivery.jobId, delivery.generation),
|
|
2098
|
+
);
|
|
1731
2099
|
return this.#inFlightDeliveries.filter(
|
|
1732
|
-
delivery => delivery.ownerId === ownerId && !this.isDeliverySuppressed(delivery.jobId),
|
|
2100
|
+
delivery => delivery.ownerId === ownerId && !this.isDeliverySuppressed(delivery.jobId, delivery.generation),
|
|
1733
2101
|
);
|
|
1734
2102
|
}
|
|
1735
2103
|
|
|
@@ -1739,7 +2107,8 @@ export class AsyncJobManager {
|
|
|
1739
2107
|
|
|
1740
2108
|
#hasDeliverable(): boolean {
|
|
1741
2109
|
return this.#deliveries.some(
|
|
1742
|
-
delivery =>
|
|
2110
|
+
delivery =>
|
|
2111
|
+
!this.isDeliverySuppressed(delivery.jobId, delivery.generation) && !this.#isDeliveryFenced(delivery),
|
|
1743
2112
|
);
|
|
1744
2113
|
}
|
|
1745
2114
|
|
|
@@ -1748,7 +2117,8 @@ export class AsyncJobManager {
|
|
|
1748
2117
|
let selected: AsyncJobDelivery | undefined;
|
|
1749
2118
|
for (const delivery of this.#deliveries) {
|
|
1750
2119
|
if (delivery.ownerId !== filter.ownerId) continue;
|
|
1751
|
-
if (this.isDeliverySuppressed(delivery.jobId) || this.#isDeliveryFenced(delivery))
|
|
2120
|
+
if (this.isDeliverySuppressed(delivery.jobId, delivery.generation) || this.#isDeliveryFenced(delivery))
|
|
2121
|
+
continue;
|
|
1752
2122
|
if (!selected || delivery.nextAttemptAt < selected.nextAttemptAt) {
|
|
1753
2123
|
selected = delivery;
|
|
1754
2124
|
}
|
|
@@ -1770,18 +2140,21 @@ export class AsyncJobManager {
|
|
|
1770
2140
|
const index = this.#deliveries.indexOf(selected);
|
|
1771
2141
|
if (index === -1) continue;
|
|
1772
2142
|
this.#deliveries.splice(index, 1);
|
|
1773
|
-
if (this.isDeliverySuppressed(selected.jobId)) continue;
|
|
2143
|
+
if (this.isDeliverySuppressed(selected.jobId, selected.generation)) continue;
|
|
1774
2144
|
|
|
1775
2145
|
return this.#waitForDeliveryPromise(this.#deliverDelivery(selected), deadline);
|
|
1776
2146
|
}
|
|
1777
2147
|
}
|
|
1778
2148
|
|
|
1779
|
-
#isDeliveryAcknowledged(jobId: string): boolean {
|
|
1780
|
-
return
|
|
2149
|
+
#isDeliveryAcknowledged(jobId: string, generation?: string): boolean {
|
|
2150
|
+
return (
|
|
2151
|
+
(generation !== undefined && this.#suppressedDeliveries.has(generation)) ||
|
|
2152
|
+
this.#suppressedDeliveries.has(jobId)
|
|
2153
|
+
);
|
|
1781
2154
|
}
|
|
1782
2155
|
|
|
1783
|
-
isDeliverySuppressed(jobId: string): boolean {
|
|
1784
|
-
return this.#isDeliveryAcknowledged(jobId) || this.#watchedJobs.has(jobId);
|
|
2156
|
+
isDeliverySuppressed(jobId: string, generation?: string): boolean {
|
|
2157
|
+
return this.#isDeliveryAcknowledged(jobId, generation) || this.#watchedJobs.has(jobId);
|
|
1785
2158
|
}
|
|
1786
2159
|
|
|
1787
2160
|
#pruneEvictedDeadLetters(): void {
|
|
@@ -1794,11 +2167,13 @@ export class AsyncJobManager {
|
|
|
1794
2167
|
|
|
1795
2168
|
#recordDeadLetter(delivery: AsyncJobDelivery): void {
|
|
1796
2169
|
this.#pruneEvictedDeadLetters();
|
|
1797
|
-
|
|
2170
|
+
const currentJob = this.#jobs.get(delivery.jobId);
|
|
2171
|
+
if (!currentJob || currentJob.generation !== delivery.generation) return;
|
|
1798
2172
|
this.#deadLetteredDeliveries.delete(delivery.jobId);
|
|
1799
2173
|
this.#deadLetteredDeliveryOwners.delete(delivery.jobId);
|
|
1800
2174
|
this.#deadLetteredDeliveries.set(delivery.jobId, {
|
|
1801
2175
|
jobId: delivery.jobId,
|
|
2176
|
+
generation: delivery.generation,
|
|
1802
2177
|
attempt: delivery.attempt,
|
|
1803
2178
|
lastError: delivery.lastError,
|
|
1804
2179
|
});
|
|
@@ -1812,19 +2187,19 @@ export class AsyncJobManager {
|
|
|
1812
2187
|
}
|
|
1813
2188
|
|
|
1814
2189
|
#enqueueDelivery(jobId: string, text: string): void {
|
|
1815
|
-
|
|
1816
|
-
if (this.#isDeliveryAcknowledged(jobId))
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
2190
|
+
const job = this.#jobs.get(jobId);
|
|
2191
|
+
if (!job || this.#isDeliveryAcknowledged(jobId, job.generation)) return;
|
|
1819
2192
|
const deliveryText = this.#boundedDeliveryText(text);
|
|
1820
2193
|
this.#deliveries.push({
|
|
1821
2194
|
jobId,
|
|
2195
|
+
generation: job.generation,
|
|
2196
|
+
job,
|
|
1822
2197
|
text: deliveryText.text,
|
|
1823
2198
|
originalBytes: deliveryText.originalBytes,
|
|
1824
2199
|
truncated: deliveryText.truncated,
|
|
1825
2200
|
attempt: 0,
|
|
1826
2201
|
nextAttemptAt: Date.now(),
|
|
1827
|
-
ownerId:
|
|
2202
|
+
ownerId: job.ownerId,
|
|
1828
2203
|
});
|
|
1829
2204
|
while (this.#deliveries.length > DEFAULT_MAX_DELIVERY_QUEUE) {
|
|
1830
2205
|
const dropped = this.#deliveries.shift();
|
|
@@ -1867,7 +2242,8 @@ export class AsyncJobManager {
|
|
|
1867
2242
|
async #runDeliveryLoop(): Promise<void> {
|
|
1868
2243
|
while (this.#deliveries.length > 0) {
|
|
1869
2244
|
const delivery = this.#deliveries.find(
|
|
1870
|
-
candidate =>
|
|
2245
|
+
candidate =>
|
|
2246
|
+
!this.isDeliverySuppressed(candidate.jobId, candidate.generation) && !this.#isDeliveryFenced(candidate),
|
|
1871
2247
|
);
|
|
1872
2248
|
if (!delivery) return;
|
|
1873
2249
|
const waitMs = delivery.nextAttemptAt - Date.now();
|
|
@@ -1876,7 +2252,8 @@ export class AsyncJobManager {
|
|
|
1876
2252
|
}
|
|
1877
2253
|
const index = this.#deliveries.indexOf(delivery);
|
|
1878
2254
|
if (index === -1) continue;
|
|
1879
|
-
if (this.isDeliverySuppressed(delivery.jobId) || this.#isDeliveryFenced(delivery))
|
|
2255
|
+
if (this.isDeliverySuppressed(delivery.jobId, delivery.generation) || this.#isDeliveryFenced(delivery))
|
|
2256
|
+
continue;
|
|
1880
2257
|
|
|
1881
2258
|
this.#deliveries.splice(index, 1);
|
|
1882
2259
|
await this.#deliverDelivery(delivery);
|
|
@@ -1887,7 +2264,10 @@ export class AsyncJobManager {
|
|
|
1887
2264
|
const promise = (async () => {
|
|
1888
2265
|
this.#inFlightDeliveries.push(delivery);
|
|
1889
2266
|
try {
|
|
1890
|
-
|
|
2267
|
+
const currentJob = this.#jobs.get(delivery.jobId);
|
|
2268
|
+
if (currentJob && currentJob.generation !== delivery.generation) return;
|
|
2269
|
+
if (this.#isDeliveryAcknowledged(delivery.jobId, delivery.generation)) return;
|
|
2270
|
+
await this.#onJobComplete(delivery.jobId, delivery.text, delivery.job);
|
|
1891
2271
|
} catch (error) {
|
|
1892
2272
|
delivery.attempt += 1;
|
|
1893
2273
|
delivery.lastError = error instanceof Error ? error.message : String(error);
|
|
@@ -1900,7 +2280,11 @@ export class AsyncJobManager {
|
|
|
1900
2280
|
});
|
|
1901
2281
|
} else {
|
|
1902
2282
|
delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
|
|
1903
|
-
|
|
2283
|
+
const currentJob = this.#jobs.get(delivery.jobId);
|
|
2284
|
+
if (
|
|
2285
|
+
currentJob?.generation === delivery.generation &&
|
|
2286
|
+
!this.#isDeliveryAcknowledged(delivery.jobId, delivery.generation)
|
|
2287
|
+
) {
|
|
1904
2288
|
this.#deliveries.push(delivery);
|
|
1905
2289
|
}
|
|
1906
2290
|
logger.warn("Async job completion delivery failed", {
|