@parall/agent-core 1.44.0 → 1.45.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/gateway-base.d.ts +8 -0
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +195 -75
- package/dist/gateway-lane-flow.d.ts +75 -5
- package/dist/gateway-lane-flow.d.ts.map +1 -1
- package/dist/gateway-lane-flow.js +234 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lane-ledger.d.ts +8 -0
- package/dist/lane-ledger.d.ts.map +1 -1
- package/dist/lane-ledger.js +14 -0
- package/dist/skills/parall-schedules.d.ts +1 -1
- package/dist/skills/parall-schedules.d.ts.map +1 -1
- package/dist/skills/parall-schedules.js +1 -1
- package/dist/skills/parall-tasks.d.ts +1 -1
- package/dist/skills/parall-tasks.d.ts.map +1 -1
- package/dist/skills/parall-tasks.js +21 -5
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/gateway-base.ts +277 -93
- package/src/gateway-lane-flow.ts +270 -18
- package/src/index.ts +1 -1
- package/src/lane-ledger.ts +14 -0
- package/src/skills/parall-schedules.ts +1 -1
- package/src/skills/parall-tasks.ts +21 -5
- package/src/types.ts +2 -1
package/src/gateway-base.ts
CHANGED
|
@@ -41,8 +41,11 @@ import {
|
|
|
41
41
|
consumeMessageWorkItem,
|
|
42
42
|
consumeTypedDispatch,
|
|
43
43
|
dispatchLaneGroup,
|
|
44
|
+
resolveDispatchByID,
|
|
45
|
+
settleDrainedTypedGroup,
|
|
46
|
+
typedLedgerEventIds,
|
|
44
47
|
} from './gateway-lane-flow.js';
|
|
45
|
-
import type { LaneFlowHost } from './gateway-lane-flow.js';
|
|
48
|
+
import type { LaneFlowHost, TypedConsumeHooks } from './gateway-lane-flow.js';
|
|
46
49
|
import { LaneLedger } from './lane-ledger.js';
|
|
47
50
|
import { routeTrigger } from './routing.js';
|
|
48
51
|
import {
|
|
@@ -447,30 +450,33 @@ export class ParallAgentGateway {
|
|
|
447
450
|
? { dispatchEventId: data.dispatch_event_id }
|
|
448
451
|
: { sourceType: 'task_activity', sourceId: data.id },
|
|
449
452
|
(dispatchEventId) => this.handleTaskAssignment(data, data.id, dispatchEventId),
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
this.
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
return this.opts.client
|
|
457
|
-
.ackDispatch(this.opts.config.org_id, {
|
|
458
|
-
source_type: 'task_activity',
|
|
459
|
-
source_id: data.id,
|
|
460
|
-
})
|
|
461
|
-
.then(
|
|
462
|
-
() => true,
|
|
463
|
-
(err) => {
|
|
464
|
-
// Same contract as ackDispatchEvent: a failed ack is a
|
|
465
|
-
// failed consume (arms backoff) and must free the hot-path
|
|
466
|
-
// dedupe so the re-drive isn't rejected by this pod forever.
|
|
453
|
+
{
|
|
454
|
+
legacyAck: (dispatchEventId) => {
|
|
455
|
+
if (dispatchEventId) {
|
|
456
|
+
return this.ackDispatchEvent(dispatchEventId, () => {
|
|
467
457
|
this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return this.opts.client
|
|
461
|
+
.ackDispatch(this.opts.config.org_id, {
|
|
462
|
+
source_type: 'task_activity',
|
|
463
|
+
source_id: data.id,
|
|
464
|
+
})
|
|
465
|
+
.then(
|
|
466
|
+
() => true,
|
|
467
|
+
(err) => {
|
|
468
|
+
// Same contract as ackDispatchEvent: a failed ack is a
|
|
469
|
+
// failed consume (arms backoff) and must free the hot-path
|
|
470
|
+
// dedupe so the re-drive isn't rejected by this pod forever.
|
|
471
|
+
this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`);
|
|
472
|
+
this.opts.log?.warn(
|
|
473
|
+
`dispatch ack failed for task ${data.id}, releasing for re-drive: ${String(err)}`,
|
|
474
|
+
);
|
|
475
|
+
return false;
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
clearDedupe: () => this.dispatchedTasks.delete(`${data.id}:${data.updated_at}`),
|
|
474
480
|
},
|
|
475
481
|
);
|
|
476
482
|
} catch (err) {
|
|
@@ -493,14 +499,19 @@ export class ParallAgentGateway {
|
|
|
493
499
|
try {
|
|
494
500
|
await this.consumeTypedDispatch(
|
|
495
501
|
{ dispatchEventId: data.id },
|
|
496
|
-
() =>
|
|
502
|
+
(dispatchEventId) =>
|
|
497
503
|
this.handleTaskComment(
|
|
498
504
|
data.source_id,
|
|
499
505
|
data.task_id ?? '',
|
|
500
506
|
data.actor_id,
|
|
501
507
|
data.delivery_reason,
|
|
508
|
+
dispatchEventId,
|
|
502
509
|
),
|
|
503
|
-
|
|
510
|
+
{
|
|
511
|
+
legacyAck: () =>
|
|
512
|
+
this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
|
|
513
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(data),
|
|
514
|
+
},
|
|
504
515
|
);
|
|
505
516
|
} catch (err) {
|
|
506
517
|
this.opts.log?.error(
|
|
@@ -512,8 +523,18 @@ export class ParallAgentGateway {
|
|
|
512
523
|
try {
|
|
513
524
|
await this.consumeTypedDispatch(
|
|
514
525
|
{ dispatchEventId: data.id },
|
|
515
|
-
() =>
|
|
516
|
-
|
|
526
|
+
(dispatchEventId) =>
|
|
527
|
+
this.handleWikiComment(
|
|
528
|
+
data.source_id,
|
|
529
|
+
data.actor_id,
|
|
530
|
+
data.delivery_reason,
|
|
531
|
+
dispatchEventId,
|
|
532
|
+
),
|
|
533
|
+
{
|
|
534
|
+
legacyAck: () =>
|
|
535
|
+
this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
|
|
536
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(data),
|
|
537
|
+
},
|
|
517
538
|
);
|
|
518
539
|
} catch (err) {
|
|
519
540
|
this.opts.log?.error(
|
|
@@ -530,7 +551,11 @@ export class ParallAgentGateway {
|
|
|
530
551
|
allowCreator: true,
|
|
531
552
|
dispatchEventId,
|
|
532
553
|
}),
|
|
533
|
-
|
|
554
|
+
{
|
|
555
|
+
legacyAck: () =>
|
|
556
|
+
this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
|
|
557
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(data),
|
|
558
|
+
},
|
|
534
559
|
);
|
|
535
560
|
} catch (err) {
|
|
536
561
|
this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
|
|
@@ -540,8 +565,13 @@ export class ParallAgentGateway {
|
|
|
540
565
|
try {
|
|
541
566
|
await this.consumeTypedDispatch(
|
|
542
567
|
{ dispatchEventId: data.id },
|
|
543
|
-
() =>
|
|
544
|
-
|
|
568
|
+
(dispatchEventId) =>
|
|
569
|
+
this.fetchAndHandleScheduleFire(data.source_id, data.actor_id, dispatchEventId),
|
|
570
|
+
{
|
|
571
|
+
legacyAck: () =>
|
|
572
|
+
this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
|
|
573
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(data),
|
|
574
|
+
},
|
|
545
575
|
);
|
|
546
576
|
} catch (err) {
|
|
547
577
|
this.opts.log?.error(
|
|
@@ -553,8 +583,13 @@ export class ParallAgentGateway {
|
|
|
553
583
|
try {
|
|
554
584
|
await this.consumeTypedDispatch(
|
|
555
585
|
{ dispatchEventId: data.id },
|
|
556
|
-
() =>
|
|
557
|
-
|
|
586
|
+
(dispatchEventId) =>
|
|
587
|
+
this.fetchAndHandleExternalTriggerRun(data.source_id, dispatchEventId),
|
|
588
|
+
{
|
|
589
|
+
legacyAck: () =>
|
|
590
|
+
this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
|
|
591
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(data),
|
|
592
|
+
},
|
|
558
593
|
);
|
|
559
594
|
} catch (err) {
|
|
560
595
|
this.opts.log?.error(
|
|
@@ -566,8 +601,12 @@ export class ParallAgentGateway {
|
|
|
566
601
|
try {
|
|
567
602
|
await this.consumeTypedDispatch(
|
|
568
603
|
{ dispatchEventId: data.id },
|
|
569
|
-
() => this.fetchAndHandleChannelMessage(data.source_id),
|
|
570
|
-
|
|
604
|
+
(dispatchEventId) => this.fetchAndHandleChannelMessage(data.source_id, dispatchEventId),
|
|
605
|
+
{
|
|
606
|
+
legacyAck: () =>
|
|
607
|
+
this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
|
|
608
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(data),
|
|
609
|
+
},
|
|
571
610
|
);
|
|
572
611
|
} catch (err) {
|
|
573
612
|
this.opts.log?.error(
|
|
@@ -579,13 +618,18 @@ export class ParallAgentGateway {
|
|
|
579
618
|
try {
|
|
580
619
|
await this.consumeTypedDispatch(
|
|
581
620
|
{ dispatchEventId: data.id },
|
|
582
|
-
() =>
|
|
621
|
+
(dispatchEventId) =>
|
|
583
622
|
this.fetchAndHandleApprovalDecided(
|
|
584
623
|
data.source_id,
|
|
585
624
|
data.actor_id,
|
|
586
625
|
data.chat_id ?? null,
|
|
626
|
+
dispatchEventId,
|
|
587
627
|
),
|
|
588
|
-
|
|
628
|
+
{
|
|
629
|
+
legacyAck: () =>
|
|
630
|
+
this.ackDispatchEvent(data.id, () => this.clearTypedDispatchDedupe(data)),
|
|
631
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(data),
|
|
632
|
+
},
|
|
589
633
|
);
|
|
590
634
|
} catch (err) {
|
|
591
635
|
this.opts.log?.error(
|
|
@@ -689,6 +733,26 @@ export class ParallAgentGateway {
|
|
|
689
733
|
});
|
|
690
734
|
}
|
|
691
735
|
|
|
736
|
+
/**
|
|
737
|
+
* Sticky: the server answered a by-id complete with 400 (predates the
|
|
738
|
+
* form). Typed resolution falls back to the legacy ack for the rest of
|
|
739
|
+
* the process lifetime.
|
|
740
|
+
*/
|
|
741
|
+
typedByIdCompleteUnsupported = false;
|
|
742
|
+
|
|
743
|
+
// Typed-face ledger helpers live in gateway-lane-flow.ts; thin delegates
|
|
744
|
+
// keep the site code and tests on the class surface.
|
|
745
|
+
private typedLedgerEventIds(events: ParallEvent[]): string[] | null {
|
|
746
|
+
return typedLedgerEventIds(this.laneFlowHost(), events);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private resolveDispatchByID(
|
|
750
|
+
dispatchEventId: string,
|
|
751
|
+
lane?: string,
|
|
752
|
+
): ReturnType<typeof resolveDispatchByID> {
|
|
753
|
+
return resolveDispatchByID(this.laneFlowHost(), dispatchEventId, lane);
|
|
754
|
+
}
|
|
755
|
+
|
|
692
756
|
/** True when this event's lifecycle is owned by the dispatch lane ledger. */
|
|
693
757
|
private usesLaneLedger(event: ParallEvent): boolean {
|
|
694
758
|
return this.laneLedger != null && !this.ledgerDisabled && this.laneLedger.handles(event);
|
|
@@ -739,17 +803,18 @@ export class ParallAgentGateway {
|
|
|
739
803
|
private consumeTypedDispatch(
|
|
740
804
|
ref: { dispatchEventId?: string; sourceType?: string; sourceId?: string },
|
|
741
805
|
run: (dispatchEventId?: string) => Promise<boolean>,
|
|
742
|
-
|
|
806
|
+
hooks: TypedConsumeHooks,
|
|
743
807
|
): Promise<void> {
|
|
744
|
-
return consumeTypedDispatch(this.laneFlowHost(), ref, run,
|
|
808
|
+
return consumeTypedDispatch(this.laneFlowHost(), ref, run, hooks);
|
|
745
809
|
}
|
|
746
810
|
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
811
|
+
// Legacy administrative ack (ledger-disabled fallback only). Typed
|
|
812
|
+
// completion must wait until the ack has either committed or failed.
|
|
813
|
+
// Errors stay best-effort: a failed ack leaves the row received, so
|
|
814
|
+
// Complete releases and re-drives it safely. The boolean outcome feeds the
|
|
815
|
+
// typed-consume backoff — an ack that failed must count as a failed
|
|
816
|
+
// consume, or an ack outage would clear the backoff entry and let the
|
|
817
|
+
// release re-drive spin at wire speed.
|
|
753
818
|
private ackDispatchEvent(dispatchEventId: string, onFailure?: () => void): Promise<boolean> {
|
|
754
819
|
return this.opts.client.ackDispatchByID(this.opts.config.org_id, dispatchEventId).then(
|
|
755
820
|
() => true,
|
|
@@ -766,6 +831,9 @@ export class ParallAgentGateway {
|
|
|
766
831
|
);
|
|
767
832
|
}
|
|
768
833
|
|
|
834
|
+
// PARITY: this switch and gateway-lane-flow's clearTypedDedupeForEvent must
|
|
835
|
+
// handle the same typed source families — extend BOTH when adding a typed
|
|
836
|
+
// event type (same dedupe entries, keyed from different event shapes).
|
|
769
837
|
private clearTypedDispatchDedupe(item: DispatchNewData): void {
|
|
770
838
|
switch (item.event_type) {
|
|
771
839
|
case 'task_assign':
|
|
@@ -1570,6 +1638,22 @@ export class ParallAgentGateway {
|
|
|
1570
1638
|
earlier,
|
|
1571
1639
|
batchText,
|
|
1572
1640
|
);
|
|
1641
|
+
if (
|
|
1642
|
+
dispatched &&
|
|
1643
|
+
this.typedLedgerEventIds(events) &&
|
|
1644
|
+
this.consumeTurnError(fork.fork.sessionKey)
|
|
1645
|
+
) {
|
|
1646
|
+
// Typed error turn in a fork: report failure to the awaiting
|
|
1647
|
+
// typed consumes so the members release for a budgeted retry,
|
|
1648
|
+
// and do NOT record the events as handled — the redrive must
|
|
1649
|
+
// not arrive wearing an "already handled" prefix. Keep draining
|
|
1650
|
+
// (an error turn is not a shutdown).
|
|
1651
|
+
this.opts.log?.info(
|
|
1652
|
+
`typed fork turn for ${last.messageId} surfaced a runtime error — releasing for retry`,
|
|
1653
|
+
);
|
|
1654
|
+
for (const item of items) item.resolve(false);
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1573
1657
|
}
|
|
1574
1658
|
if (!dispatched) {
|
|
1575
1659
|
// Shutdown short-circuit — resolve un-acked so the server requeues
|
|
@@ -1755,15 +1839,22 @@ export class ParallAgentGateway {
|
|
|
1755
1839
|
// no_action sweep); no legacy acks.
|
|
1756
1840
|
continue;
|
|
1757
1841
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1842
|
+
// Buffered typed events are the one wrapper-less dispatch path: their
|
|
1843
|
+
// consumeTypedDispatch guard returned long ago (buffer-main resolves
|
|
1844
|
+
// false) and released the claims, so THIS site owns their resolution.
|
|
1845
|
+
// Message groups here are the ledger-disabled legacy flow.
|
|
1846
|
+
const typedRefs = this.typedLedgerEventIds(events);
|
|
1847
|
+
if (!typedRefs) {
|
|
1848
|
+
try {
|
|
1849
|
+
await this.emitDispatchReceived(event);
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
this.opts.log?.warn?.(
|
|
1852
|
+
`mark-received failed for buffered dispatch, leaving unacked for retry: ${String(err)}`,
|
|
1853
|
+
);
|
|
1854
|
+
this.dispatchState.mainBuffer.unshift(...events);
|
|
1855
|
+
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1767
1858
|
}
|
|
1768
1859
|
const dispatched = await this.runDispatch(
|
|
1769
1860
|
event,
|
|
@@ -1779,6 +1870,18 @@ export class ParallAgentGateway {
|
|
|
1779
1870
|
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1780
1871
|
break;
|
|
1781
1872
|
}
|
|
1873
|
+
if (typedRefs) {
|
|
1874
|
+
// Wrapper-less resolution of the buffered typed group — protocol
|
|
1875
|
+
// lives in gateway-lane-flow.ts. The turn-error marker is consumed
|
|
1876
|
+
// HERE, before this loop can start another turn on the session.
|
|
1877
|
+
await settleDrainedTypedGroup(
|
|
1878
|
+
this.laneFlowHost(),
|
|
1879
|
+
events,
|
|
1880
|
+
typedRefs,
|
|
1881
|
+
this.consumeTurnError(this.opts.runtimeKey),
|
|
1882
|
+
);
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1782
1885
|
for (const bufferedEvent of events) {
|
|
1783
1886
|
const sourceType =
|
|
1784
1887
|
bufferedEvent.ackSourceType ??
|
|
@@ -1866,16 +1969,24 @@ export class ParallAgentGateway {
|
|
|
1866
1969
|
}
|
|
1867
1970
|
return outcome === 'dispatched';
|
|
1868
1971
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1972
|
+
// Ledger-claimed typed events skip the legacy received write — the
|
|
1973
|
+
// claim already marked the row received, and a second mark here would
|
|
1974
|
+
// wipe its lease owner (the 0713 black-hole shape on the typed face).
|
|
1975
|
+
const typedRefs = this.typedLedgerEventIds([event]);
|
|
1976
|
+
if (!typedRefs) {
|
|
1977
|
+
try {
|
|
1978
|
+
await this.emitDispatchReceived(event);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
this.opts.log?.warn?.(
|
|
1981
|
+
`mark-received failed, leaving unacked for retry: ${String(err)}`,
|
|
1982
|
+
);
|
|
1983
|
+
this.dispatchState.mainDispatching = false;
|
|
1984
|
+
this.dispatchState.mainCurrentTargetId = undefined;
|
|
1985
|
+
this.mainCurrentGroupKey = undefined;
|
|
1986
|
+
this.dispatchState.mainPreDispatchBranchPoint = undefined;
|
|
1987
|
+
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1879
1990
|
}
|
|
1880
1991
|
let dispatched = false;
|
|
1881
1992
|
try {
|
|
@@ -1884,6 +1995,17 @@ export class ParallAgentGateway {
|
|
|
1884
1995
|
this.opts.runtimeKey,
|
|
1885
1996
|
forkPrefix + buildEventBody(event),
|
|
1886
1997
|
);
|
|
1998
|
+
if (dispatched && typedRefs && this.consumeTurnError(this.opts.runtimeKey)) {
|
|
1999
|
+
// Typed error turn: report failure to the enclosing typed consume
|
|
2000
|
+
// so the member releases for a budgeted retry instead of being
|
|
2001
|
+
// terminally resolved (tech-debt: typed-dispatch-error-outcome).
|
|
2002
|
+
// Consumed HERE, before the finally's drain can start another
|
|
2003
|
+
// turn on this session and clear the marker.
|
|
2004
|
+
this.opts.log?.info(
|
|
2005
|
+
`typed dispatch turn for ${event.messageId} surfaced a runtime error — releasing for retry`,
|
|
2006
|
+
);
|
|
2007
|
+
dispatched = false;
|
|
2008
|
+
}
|
|
1887
2009
|
if (!dispatched) {
|
|
1888
2010
|
// Shutdown short-circuit — restore the fork results so a future
|
|
1889
2011
|
// pod can replay them, and return false so handleMessage skips ack.
|
|
@@ -1973,8 +2095,9 @@ export class ParallAgentGateway {
|
|
|
1973
2095
|
// — the fork's lane claim marks them received. A mark-received here
|
|
1974
2096
|
// outruns the claim, strands the row ownerless, and the chat goes
|
|
1975
2097
|
// silent (the 0713 black hole; dispatch-convergence-design.md §6).
|
|
1976
|
-
//
|
|
1977
|
-
|
|
2098
|
+
// Same rule for ledger-claimed typed events: their dsp-lane claim
|
|
2099
|
+
// owns received-ness, and a re-mark would wipe the lease owner.
|
|
2100
|
+
if (!this.usesLaneLedger(event) && !this.typedLedgerEventIds([event])) {
|
|
1978
2101
|
try {
|
|
1979
2102
|
await this.emitDispatchReceived(event);
|
|
1980
2103
|
} catch (err) {
|
|
@@ -2185,10 +2308,13 @@ export class ParallAgentGateway {
|
|
|
2185
2308
|
this.handleTaskDispatch(item.task_id ?? '', item.source_id ?? item.task_id ?? '', {
|
|
2186
2309
|
dispatchEventId,
|
|
2187
2310
|
}),
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
this.
|
|
2191
|
-
|
|
2311
|
+
{
|
|
2312
|
+
legacyAck: (dispatchEventId) =>
|
|
2313
|
+
this.ackDispatchEvent(dispatchEventId ?? item.id, () =>
|
|
2314
|
+
this.clearTypedDispatchDedupe(item),
|
|
2315
|
+
),
|
|
2316
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(item),
|
|
2317
|
+
},
|
|
2192
2318
|
);
|
|
2193
2319
|
}
|
|
2194
2320
|
|
|
@@ -2271,6 +2397,7 @@ export class ParallAgentGateway {
|
|
|
2271
2397
|
taskId: string,
|
|
2272
2398
|
actorId: string | null,
|
|
2273
2399
|
deliveryReason?: DispatchDeliveryReason | null,
|
|
2400
|
+
dispatchEventId?: string,
|
|
2274
2401
|
): Promise<boolean> {
|
|
2275
2402
|
if (this.shuttingDown) return false; // drain window — let server requeue via catch-up
|
|
2276
2403
|
const dedupeKey = `comment:${commentId}`;
|
|
@@ -2335,6 +2462,7 @@ export class ParallAgentGateway {
|
|
|
2335
2462
|
deliveryReason: deliveryReason ?? undefined,
|
|
2336
2463
|
ackSourceType: 'comment',
|
|
2337
2464
|
ackSourceId: commentId,
|
|
2465
|
+
dispatchEventId,
|
|
2338
2466
|
};
|
|
2339
2467
|
|
|
2340
2468
|
let dispatched: boolean;
|
|
@@ -2355,6 +2483,7 @@ export class ParallAgentGateway {
|
|
|
2355
2483
|
commentId: string,
|
|
2356
2484
|
actorId: string | null,
|
|
2357
2485
|
deliveryReason?: DispatchDeliveryReason | null,
|
|
2486
|
+
dispatchEventId?: string,
|
|
2358
2487
|
): Promise<boolean> {
|
|
2359
2488
|
if (this.shuttingDown) return false; // drain window — let server requeue via catch-up
|
|
2360
2489
|
// Shares the comment dedupe namespace with handleTaskComment; comment IDs
|
|
@@ -2410,6 +2539,7 @@ export class ParallAgentGateway {
|
|
|
2410
2539
|
replyTargetUri: comment.target_uri,
|
|
2411
2540
|
ackSourceType: 'comment',
|
|
2412
2541
|
ackSourceId: commentId,
|
|
2542
|
+
dispatchEventId,
|
|
2413
2543
|
};
|
|
2414
2544
|
|
|
2415
2545
|
let dispatched: boolean;
|
|
@@ -2437,6 +2567,7 @@ export class ParallAgentGateway {
|
|
|
2437
2567
|
private async fetchAndHandleScheduleFire(
|
|
2438
2568
|
runId: string,
|
|
2439
2569
|
actorId: string | null,
|
|
2570
|
+
dispatchEventId?: string,
|
|
2440
2571
|
): Promise<boolean> {
|
|
2441
2572
|
let run: ScheduleRun | null = null;
|
|
2442
2573
|
try {
|
|
@@ -2458,10 +2589,14 @@ export class ParallAgentGateway {
|
|
|
2458
2589
|
return false;
|
|
2459
2590
|
}
|
|
2460
2591
|
if (!run) return true;
|
|
2461
|
-
return this.handleScheduleFire(run, actorId);
|
|
2592
|
+
return this.handleScheduleFire(run, actorId, dispatchEventId);
|
|
2462
2593
|
}
|
|
2463
2594
|
|
|
2464
|
-
private async handleScheduleFire(
|
|
2595
|
+
private async handleScheduleFire(
|
|
2596
|
+
run: ScheduleRun,
|
|
2597
|
+
actorId: string | null,
|
|
2598
|
+
dispatchEventId?: string,
|
|
2599
|
+
): Promise<boolean> {
|
|
2465
2600
|
if (this.shuttingDown) return false; // drain window — let server requeue via catch-up
|
|
2466
2601
|
const dedupeKey = `schedule_run:${run.id}`;
|
|
2467
2602
|
if (this.dispatchedTasks.has(dedupeKey)) return false;
|
|
@@ -2484,6 +2619,7 @@ export class ParallAgentGateway {
|
|
|
2484
2619
|
attachedUri: run.fired_attached_uri ?? undefined,
|
|
2485
2620
|
ackSourceType: 'schedule_run',
|
|
2486
2621
|
ackSourceId: run.id,
|
|
2622
|
+
dispatchEventId,
|
|
2487
2623
|
};
|
|
2488
2624
|
|
|
2489
2625
|
let dispatched: boolean;
|
|
@@ -2499,7 +2635,10 @@ export class ParallAgentGateway {
|
|
|
2499
2635
|
return dispatched;
|
|
2500
2636
|
}
|
|
2501
2637
|
|
|
2502
|
-
private async fetchAndHandleExternalTriggerRun(
|
|
2638
|
+
private async fetchAndHandleExternalTriggerRun(
|
|
2639
|
+
runId: string,
|
|
2640
|
+
dispatchEventId?: string,
|
|
2641
|
+
): Promise<boolean> {
|
|
2503
2642
|
let run: ExternalTriggerRun | null = null;
|
|
2504
2643
|
try {
|
|
2505
2644
|
run = await this.opts.client.getExternalTriggerRun(this.opts.config.org_id, runId);
|
|
@@ -2517,7 +2656,7 @@ export class ParallAgentGateway {
|
|
|
2517
2656
|
return false;
|
|
2518
2657
|
}
|
|
2519
2658
|
if (!run) return true;
|
|
2520
|
-
return this.handleExternalTriggerRun(run);
|
|
2659
|
+
return this.handleExternalTriggerRun(run, dispatchEventId);
|
|
2521
2660
|
}
|
|
2522
2661
|
|
|
2523
2662
|
// fetchAndHandleChannelMessage resolves a channel_message dispatch to its
|
|
@@ -2525,7 +2664,10 @@ export class ParallAgentGateway {
|
|
|
2525
2664
|
// pipeline. targetId = the ChannelConversation id, so per-conversation
|
|
2526
2665
|
// multi-turn continuity rides the same per-target session mechanics as
|
|
2527
2666
|
// chats. Design: docs/engineering-design/external-im-channel-design.md.
|
|
2528
|
-
private async fetchAndHandleChannelMessage(
|
|
2667
|
+
private async fetchAndHandleChannelMessage(
|
|
2668
|
+
messageId: string,
|
|
2669
|
+
dispatchEventId?: string,
|
|
2670
|
+
): Promise<boolean> {
|
|
2529
2671
|
if (this.shuttingDown) return false;
|
|
2530
2672
|
// Capped dedupe (the chat-message path, not the unbounded task set): a busy
|
|
2531
2673
|
// external IM conversation would otherwise retain one key per message ever
|
|
@@ -2609,6 +2751,7 @@ export class ParallAgentGateway {
|
|
|
2609
2751
|
channelCliCapable: cliCapable,
|
|
2610
2752
|
ackSourceType: 'channel_message',
|
|
2611
2753
|
ackSourceId: msg.id,
|
|
2754
|
+
dispatchEventId,
|
|
2612
2755
|
};
|
|
2613
2756
|
|
|
2614
2757
|
// Release the claim if the event isn't actually dispatched (or throws) so
|
|
@@ -2626,7 +2769,10 @@ export class ParallAgentGateway {
|
|
|
2626
2769
|
return dispatched;
|
|
2627
2770
|
}
|
|
2628
2771
|
|
|
2629
|
-
private async handleExternalTriggerRun(
|
|
2772
|
+
private async handleExternalTriggerRun(
|
|
2773
|
+
run: ExternalTriggerRun,
|
|
2774
|
+
dispatchEventId?: string,
|
|
2775
|
+
): Promise<boolean> {
|
|
2630
2776
|
if (this.shuttingDown) return false;
|
|
2631
2777
|
const dedupeKey = `external_trigger_run:${run.id}`;
|
|
2632
2778
|
if (this.dispatchedTasks.has(dedupeKey)) return false;
|
|
@@ -2654,6 +2800,7 @@ export class ParallAgentGateway {
|
|
|
2654
2800
|
attachedUri,
|
|
2655
2801
|
ackSourceType: 'external_trigger_run',
|
|
2656
2802
|
ackSourceId: run.id,
|
|
2803
|
+
dispatchEventId,
|
|
2657
2804
|
};
|
|
2658
2805
|
|
|
2659
2806
|
let dispatched: boolean;
|
|
@@ -2673,6 +2820,7 @@ export class ParallAgentGateway {
|
|
|
2673
2820
|
approvalId: string,
|
|
2674
2821
|
actorId: string | null,
|
|
2675
2822
|
chatId: string | null,
|
|
2823
|
+
dispatchEventId?: string,
|
|
2676
2824
|
): Promise<boolean> {
|
|
2677
2825
|
let approval: Approval | null = null;
|
|
2678
2826
|
try {
|
|
@@ -2709,6 +2857,11 @@ export class ParallAgentGateway {
|
|
|
2709
2857
|
senderName: 'approver',
|
|
2710
2858
|
messageId: approval.id,
|
|
2711
2859
|
body,
|
|
2860
|
+
// The WorkItem's real source pair. Without it the legacy fallback
|
|
2861
|
+
// guessed ('message', approval_id) — a pair no row matches.
|
|
2862
|
+
ackSourceType: 'approval',
|
|
2863
|
+
ackSourceId: approval.id,
|
|
2864
|
+
dispatchEventId,
|
|
2712
2865
|
};
|
|
2713
2866
|
|
|
2714
2867
|
let dispatched: boolean;
|
|
@@ -2767,7 +2920,24 @@ export class ParallAgentGateway {
|
|
|
2767
2920
|
|
|
2768
2921
|
if (overflowMode && processed >= CATCHUP_MAX) {
|
|
2769
2922
|
try {
|
|
2770
|
-
|
|
2923
|
+
// Administrative drop of the overflow backlog (summarized to the
|
|
2924
|
+
// agent instead of dispatched). Ledger path: by-id complete —
|
|
2925
|
+
// closes the pending row and refuses (stale) one a live lane
|
|
2926
|
+
// owns; the legacy ack stays as the ledger-disabled / pre-by-id
|
|
2927
|
+
// fallback.
|
|
2928
|
+
if (this.laneLedger && !this.ledgerDisabled && !this.typedByIdCompleteUnsupported) {
|
|
2929
|
+
const outcome = await this.resolveDispatchByID(item.id);
|
|
2930
|
+
if (outcome === 'unsupported') {
|
|
2931
|
+
this.typedByIdCompleteUnsupported = true;
|
|
2932
|
+
await this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id);
|
|
2933
|
+
} else if (outcome === 'failed') {
|
|
2934
|
+
throw new Error('by-id complete failed');
|
|
2935
|
+
}
|
|
2936
|
+
// 'stale' counts as skipped: a live lane owns the row and its
|
|
2937
|
+
// turn will resolve it — nothing left for catch-up to do.
|
|
2938
|
+
} else {
|
|
2939
|
+
await this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id);
|
|
2940
|
+
}
|
|
2771
2941
|
skipped++;
|
|
2772
2942
|
const key = item.event_type;
|
|
2773
2943
|
skippedByType.set(key, (skippedByType.get(key) ?? 0) + 1);
|
|
@@ -2779,8 +2949,11 @@ export class ParallAgentGateway {
|
|
|
2779
2949
|
|
|
2780
2950
|
processed++;
|
|
2781
2951
|
try {
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2952
|
+
const typedHooks: TypedConsumeHooks = {
|
|
2953
|
+
legacyAck: () =>
|
|
2954
|
+
this.ackDispatchEvent(item.id, () => this.clearTypedDispatchDedupe(item)),
|
|
2955
|
+
clearDedupe: () => this.clearTypedDispatchDedupe(item),
|
|
2956
|
+
};
|
|
2784
2957
|
if (item.event_type === 'task_assign' && item.task_id) {
|
|
2785
2958
|
try {
|
|
2786
2959
|
await this.consumeTypedDispatch(
|
|
@@ -2793,7 +2966,7 @@ export class ParallAgentGateway {
|
|
|
2793
2966
|
dispatchEventId,
|
|
2794
2967
|
},
|
|
2795
2968
|
),
|
|
2796
|
-
|
|
2969
|
+
typedHooks,
|
|
2797
2970
|
);
|
|
2798
2971
|
} catch (err: unknown) {
|
|
2799
2972
|
this.opts.log?.warn(
|
|
@@ -2814,7 +2987,7 @@ export class ParallAgentGateway {
|
|
|
2814
2987
|
dispatchEventId,
|
|
2815
2988
|
},
|
|
2816
2989
|
),
|
|
2817
|
-
|
|
2990
|
+
typedHooks,
|
|
2818
2991
|
);
|
|
2819
2992
|
} catch (err: unknown) {
|
|
2820
2993
|
this.opts.log?.warn(
|
|
@@ -2825,49 +2998,60 @@ export class ParallAgentGateway {
|
|
|
2825
2998
|
} else if (item.event_type === 'task_comment' && item.source_id && item.task_id) {
|
|
2826
2999
|
await this.consumeTypedDispatch(
|
|
2827
3000
|
{ dispatchEventId: item.id },
|
|
2828
|
-
() =>
|
|
3001
|
+
(dispatchEventId) =>
|
|
2829
3002
|
this.handleTaskComment(
|
|
2830
3003
|
item.source_id,
|
|
2831
3004
|
item.task_id ?? '',
|
|
2832
3005
|
item.actor_id,
|
|
2833
3006
|
item.delivery_reason,
|
|
3007
|
+
dispatchEventId,
|
|
2834
3008
|
),
|
|
2835
|
-
|
|
3009
|
+
typedHooks,
|
|
2836
3010
|
);
|
|
2837
3011
|
} else if (item.event_type === 'wiki_comment' && item.source_id) {
|
|
2838
3012
|
await this.consumeTypedDispatch(
|
|
2839
3013
|
{ dispatchEventId: item.id },
|
|
2840
|
-
() =>
|
|
2841
|
-
|
|
3014
|
+
(dispatchEventId) =>
|
|
3015
|
+
this.handleWikiComment(
|
|
3016
|
+
item.source_id,
|
|
3017
|
+
item.actor_id,
|
|
3018
|
+
item.delivery_reason,
|
|
3019
|
+
dispatchEventId,
|
|
3020
|
+
),
|
|
3021
|
+
typedHooks,
|
|
2842
3022
|
);
|
|
2843
3023
|
} else if (item.event_type === 'schedule.fire' && item.source_id) {
|
|
2844
3024
|
await this.consumeTypedDispatch(
|
|
2845
3025
|
{ dispatchEventId: item.id },
|
|
2846
|
-
() =>
|
|
2847
|
-
|
|
3026
|
+
(dispatchEventId) =>
|
|
3027
|
+
this.fetchAndHandleScheduleFire(item.source_id, item.actor_id, dispatchEventId),
|
|
3028
|
+
typedHooks,
|
|
2848
3029
|
);
|
|
2849
3030
|
} else if (item.event_type === 'external_trigger' && item.source_id) {
|
|
2850
3031
|
await this.consumeTypedDispatch(
|
|
2851
3032
|
{ dispatchEventId: item.id },
|
|
2852
|
-
() =>
|
|
2853
|
-
|
|
3033
|
+
(dispatchEventId) =>
|
|
3034
|
+
this.fetchAndHandleExternalTriggerRun(item.source_id, dispatchEventId),
|
|
3035
|
+
typedHooks,
|
|
2854
3036
|
);
|
|
2855
3037
|
} else if (item.event_type === 'channel_message' && item.source_id) {
|
|
2856
3038
|
await this.consumeTypedDispatch(
|
|
2857
3039
|
{ dispatchEventId: item.id },
|
|
2858
|
-
() =>
|
|
2859
|
-
|
|
3040
|
+
(dispatchEventId) =>
|
|
3041
|
+
this.fetchAndHandleChannelMessage(item.source_id, dispatchEventId),
|
|
3042
|
+
typedHooks,
|
|
2860
3043
|
);
|
|
2861
3044
|
} else if (item.event_type === 'approval_decided' && item.source_id) {
|
|
2862
3045
|
await this.consumeTypedDispatch(
|
|
2863
3046
|
{ dispatchEventId: item.id },
|
|
2864
|
-
() =>
|
|
3047
|
+
(dispatchEventId) =>
|
|
2865
3048
|
this.fetchAndHandleApprovalDecided(
|
|
2866
3049
|
item.source_id,
|
|
2867
3050
|
item.actor_id,
|
|
2868
3051
|
item.chat_id ?? null,
|
|
3052
|
+
dispatchEventId,
|
|
2869
3053
|
),
|
|
2870
|
-
|
|
3054
|
+
typedHooks,
|
|
2871
3055
|
);
|
|
2872
3056
|
} else if (item.event_type === 'message' && item.source_id && item.chat_id) {
|
|
2873
3057
|
await this.consumeMessageWorkItem({
|