@parall/agent-core 1.37.0 → 1.38.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/dispatch-adapter.d.ts +6 -0
- package/dist/dispatch-adapter.d.ts.map +1 -1
- package/dist/gateway-base.d.ts +54 -0
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +331 -95
- package/dist/gateway-lane-flow.d.ts +74 -0
- package/dist/gateway-lane-flow.d.ts.map +1 -0
- package/dist/gateway-lane-flow.js +167 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lane-key.d.ts +45 -0
- package/dist/lane-key.d.ts.map +1 -0
- package/dist/lane-key.js +34 -0
- package/dist/lane-ledger.d.ts +112 -0
- package/dist/lane-ledger.d.ts.map +1 -0
- package/dist/lane-ledger.js +333 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/dispatch-adapter.ts +6 -0
- package/src/gateway-base.ts +493 -142
- package/src/gateway-lane-flow.ts +235 -0
- package/src/index.ts +2 -0
- package/src/lane-key.ts +67 -0
- package/src/lane-ledger.ts +370 -0
- package/src/types.ts +2 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ParallClient } from '@parall/sdk';
|
|
2
|
+
import type { DispatchAdapter, GatewayLogger } from './dispatch-adapter.js';
|
|
3
|
+
import type { DispatchableMessage, MessageDispatchDecision } from './gateway-base.js';
|
|
4
|
+
import type { LaneLedger } from './lane-ledger.js';
|
|
5
|
+
import type { ParallEvent } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Lane-flow layer: the ledger-aware consumption protocols that sit between
|
|
8
|
+
* inbound routing and `runDispatch`. Split out of gateway-base.ts (already an
|
|
9
|
+
* oversized outlier) — the gateway keeps thin delegating methods so tests and
|
|
10
|
+
* call sites stay on the class surface.
|
|
11
|
+
*/
|
|
12
|
+
/** The slice of ParallAgentGateway the lane-flow functions operate on. */
|
|
13
|
+
export interface LaneFlowHost {
|
|
14
|
+
laneLedger?: LaneLedger;
|
|
15
|
+
ledgerDisabled: boolean;
|
|
16
|
+
shuttingDown: boolean;
|
|
17
|
+
dispatchedMessages: Set<string>;
|
|
18
|
+
opts: {
|
|
19
|
+
client: ParallClient;
|
|
20
|
+
log?: GatewayLogger;
|
|
21
|
+
config: {
|
|
22
|
+
org_id: string;
|
|
23
|
+
};
|
|
24
|
+
dispatchAdapter: DispatchAdapter;
|
|
25
|
+
agentUserId: string;
|
|
26
|
+
};
|
|
27
|
+
disableLedger(reason: string): void;
|
|
28
|
+
usesLaneLedger(event: ParallEvent): boolean;
|
|
29
|
+
emitDispatchReceived(event: ParallEvent): Promise<void>;
|
|
30
|
+
runDispatch(event: ParallEvent, sessionKey: string, bodyForAgent: string, earlierEvents?: ParallEvent[], captureText?: string[]): Promise<boolean>;
|
|
31
|
+
tryClaimMessage(id: string): boolean;
|
|
32
|
+
buildMessageDispatchDecision(chatId: string, message: DispatchableMessage): Promise<MessageDispatchDecision>;
|
|
33
|
+
handleInboundEvent(event: ParallEvent): Promise<boolean>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Dispatch one group of same-lane message events under the ledger contract:
|
|
37
|
+
* claim (or reuse) the lane, run the dispatch, then complete when no local
|
|
38
|
+
* work remains for the lane. No legacy received/ack calls — the claim marks
|
|
39
|
+
* members received and complete/reply resolve them server-side.
|
|
40
|
+
*/
|
|
41
|
+
export declare function dispatchLaneGroup(host: LaneFlowHost, opts: {
|
|
42
|
+
events: ParallEvent[];
|
|
43
|
+
sessionKey: string;
|
|
44
|
+
body: string;
|
|
45
|
+
earlier: ParallEvent[];
|
|
46
|
+
captureText?: string[];
|
|
47
|
+
hasMoreLocal: () => boolean;
|
|
48
|
+
}): Promise<'dispatched' | 'foreign' | 'shutdown'>;
|
|
49
|
+
/**
|
|
50
|
+
* Consume one typed dispatch (task/comment/schedule/trigger/approval) under
|
|
51
|
+
* its typed-lane occupancy guard: claim the dsp:<id> lane (skip when another
|
|
52
|
+
* pod holds it or the WorkItem is already resolved), run the handler, ack on
|
|
53
|
+
* success (the doc's option (b): notification delivered, tracked elsewhere),
|
|
54
|
+
* then release the lane. Legacy run+ack flow when the ledger is unavailable.
|
|
55
|
+
*/
|
|
56
|
+
export declare function consumeTypedDispatch(host: LaneFlowHost, ref: {
|
|
57
|
+
dispatchEventId?: string;
|
|
58
|
+
sourceType?: string;
|
|
59
|
+
sourceId?: string;
|
|
60
|
+
}, run: (dispatchEventId?: string) => Promise<boolean>, ack: (dispatchEventId?: string) => void): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Shared consumption of a message WorkItem referenced by id — the ONE
|
|
63
|
+
* protocol for dispatch catch-up pages and post-complete re-drive hints
|
|
64
|
+
* (live first delivery stays on the message.new handler, which already has
|
|
65
|
+
* the payload). Owns: in-memory dedupe, source fetch (deleted source ⇒
|
|
66
|
+
* administrative ack), self-sender ack, decision routing, ledger-vs-legacy
|
|
67
|
+
* ack parity, and dedupe-entry cleanup on every non-dispatched path.
|
|
68
|
+
*/
|
|
69
|
+
export declare function consumeMessageWorkItem(host: LaneFlowHost, item: {
|
|
70
|
+
id: string;
|
|
71
|
+
source_id: string;
|
|
72
|
+
chat_id: string;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
//# sourceMappingURL=gateway-lane-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-lane-flow.d.ts","sourceRoot":"","sources":["../src/gateway-lane-flow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAEtF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE;QACJ,MAAM,EAAE,YAAY,CAAC;QACrB,GAAG,CAAC,EAAE,aAAa,CAAC;QACpB,MAAM,EAAE;YAAE,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3B,eAAe,EAAE,eAAe,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;IAC5C,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,WAAW,CACT,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,WAAW,EAAE,EAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACrC,4BAA4B,CAC1B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;IACJ,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,OAAO,CAAC;CAC7B,GACA,OAAO,CAAC,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC,CA6DhD;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EACzE,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EACnD,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,IAAI,GACtC,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,IAAI,CAAC,CAuDf"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { LedgerUnsupportedError } from './lane-ledger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dispatch one group of same-lane message events under the ledger contract:
|
|
4
|
+
* claim (or reuse) the lane, run the dispatch, then complete when no local
|
|
5
|
+
* work remains for the lane. No legacy received/ack calls — the claim marks
|
|
6
|
+
* members received and complete/reply resolve them server-side.
|
|
7
|
+
*/
|
|
8
|
+
export async function dispatchLaneGroup(host, opts) {
|
|
9
|
+
const ledger = host.laneLedger;
|
|
10
|
+
const event = opts.events[opts.events.length - 1];
|
|
11
|
+
let lane;
|
|
12
|
+
try {
|
|
13
|
+
lane = await ledger.ensureLane(opts.events);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
if (!(err instanceof LedgerUnsupportedError))
|
|
17
|
+
throw err;
|
|
18
|
+
// Old server: run this group through the legacy flow inline and let
|
|
19
|
+
// every later event take the legacy branch via the sticky flag.
|
|
20
|
+
host.disableLedger('claim endpoint missing');
|
|
21
|
+
await host.emitDispatchReceived(event);
|
|
22
|
+
const dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
|
|
23
|
+
if (!dispatched)
|
|
24
|
+
return 'shutdown';
|
|
25
|
+
for (const ev of opts.events) {
|
|
26
|
+
host.opts.client
|
|
27
|
+
.ackDispatch(host.opts.config.org_id, {
|
|
28
|
+
source_type: ev.ackSourceType ?? 'message',
|
|
29
|
+
source_id: ev.ackSourceId ?? ev.messageId,
|
|
30
|
+
})
|
|
31
|
+
.catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
return 'dispatched';
|
|
34
|
+
}
|
|
35
|
+
if (!lane) {
|
|
36
|
+
// A healthy incumbent owns the resource. Free the in-memory dedupe
|
|
37
|
+
// entries so the post-complete re-drive can re-trigger these messages.
|
|
38
|
+
for (const ev of opts.events) {
|
|
39
|
+
host.dispatchedMessages.delete(ev.messageId);
|
|
40
|
+
}
|
|
41
|
+
return 'foreign';
|
|
42
|
+
}
|
|
43
|
+
let dispatched = false;
|
|
44
|
+
try {
|
|
45
|
+
dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// Failed turn: hand the members back so the retry (this pod or the
|
|
49
|
+
// next) re-claims immediately instead of waiting out the lease.
|
|
50
|
+
await ledger.release(lane.laneKey).catch(() => { });
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
if (!dispatched) {
|
|
54
|
+
// Shutdown short-circuit — shutdown() releases all active lanes.
|
|
55
|
+
return 'shutdown';
|
|
56
|
+
}
|
|
57
|
+
const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
|
|
58
|
+
await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
|
|
59
|
+
return 'dispatched';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Consume one typed dispatch (task/comment/schedule/trigger/approval) under
|
|
63
|
+
* its typed-lane occupancy guard: claim the dsp:<id> lane (skip when another
|
|
64
|
+
* pod holds it or the WorkItem is already resolved), run the handler, ack on
|
|
65
|
+
* success (the doc's option (b): notification delivered, tracked elsewhere),
|
|
66
|
+
* then release the lane. Legacy run+ack flow when the ledger is unavailable.
|
|
67
|
+
*/
|
|
68
|
+
export async function consumeTypedDispatch(host, ref, run, ack) {
|
|
69
|
+
if (!host.laneLedger || host.ledgerDisabled) {
|
|
70
|
+
if (await run(ref.dispatchEventId))
|
|
71
|
+
ack(ref.dispatchEventId);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let lane;
|
|
75
|
+
try {
|
|
76
|
+
lane = await host.laneLedger.claimTyped(ref);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err instanceof LedgerUnsupportedError) {
|
|
80
|
+
host.disableLedger('claim endpoint missing');
|
|
81
|
+
if (await run(ref.dispatchEventId))
|
|
82
|
+
ack(ref.dispatchEventId);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
if (!lane) {
|
|
88
|
+
host.opts.log?.info(`typed dispatch ${ref.dispatchEventId ?? `${ref.sourceType}:${ref.sourceId}`} not claimable (held elsewhere or already resolved) — skipping`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
if (await run(lane.typedDispatchEventId))
|
|
93
|
+
ack(lane.typedDispatchEventId);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
// Release the occupancy row. A buffered dispatch may outlive this guard
|
|
97
|
+
// (lane TTL) — acceptable at-least-once; the ledger's resolution paths
|
|
98
|
+
// still dedupe the persistent side effects.
|
|
99
|
+
await host.laneLedger.completeIfIdle(lane.laneKey, false).catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Shared consumption of a message WorkItem referenced by id — the ONE
|
|
104
|
+
* protocol for dispatch catch-up pages and post-complete re-drive hints
|
|
105
|
+
* (live first delivery stays on the message.new handler, which already has
|
|
106
|
+
* the payload). Owns: in-memory dedupe, source fetch (deleted source ⇒
|
|
107
|
+
* administrative ack), self-sender ack, decision routing, ledger-vs-legacy
|
|
108
|
+
* ack parity, and dedupe-entry cleanup on every non-dispatched path.
|
|
109
|
+
*/
|
|
110
|
+
export async function consumeMessageWorkItem(host, item) {
|
|
111
|
+
if (host.shuttingDown)
|
|
112
|
+
return;
|
|
113
|
+
if (!host.tryClaimMessage(item.source_id))
|
|
114
|
+
return;
|
|
115
|
+
const ackItem = () => {
|
|
116
|
+
host.opts.client.ackDispatchByID(host.opts.config.org_id, item.id).catch(() => { });
|
|
117
|
+
};
|
|
118
|
+
let msg = null;
|
|
119
|
+
try {
|
|
120
|
+
msg = await host.opts.client.getMessage(item.source_id);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const status = err?.status;
|
|
124
|
+
if (status !== 404) {
|
|
125
|
+
host.opts.log?.warn(`message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
|
|
126
|
+
host.dispatchedMessages.delete(item.source_id);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!msg || msg.sender_id === host.opts.agentUserId) {
|
|
131
|
+
host.dispatchedMessages.delete(item.source_id);
|
|
132
|
+
ackItem();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const decision = await host.buildMessageDispatchDecision(item.chat_id, msg);
|
|
136
|
+
if (decision.action === 'retry') {
|
|
137
|
+
host.dispatchedMessages.delete(item.source_id);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (decision.action === 'skip') {
|
|
141
|
+
host.dispatchedMessages.delete(item.source_id);
|
|
142
|
+
ackItem();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
decision.event.dispatchEventId = item.id;
|
|
146
|
+
const laneResolved = host.usesLaneLedger(decision.event);
|
|
147
|
+
try {
|
|
148
|
+
const dispatched = await host.handleInboundEvent(decision.event);
|
|
149
|
+
if (dispatched) {
|
|
150
|
+
// Ledger runs resolve server-side (reply cover / no_action sweep);
|
|
151
|
+
// when the ledger is not in effect the WorkItem still needs the
|
|
152
|
+
// legacy administrative ack — without it the row would sit pending
|
|
153
|
+
// forever on this path while catch-up would have acked it.
|
|
154
|
+
if (!laneResolved)
|
|
155
|
+
ackItem();
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
host.dispatchedMessages.delete(item.source_id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
// Free the dedupe entry so a later re-drive / catch-up can retry —
|
|
163
|
+
// same cleanup contract as the other inbound handlers.
|
|
164
|
+
host.dispatchedMessages.delete(item.source_id);
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './provider-config.js';
|
|
2
2
|
export * from './types.js';
|
|
3
|
+
export * from './lane-key.js';
|
|
4
|
+
export type { LaneFlowHost } from './gateway-lane-flow.js';
|
|
3
5
|
export * from './session-state.js';
|
|
4
6
|
export * from './routing.js';
|
|
5
7
|
export * from './event-format.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAClF,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical lane-key encoding — the SSOT shared by the bridge (writer of
|
|
3
|
+
* `$PRLL_CONTEXT_DIR/<lane-key>.json`) and the CLI (reader, keyed by the send
|
|
4
|
+
* target). Both sides MUST use this exact function: the lane key encodes the
|
|
5
|
+
* full lane identity `(target_uri, thread_root_id)` so a channel lane and a
|
|
6
|
+
* thread lane in the same chat never collide, and base64url keeps any
|
|
7
|
+
* target_uri (including typed `dsp:<id>` resources) filesystem-safe with no
|
|
8
|
+
* escaping ambiguity.
|
|
9
|
+
*/
|
|
10
|
+
export declare function laneKeyForTarget(targetUri: string, threadRootId?: string | null): string;
|
|
11
|
+
/**
|
|
12
|
+
* PRLL_CONTEXT_DIR root under a per-agent stateDir. Owned here alongside the
|
|
13
|
+
* lane-key encoding so the whole context-dir contract has one home; runtime
|
|
14
|
+
* bridges import this instead of re-declaring the path.
|
|
15
|
+
*/
|
|
16
|
+
export declare function dispatchLaneContextDir(stateDir: string): string;
|
|
17
|
+
/** Path of the bridge-owned dispatch context file for a lane. */
|
|
18
|
+
export declare function laneContextFilePath(contextDir: string, targetUri: string, threadRootId?: string | null): string;
|
|
19
|
+
/**
|
|
20
|
+
* Path of the CLI-owned reply-state sidecar for a lane. Separate file from the
|
|
21
|
+
* bridge-owned context (two writers, two files): the bridge rewrites the
|
|
22
|
+
* context file throughout the turn (step_id updates) and would clobber any
|
|
23
|
+
* CLI state stored inline.
|
|
24
|
+
*/
|
|
25
|
+
export declare function laneReplyStateFilePath(contextDir: string, targetUri: string, threadRootId?: string | null): string;
|
|
26
|
+
/** Shape of the bridge-written per-lane dispatch context file. */
|
|
27
|
+
export type LaneDispatchContextFile = {
|
|
28
|
+
session_id: string | null;
|
|
29
|
+
step_id: string | null;
|
|
30
|
+
chat_id: string | null;
|
|
31
|
+
trigger_message_id: string | null;
|
|
32
|
+
no_reply: boolean;
|
|
33
|
+
/** WorkItem id of the trigger — basis of the `reply:<dsp>` effect key. */
|
|
34
|
+
dispatch_event_id: string | null;
|
|
35
|
+
/** Server-minted per-turn lane token; every dispatch-bound write carries it. */
|
|
36
|
+
lane: string | null;
|
|
37
|
+
target_uri: string | null;
|
|
38
|
+
thread_root_id: string | null;
|
|
39
|
+
};
|
|
40
|
+
/** Shape of the CLI-written reply-state sidecar. */
|
|
41
|
+
export type LaneReplyStateFile = {
|
|
42
|
+
dispatch_event_id: string;
|
|
43
|
+
reply_committed: boolean;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=lane-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lane-key.d.ts","sourceRoot":"","sources":["../src/lane-key.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAExF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,MAAM,CAER;AAED,kEAAkE;AAClE,MAAM,MAAM,uBAAuB,GAAG;IACpC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gFAAgF;IAChF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,oDAAoD;AACpD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC"}
|
package/dist/lane-key.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical lane-key encoding — the SSOT shared by the bridge (writer of
|
|
4
|
+
* `$PRLL_CONTEXT_DIR/<lane-key>.json`) and the CLI (reader, keyed by the send
|
|
5
|
+
* target). Both sides MUST use this exact function: the lane key encodes the
|
|
6
|
+
* full lane identity `(target_uri, thread_root_id)` so a channel lane and a
|
|
7
|
+
* thread lane in the same chat never collide, and base64url keeps any
|
|
8
|
+
* target_uri (including typed `dsp:<id>` resources) filesystem-safe with no
|
|
9
|
+
* escaping ambiguity.
|
|
10
|
+
*/
|
|
11
|
+
export function laneKeyForTarget(targetUri, threadRootId) {
|
|
12
|
+
return Buffer.from(`${targetUri}\n${threadRootId ?? ''}`, 'utf8').toString('base64url');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* PRLL_CONTEXT_DIR root under a per-agent stateDir. Owned here alongside the
|
|
16
|
+
* lane-key encoding so the whole context-dir contract has one home; runtime
|
|
17
|
+
* bridges import this instead of re-declaring the path.
|
|
18
|
+
*/
|
|
19
|
+
export function dispatchLaneContextDir(stateDir) {
|
|
20
|
+
return path.join(stateDir, 'dispatch-lane-context');
|
|
21
|
+
}
|
|
22
|
+
/** Path of the bridge-owned dispatch context file for a lane. */
|
|
23
|
+
export function laneContextFilePath(contextDir, targetUri, threadRootId) {
|
|
24
|
+
return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Path of the CLI-owned reply-state sidecar for a lane. Separate file from the
|
|
28
|
+
* bridge-owned context (two writers, two files): the bridge rewrites the
|
|
29
|
+
* context file throughout the turn (step_id updates) and would clobber any
|
|
30
|
+
* CLI state stored inline.
|
|
31
|
+
*/
|
|
32
|
+
export function laneReplyStateFilePath(contextDir, targetUri, threadRootId) {
|
|
33
|
+
return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.reply-state.json`);
|
|
34
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ParallClient } from '@parall/sdk';
|
|
2
|
+
import type { GatewayLogger } from './dispatch-adapter.js';
|
|
3
|
+
import type { ParallEvent } from './types.js';
|
|
4
|
+
/** One claimed lane the bridge currently occupies. */
|
|
5
|
+
export type ActiveLane = {
|
|
6
|
+
laneKey: string;
|
|
7
|
+
lane: string;
|
|
8
|
+
targetUri: string;
|
|
9
|
+
threadRootId?: string;
|
|
10
|
+
/** source_id (message id) → WorkItem id for members folded into this lane. */
|
|
11
|
+
folded: Map<string, string>;
|
|
12
|
+
/** WorkItem id, set for typed lanes (resource = dsp:<id>, single member). */
|
|
13
|
+
typedDispatchEventId?: string;
|
|
14
|
+
/** Lease expiry (ms epoch) and TTL from the claim — renewal pacing state. */
|
|
15
|
+
leaseUntilMs?: number;
|
|
16
|
+
leaseTtlMs?: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when the server predates the dispatch ledger (claim endpoint 404s
|
|
20
|
+
* during a rolling deploy). The gateway falls back to the legacy
|
|
21
|
+
* received/ack flow for the rest of its lifetime.
|
|
22
|
+
*/
|
|
23
|
+
export declare class LedgerUnsupportedError extends Error {
|
|
24
|
+
}
|
|
25
|
+
export type LaneGroupOutcome = 'claimed' | 'foreign';
|
|
26
|
+
/**
|
|
27
|
+
* LaneLedger is the bridge-side client of the server dispatch ledger
|
|
28
|
+
* (docs/engineering-design/agent-dispatch-idempotency-design.md): it claims
|
|
29
|
+
* lane occupancy just-in-time before a message dispatch, folds mid-turn
|
|
30
|
+
* same-target messages via steer, completes (no_action sweep + release) when
|
|
31
|
+
* the local queue for a lane runs dry, and releases leftovers on shutdown.
|
|
32
|
+
*
|
|
33
|
+
* It is also the single writer of the per-lane dispatch context files under
|
|
34
|
+
* PRLL_CONTEXT_DIR (`<lane-key>.json`) that the CLI reads to derive dispatch
|
|
35
|
+
* effect keys. The CLI's reply-state sidecar lives next to it and is never
|
|
36
|
+
* touched here (two writers, two files).
|
|
37
|
+
*/
|
|
38
|
+
export declare class LaneLedger {
|
|
39
|
+
private readonly opts;
|
|
40
|
+
private readonly lanes;
|
|
41
|
+
constructor(opts: {
|
|
42
|
+
client: ParallClient;
|
|
43
|
+
orgId: string;
|
|
44
|
+
contextDir: string;
|
|
45
|
+
log?: GatewayLogger;
|
|
46
|
+
});
|
|
47
|
+
get contextDir(): string;
|
|
48
|
+
/** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
|
|
49
|
+
handles(event: ParallEvent): boolean;
|
|
50
|
+
laneKeyFor(event: ParallEvent): string;
|
|
51
|
+
getForEvent(event: ParallEvent): ActiveLane | undefined;
|
|
52
|
+
laneContextPath(lane: ActiveLane): string;
|
|
53
|
+
/**
|
|
54
|
+
* Claim (or reuse) the lane for a group of same-lane message events and
|
|
55
|
+
* fold every group member into it. Returns 'foreign' when a healthy
|
|
56
|
+
* incumbent (another pod) holds the resource — the caller must not
|
|
57
|
+
* dispatch; the events stay pending server-side and re-drive after the
|
|
58
|
+
* incumbent completes.
|
|
59
|
+
*/
|
|
60
|
+
ensureLane(events: ParallEvent[]): Promise<ActiveLane | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Fold a live mid-turn message into its active lane BEFORE injecting it
|
|
63
|
+
* into the running turn. Injection without a successful fold is forbidden —
|
|
64
|
+
* an un-folded injected message would be re-driven after complete and the
|
|
65
|
+
* model would handle it twice.
|
|
66
|
+
*/
|
|
67
|
+
steerLive(event: ParallEvent): Promise<boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* Complete the lane when no local work remains for it: the server sweeps
|
|
70
|
+
* still-leased members as no_action, releases the occupancy row, and
|
|
71
|
+
* re-drives any same-target pending work. A STALE_LANE answer means a
|
|
72
|
+
* takeover already owns the resource — local state is dropped either way.
|
|
73
|
+
*/
|
|
74
|
+
completeIfIdle(laneKey: string, hasMoreLocal: boolean): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Long-turn keepalive: renew the lane's lease on runtime activity, throttled
|
|
77
|
+
* so a chatty turn doesn't spam the server. Without this, a legitimately
|
|
78
|
+
* long turn (> lane TTL) would be dethroned mid-flight and every subsequent
|
|
79
|
+
* write misfired with STALE_LANE — the design doc's "long turns renew via
|
|
80
|
+
* step writes". Fire-and-forget: a failed renewal is surfaced by the next
|
|
81
|
+
* write's incumbency check anyway.
|
|
82
|
+
*/
|
|
83
|
+
maybeRenew(lane: ActiveLane): void;
|
|
84
|
+
/**
|
|
85
|
+
* Release a lane's unresolved members back to pending (dispatch error /
|
|
86
|
+
* shutdown) so the next pod re-claims immediately instead of waiting out
|
|
87
|
+
* the lease.
|
|
88
|
+
*/
|
|
89
|
+
release(laneKey: string): Promise<void>;
|
|
90
|
+
releaseAll(): Promise<void>;
|
|
91
|
+
/** True when any lane is currently active (used by shutdown logging). */
|
|
92
|
+
get activeCount(): number;
|
|
93
|
+
/**
|
|
94
|
+
* Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
|
|
95
|
+
* by WorkItem id or by source identity (the live task.assigned event has no
|
|
96
|
+
* WorkItem id). Returns null when a healthy incumbent (another pod) holds
|
|
97
|
+
* it or the WorkItem is already resolved — the caller must skip processing.
|
|
98
|
+
*/
|
|
99
|
+
claimTyped(ref: {
|
|
100
|
+
dispatchEventId?: string;
|
|
101
|
+
sourceType?: string;
|
|
102
|
+
sourceId?: string;
|
|
103
|
+
}): Promise<ActiveLane | null>;
|
|
104
|
+
/**
|
|
105
|
+
* Remove the per-lane context file (and its CLI sidecar) when the lane
|
|
106
|
+
* ends. A leftover file would make a later cross-context send to the same
|
|
107
|
+
* target bind a dead lane token and misfire with STALE_LANE instead of
|
|
108
|
+
* taking the plain non-ledger path.
|
|
109
|
+
*/
|
|
110
|
+
private removeLaneContext;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=lane-ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lane-ledger.d.ts","sourceRoot":"","sources":["../src/lane-ledger.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,sDAAsD;AACtD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,6EAA6E;IAC7E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;CAAG;AAEpD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,CAAC;AAqBrD;;;;;;;;;;;GAWG;AACH,qBAAa,UAAU;IAInB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;gBAGpC,IAAI,EAAE;QACrB,MAAM,EAAE,YAAY,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,aAAa,CAAC;KACrB;IAGH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,+FAA+F;IAC/F,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO;IAIpC,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM;IAOtC,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,GAAG,SAAS;IAIvD,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;IAIzC;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA4EnE;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IA0BrD;;;;;OAKG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B3E;;;;;;;OAOG;IACH,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IA6BlC;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAOjC,yEAAyE;IACzE,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;;;;OAKG;IACG,UAAU,CAAC,GAAG,EAAE;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA8B9B;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;CAW1B"}
|