@parall/agent-core 1.43.0 → 1.44.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 +25 -0
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +90 -9
- package/dist/gateway-lane-flow.d.ts +3 -1
- package/dist/gateway-lane-flow.d.ts.map +1 -1
- package/dist/gateway-lane-flow.js +43 -0
- package/dist/lane-ledger.d.ts +22 -0
- package/dist/lane-ledger.d.ts.map +1 -1
- package/dist/lane-ledger.js +36 -1
- package/package.json +2 -2
- package/src/gateway-base.ts +97 -13
- package/src/gateway-lane-flow.ts +43 -1
- package/src/lane-ledger.ts +46 -3
package/dist/gateway-base.d.ts
CHANGED
|
@@ -120,6 +120,31 @@ export declare class ParallAgentGateway {
|
|
|
120
120
|
constructor(opts: ParallGatewayOptions);
|
|
121
121
|
run(abortSignal: AbortSignal): Promise<void>;
|
|
122
122
|
private tryClaimMessage;
|
|
123
|
+
/**
|
|
124
|
+
* Lane currently being dispatched per session — lets external activity
|
|
125
|
+
* signals renew exactly the caller's lane (renewing all lanes would keep
|
|
126
|
+
* an unrelated stalled fork's lane leased forever).
|
|
127
|
+
*/
|
|
128
|
+
private sessionActiveLanes;
|
|
129
|
+
noteSessionLane(sessionKey: string, laneKey: string | null): void;
|
|
130
|
+
/**
|
|
131
|
+
* External runtime-activity signal for adapters whose tool activity does
|
|
132
|
+
* not flow through the RuntimeEvent stream (openclaw hooks call this from
|
|
133
|
+
* the tool-call lifecycle): renews the session's OWN active ledger lane so
|
|
134
|
+
* a long tool call cannot outlive the lease and get dethroned mid-turn.
|
|
135
|
+
* No-op without an active ledger lane for the session.
|
|
136
|
+
*/
|
|
137
|
+
touchRuntimeActivity(sessionKey: string): void;
|
|
138
|
+
/** Sessions whose in-flight turn surfaced a runtime error event. */
|
|
139
|
+
private turnErrorSessions;
|
|
140
|
+
/**
|
|
141
|
+
* Consume (read-and-clear) the error marker for sessionKey's last turn.
|
|
142
|
+
* Feeds complete's turn_outcome so an error turn's lane members are
|
|
143
|
+
* released for retry instead of no_action-swept (design §3). Consuming
|
|
144
|
+
* (rather than peeking) keeps one-shot fork session keys from accumulating
|
|
145
|
+
* in the set forever.
|
|
146
|
+
*/
|
|
147
|
+
consumeTurnError(sessionKey: string): boolean;
|
|
123
148
|
private emitDispatchReceived;
|
|
124
149
|
/** True when this event's lifecycle is owned by the dispatch lane ledger. */
|
|
125
150
|
private usesLaneLedger;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"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"}
|
package/dist/gateway-base.js
CHANGED
|
@@ -410,6 +410,44 @@ export class ParallAgentGateway {
|
|
|
410
410
|
this.dispatchedMessages.add(id);
|
|
411
411
|
return true;
|
|
412
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Lane currently being dispatched per session — lets external activity
|
|
415
|
+
* signals renew exactly the caller's lane (renewing all lanes would keep
|
|
416
|
+
* an unrelated stalled fork's lane leased forever).
|
|
417
|
+
*/
|
|
418
|
+
sessionActiveLanes = new Map();
|
|
419
|
+
noteSessionLane(sessionKey, laneKey) {
|
|
420
|
+
if (laneKey == null)
|
|
421
|
+
this.sessionActiveLanes.delete(sessionKey);
|
|
422
|
+
else
|
|
423
|
+
this.sessionActiveLanes.set(sessionKey, laneKey);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* External runtime-activity signal for adapters whose tool activity does
|
|
427
|
+
* not flow through the RuntimeEvent stream (openclaw hooks call this from
|
|
428
|
+
* the tool-call lifecycle): renews the session's OWN active ledger lane so
|
|
429
|
+
* a long tool call cannot outlive the lease and get dethroned mid-turn.
|
|
430
|
+
* No-op without an active ledger lane for the session.
|
|
431
|
+
*/
|
|
432
|
+
touchRuntimeActivity(sessionKey) {
|
|
433
|
+
if (this.ledgerDisabled)
|
|
434
|
+
return;
|
|
435
|
+
const laneKey = this.sessionActiveLanes.get(sessionKey);
|
|
436
|
+
if (laneKey)
|
|
437
|
+
this.laneLedger?.renewByKey(laneKey);
|
|
438
|
+
}
|
|
439
|
+
/** Sessions whose in-flight turn surfaced a runtime error event. */
|
|
440
|
+
turnErrorSessions = new Set();
|
|
441
|
+
/**
|
|
442
|
+
* Consume (read-and-clear) the error marker for sessionKey's last turn.
|
|
443
|
+
* Feeds complete's turn_outcome so an error turn's lane members are
|
|
444
|
+
* released for retry instead of no_action-swept (design §3). Consuming
|
|
445
|
+
* (rather than peeking) keeps one-shot fork session keys from accumulating
|
|
446
|
+
* in the set forever.
|
|
447
|
+
*/
|
|
448
|
+
consumeTurnError(sessionKey) {
|
|
449
|
+
return this.turnErrorSessions.delete(sessionKey);
|
|
450
|
+
}
|
|
413
451
|
async emitDispatchReceived(event) {
|
|
414
452
|
const sourceType = event.ackSourceType ?? (event.type === 'task' ? 'task_activity' : 'message');
|
|
415
453
|
const sourceId = event.ackSourceId ?? event.messageId;
|
|
@@ -814,6 +852,7 @@ export class ParallAgentGateway {
|
|
|
814
852
|
this.pendingRestartNotification = null;
|
|
815
853
|
}
|
|
816
854
|
resetDispatchMetrics(sessionKey);
|
|
855
|
+
this.turnErrorSessions.delete(sessionKey);
|
|
817
856
|
return runWithSessionKey(sessionKey, async () => {
|
|
818
857
|
let dispatchSpan = null;
|
|
819
858
|
setSessionChatId(sessionKey, event.targetId);
|
|
@@ -950,6 +989,9 @@ export class ParallAgentGateway {
|
|
|
950
989
|
pendingSendCallIds.delete(runtimeEvent.callId)) {
|
|
951
990
|
recordMessageSend(sessionKey, !runtimeEvent.error);
|
|
952
991
|
}
|
|
992
|
+
if (runtimeEvent.type === 'error') {
|
|
993
|
+
this.turnErrorSessions.add(sessionKey);
|
|
994
|
+
}
|
|
953
995
|
await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath);
|
|
954
996
|
}
|
|
955
997
|
if (!binding) {
|
|
@@ -1122,7 +1164,12 @@ export class ParallAgentGateway {
|
|
|
1122
1164
|
body: buildForkScopePrefix(last) + buildEventBody(last),
|
|
1123
1165
|
earlier,
|
|
1124
1166
|
captureText: batchText,
|
|
1125
|
-
|
|
1167
|
+
// Per-LANE residue check (parity with the main-buffer path):
|
|
1168
|
+
// the fork queue can hold several lanes (channel + thread of
|
|
1169
|
+
// the same chat). A whole-queue check would defer THIS lane's
|
|
1170
|
+
// complete behind another lane's items and never revisit it —
|
|
1171
|
+
// its members would sit received until lease expiry.
|
|
1172
|
+
hasMoreLocal: () => fork.queue.some((it) => this.dispatchGroupKey(it.event) === this.dispatchGroupKey(last)),
|
|
1126
1173
|
});
|
|
1127
1174
|
if (outcome === 'foreign') {
|
|
1128
1175
|
// Another pod owns the lane — the events stay pending server-side.
|
|
@@ -1130,6 +1177,15 @@ export class ParallAgentGateway {
|
|
|
1130
1177
|
item.resolve(false);
|
|
1131
1178
|
break;
|
|
1132
1179
|
}
|
|
1180
|
+
if (outcome === 'failed') {
|
|
1181
|
+
// Error turn: complete(error) already released the members for
|
|
1182
|
+
// redelivery. Do NOT push them to processedEvents — the redrive
|
|
1183
|
+
// must not arrive wearing an "already handled" prefix. Keep
|
|
1184
|
+
// draining; the released work rejoins the next claim.
|
|
1185
|
+
for (const item of items)
|
|
1186
|
+
item.resolve(false);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1133
1189
|
dispatched = outcome === 'dispatched';
|
|
1134
1190
|
}
|
|
1135
1191
|
else {
|
|
@@ -1168,6 +1224,10 @@ export class ParallAgentGateway {
|
|
|
1168
1224
|
}
|
|
1169
1225
|
}
|
|
1170
1226
|
finally {
|
|
1227
|
+
// One-shot fork keys are never dispatched again — drop any error marker
|
|
1228
|
+
// the legacy (non-ledger) path left unconsumed, or the set grows with
|
|
1229
|
+
// every failed ephemeral fork.
|
|
1230
|
+
this.turnErrorSessions.delete(fork.fork.sessionKey);
|
|
1171
1231
|
if (fork.deadlineTimer) {
|
|
1172
1232
|
clearTimeout(fork.deadlineTimer);
|
|
1173
1233
|
fork.deadlineTimer = null;
|
|
@@ -1295,6 +1355,13 @@ export class ParallAgentGateway {
|
|
|
1295
1355
|
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1296
1356
|
continue;
|
|
1297
1357
|
}
|
|
1358
|
+
if (outcome === 'failed') {
|
|
1359
|
+
// Error turn settled with complete(error) — members released for
|
|
1360
|
+
// redelivery. The fork results were not consumed by the failed
|
|
1361
|
+
// turn; keep them for the next real dispatch.
|
|
1362
|
+
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1298
1365
|
// Dispatched — resolution happened server-side (reply cover or
|
|
1299
1366
|
// no_action sweep); no legacy acks.
|
|
1300
1367
|
continue;
|
|
@@ -1438,9 +1505,16 @@ export class ParallAgentGateway {
|
|
|
1438
1505
|
// fold leaves the event buffered — the drain claims it as its own
|
|
1439
1506
|
// turn. Injection requires an exact lane match (same chat AND same
|
|
1440
1507
|
// thread) — a thread message never rides a channel turn.
|
|
1508
|
+
// The adapter-capability check comes BEFORE the fold: on a
|
|
1509
|
+
// buffer-only adapter (openclaw — no enqueueDuringDispatch) a
|
|
1510
|
+
// folded-but-never-injected member would be broad-covered by the
|
|
1511
|
+
// current turn's reply without the model ever seeing it. Leaving
|
|
1512
|
+
// the event un-folded keeps it buffered; the drain claims it as
|
|
1513
|
+
// its own turn and folds it there.
|
|
1441
1514
|
if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) &&
|
|
1515
|
+
this.opts.dispatchAdapter.enqueueDuringDispatch != null &&
|
|
1442
1516
|
(await this.laneLedger?.steerLive(event)) &&
|
|
1443
|
-
(await this.opts.dispatchAdapter.enqueueDuringDispatch
|
|
1517
|
+
(await this.opts.dispatchAdapter.enqueueDuringDispatch(this.opts.runtimeKey, buildEventBody(event)))) {
|
|
1444
1518
|
this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
|
|
1445
1519
|
}
|
|
1446
1520
|
}
|
|
@@ -1474,13 +1548,20 @@ export class ParallAgentGateway {
|
|
|
1474
1548
|
this.dispatchState.mainBuffer.push(event);
|
|
1475
1549
|
return false;
|
|
1476
1550
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1551
|
+
// Ledger-managed events must never touch the legacy received surface
|
|
1552
|
+
// — the fork's lane claim marks them received. A mark-received here
|
|
1553
|
+
// outruns the claim, strands the row ownerless, and the chat goes
|
|
1554
|
+
// silent (the 0713 black hole; dispatch-convergence-design.md §6).
|
|
1555
|
+
// This was the only unguarded call site of the three.
|
|
1556
|
+
if (!this.usesLaneLedger(event)) {
|
|
1557
|
+
try {
|
|
1558
|
+
await this.emitDispatchReceived(event);
|
|
1559
|
+
}
|
|
1560
|
+
catch (err) {
|
|
1561
|
+
this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
|
|
1562
|
+
this.dispatchState.mainBuffer.push(event);
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1484
1565
|
}
|
|
1485
1566
|
const fork = await this.opts.dispatchAdapter.forkSession({
|
|
1486
1567
|
sessionKey: this.opts.runtimeKey,
|
|
@@ -39,6 +39,8 @@ export interface LaneFlowHost {
|
|
|
39
39
|
disableLedger(reason: string): void;
|
|
40
40
|
usesLaneLedger(event: ParallEvent): boolean;
|
|
41
41
|
emitDispatchReceived(event: ParallEvent): Promise<void>;
|
|
42
|
+
consumeTurnError(sessionKey: string): boolean;
|
|
43
|
+
noteSessionLane(sessionKey: string, laneKey: string | null): void;
|
|
42
44
|
runDispatch(event: ParallEvent, sessionKey: string, bodyForAgent: string, earlierEvents?: ParallEvent[], captureText?: string[]): Promise<boolean>;
|
|
43
45
|
tryClaimMessage(id: string): boolean;
|
|
44
46
|
buildMessageDispatchDecision(chatId: string, message: DispatchableMessage): Promise<MessageDispatchDecision>;
|
|
@@ -57,7 +59,7 @@ export declare function dispatchLaneGroup(host: LaneFlowHost, opts: {
|
|
|
57
59
|
earlier: ParallEvent[];
|
|
58
60
|
captureText?: string[];
|
|
59
61
|
hasMoreLocal: () => boolean;
|
|
60
|
-
}): Promise<'dispatched' | 'foreign' | 'shutdown'>;
|
|
62
|
+
}): Promise<'dispatched' | 'foreign' | 'shutdown' | 'failed'>;
|
|
61
63
|
/**
|
|
62
64
|
* Consume one typed dispatch (task/comment/schedule/trigger/approval) under
|
|
63
65
|
* its typed-lane occupancy guard: claim the dsp:<id> lane (skip when another
|
|
@@ -1 +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;;;;;;;OAOG;IACH,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,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,
|
|
1
|
+
{"version":3,"file":"gateway-lane-flow.d.ts","sourceRoot":"","sources":["../src/gateway-lane-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -40,6 +40,9 @@ export async function dispatchLaneGroup(host, opts) {
|
|
|
40
40
|
}
|
|
41
41
|
return 'foreign';
|
|
42
42
|
}
|
|
43
|
+
// Register the session's active lane so external activity signals
|
|
44
|
+
// (touchRuntimeActivity from adapter hooks) renew exactly this lane.
|
|
45
|
+
host.noteSessionLane(opts.sessionKey, lane.laneKey);
|
|
43
46
|
let dispatched = false;
|
|
44
47
|
try {
|
|
45
48
|
dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
|
|
@@ -47,13 +50,53 @@ export async function dispatchLaneGroup(host, opts) {
|
|
|
47
50
|
catch (err) {
|
|
48
51
|
// Failed turn: hand the members back so the retry (this pod or the
|
|
49
52
|
// next) re-claims immediately instead of waiting out the lease.
|
|
53
|
+
host.noteSessionLane(opts.sessionKey, null);
|
|
50
54
|
await ledger.release(lane.laneKey).catch(() => { });
|
|
51
55
|
throw err;
|
|
52
56
|
}
|
|
57
|
+
finally {
|
|
58
|
+
if (dispatched)
|
|
59
|
+
host.noteSessionLane(opts.sessionKey, null);
|
|
60
|
+
}
|
|
53
61
|
if (!dispatched) {
|
|
54
62
|
// Shutdown short-circuit — shutdown() releases all active lanes.
|
|
55
63
|
return 'shutdown';
|
|
56
64
|
}
|
|
65
|
+
if (host.consumeTurnError(opts.sessionKey)) {
|
|
66
|
+
// An error turn must not no_action-sweep its members — settle the lane
|
|
67
|
+
// NOW with an error complete so they release for retry on the redrive
|
|
68
|
+
// budget (dispatch-convergence-design.md §3). Settling immediately (even
|
|
69
|
+
// with same-lane work still buffered) is deliberate: carrying the error
|
|
70
|
+
// across buffered turns would let a later reply broad-cover the failed
|
|
71
|
+
// member, or requeue the later turn's successful work. Released members
|
|
72
|
+
// rejoin the next claim, merged with whatever was buffered. Dropping the
|
|
73
|
+
// local dedupe claims lets the server's re-drive hint retrigger the
|
|
74
|
+
// messages immediately instead of waiting out the renotify pacing.
|
|
75
|
+
ledger.markTurnError(lane.laneKey);
|
|
76
|
+
// Clear dedupe for EVERY lane member, not just this batch: an earlier
|
|
77
|
+
// successful batch may have deferred its complete via hasMoreLocal, so
|
|
78
|
+
// the error complete below releases those members too — their redrive
|
|
79
|
+
// would be permanently blocked by a stale local claim.
|
|
80
|
+
for (const msgId of lane.folded.keys()) {
|
|
81
|
+
host.dispatchedMessages.delete(msgId);
|
|
82
|
+
}
|
|
83
|
+
// Invalidate any armed steer state before settling: a pending injection
|
|
84
|
+
// left over from the failed turn would make the retry claim enter the
|
|
85
|
+
// "already injected" path, produce an empty turn, and no_action-sweep the
|
|
86
|
+
// released members without ever retrying them. abortDispatch is the
|
|
87
|
+
// adapter contract's idempotent "clear pending steer state" hook.
|
|
88
|
+
try {
|
|
89
|
+
host.opts.dispatchAdapter.abortDispatch?.(opts.sessionKey);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// best-effort — a throwing abort must not block the error settlement
|
|
93
|
+
}
|
|
94
|
+
await ledger.completeIfIdle(lane.laneKey, false);
|
|
95
|
+
// 'failed' — callers must NOT record these events as handled: the server
|
|
96
|
+
// just released them for redelivery, and an "already handled" fork prefix
|
|
97
|
+
// (or a consumed fork summary) on the redrive would be a lie.
|
|
98
|
+
return 'failed';
|
|
99
|
+
}
|
|
57
100
|
const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
|
|
58
101
|
await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
|
|
59
102
|
return 'dispatched';
|
package/dist/lane-ledger.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export type ActiveLane = {
|
|
|
14
14
|
/** Lease expiry (ms epoch) and TTL from the claim — renewal pacing state. */
|
|
15
15
|
leaseUntilMs?: number;
|
|
16
16
|
leaseTtlMs?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Sticky error bit: some turn on this lane surfaced a runtime error. Read
|
|
19
|
+
* (and only cleared) by the lane's final complete, so an error outcome
|
|
20
|
+
* survives buffered same-lane turns in between — a later successful turn
|
|
21
|
+
* must not no_action-sweep the failed turn's members.
|
|
22
|
+
*/
|
|
23
|
+
turnError?: boolean;
|
|
17
24
|
};
|
|
18
25
|
/**
|
|
19
26
|
* Thrown when the server predates the dispatch ledger (claim endpoint 404s
|
|
@@ -71,7 +78,22 @@ export declare class LaneLedger {
|
|
|
71
78
|
* re-drives any same-target pending work. A STALE_LANE answer means a
|
|
72
79
|
* takeover already owns the resource — local state is dropped either way.
|
|
73
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Record that the turn on this lane surfaced a runtime error. The flow
|
|
83
|
+
* settles an errored lane immediately (dispatchLaneGroup returns 'failed'
|
|
84
|
+
* after a forced complete), so the bit normally lives for one turn only —
|
|
85
|
+
* it is the transport between the gateway's per-session error signal and
|
|
86
|
+
* this lane's complete request.
|
|
87
|
+
*/
|
|
88
|
+
markTurnError(laneKey: string): void;
|
|
74
89
|
completeIfIdle(laneKey: string, hasMoreLocal: boolean): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Renew one lane by its key — the external runtime-activity hook for
|
|
92
|
+
* adapters whose tool traffic bypasses the RuntimeEvent stream (openclaw
|
|
93
|
+
* hooks). Scoped to the session's own lane: renewing every lane would let
|
|
94
|
+
* one busy fork keep an unrelated stalled fork's lane leased forever.
|
|
95
|
+
*/
|
|
96
|
+
renewByKey(laneKey: string): void;
|
|
75
97
|
/**
|
|
76
98
|
* Long-turn keepalive: renew the lane's lease on runtime activity, throttled
|
|
77
99
|
* so a chatty turn doesn't spam the server. Without this, a legitimately
|
|
@@ -1 +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;
|
|
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;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,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;IAsFnE;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IA0BrD;;;;;OAKG;IACH;;;;;;OAMG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK9B,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B3E;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKjC;;;;;;;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"}
|
package/dist/lane-ledger.js
CHANGED
|
@@ -88,7 +88,16 @@ export class LaneLedger {
|
|
|
88
88
|
throw err;
|
|
89
89
|
}
|
|
90
90
|
if (!res.claimed || !res.lane) {
|
|
91
|
-
|
|
91
|
+
if (res.reason === 'empty') {
|
|
92
|
+
// Nothing foldable: resolved elsewhere, or stranded outside the
|
|
93
|
+
// ledger (the reconciler reclaims strays past the received TTL).
|
|
94
|
+
// Post-convergence this should not happen on a message lane — warn
|
|
95
|
+
// so a regression surfaces (design §3).
|
|
96
|
+
this.opts.log?.warn(`claim for ${targetUri} came back empty — nothing foldable; leaving to the reconciler`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent — leaving events pending for re-drive`);
|
|
100
|
+
}
|
|
92
101
|
return null;
|
|
93
102
|
}
|
|
94
103
|
const leaseUntilMs = Date.parse(res.lease_until ?? '');
|
|
@@ -183,6 +192,18 @@ export class LaneLedger {
|
|
|
183
192
|
* re-drives any same-target pending work. A STALE_LANE answer means a
|
|
184
193
|
* takeover already owns the resource — local state is dropped either way.
|
|
185
194
|
*/
|
|
195
|
+
/**
|
|
196
|
+
* Record that the turn on this lane surfaced a runtime error. The flow
|
|
197
|
+
* settles an errored lane immediately (dispatchLaneGroup returns 'failed'
|
|
198
|
+
* after a forced complete), so the bit normally lives for one turn only —
|
|
199
|
+
* it is the transport between the gateway's per-session error signal and
|
|
200
|
+
* this lane's complete request.
|
|
201
|
+
*/
|
|
202
|
+
markTurnError(laneKey) {
|
|
203
|
+
const lane = this.lanes.get(laneKey);
|
|
204
|
+
if (lane)
|
|
205
|
+
lane.turnError = true;
|
|
206
|
+
}
|
|
186
207
|
async completeIfIdle(laneKey, hasMoreLocal) {
|
|
187
208
|
const lane = this.lanes.get(laneKey);
|
|
188
209
|
if (!lane || hasMoreLocal)
|
|
@@ -194,6 +215,9 @@ export class LaneLedger {
|
|
|
194
215
|
lane: lane.lane,
|
|
195
216
|
target_uri: lane.targetUri,
|
|
196
217
|
thread_root_id: lane.threadRootId,
|
|
218
|
+
// An error turn releases its members for retry instead of sweeping
|
|
219
|
+
// them as handled (ignored by older servers).
|
|
220
|
+
turn_outcome: lane.turnError ? 'error' : 'ok',
|
|
197
221
|
});
|
|
198
222
|
if (res.swept_no_action > 0 || res.redriven) {
|
|
199
223
|
this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
|
|
@@ -208,6 +232,17 @@ export class LaneLedger {
|
|
|
208
232
|
this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
|
|
209
233
|
}
|
|
210
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Renew one lane by its key — the external runtime-activity hook for
|
|
237
|
+
* adapters whose tool traffic bypasses the RuntimeEvent stream (openclaw
|
|
238
|
+
* hooks). Scoped to the session's own lane: renewing every lane would let
|
|
239
|
+
* one busy fork keep an unrelated stalled fork's lane leased forever.
|
|
240
|
+
*/
|
|
241
|
+
renewByKey(laneKey) {
|
|
242
|
+
const lane = this.lanes.get(laneKey);
|
|
243
|
+
if (lane)
|
|
244
|
+
this.maybeRenew(lane);
|
|
245
|
+
}
|
|
211
246
|
/**
|
|
212
247
|
* Long-turn keepalive: renew the lane's lease on runtime activity, throttled
|
|
213
248
|
* so a chatty turn doesn't spam the server. Without this, a legitimately
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/agent-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "Shared agent runtime orchestration helpers for Parall",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@opentelemetry/sdk-logs": "^0.57.0",
|
|
36
36
|
"@opentelemetry/sdk-metrics": "^1.30.0",
|
|
37
37
|
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
38
|
-
"@parall/sdk": "1.
|
|
38
|
+
"@parall/sdk": "1.44.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^22.0.0",
|
package/src/gateway-base.ts
CHANGED
|
@@ -641,6 +641,45 @@ export class ParallAgentGateway {
|
|
|
641
641
|
return true;
|
|
642
642
|
}
|
|
643
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Lane currently being dispatched per session — lets external activity
|
|
646
|
+
* signals renew exactly the caller's lane (renewing all lanes would keep
|
|
647
|
+
* an unrelated stalled fork's lane leased forever).
|
|
648
|
+
*/
|
|
649
|
+
private sessionActiveLanes = new Map<string, string>();
|
|
650
|
+
|
|
651
|
+
noteSessionLane(sessionKey: string, laneKey: string | null): void {
|
|
652
|
+
if (laneKey == null) this.sessionActiveLanes.delete(sessionKey);
|
|
653
|
+
else this.sessionActiveLanes.set(sessionKey, laneKey);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* External runtime-activity signal for adapters whose tool activity does
|
|
658
|
+
* not flow through the RuntimeEvent stream (openclaw hooks call this from
|
|
659
|
+
* the tool-call lifecycle): renews the session's OWN active ledger lane so
|
|
660
|
+
* a long tool call cannot outlive the lease and get dethroned mid-turn.
|
|
661
|
+
* No-op without an active ledger lane for the session.
|
|
662
|
+
*/
|
|
663
|
+
touchRuntimeActivity(sessionKey: string): void {
|
|
664
|
+
if (this.ledgerDisabled) return;
|
|
665
|
+
const laneKey = this.sessionActiveLanes.get(sessionKey);
|
|
666
|
+
if (laneKey) this.laneLedger?.renewByKey(laneKey);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Sessions whose in-flight turn surfaced a runtime error event. */
|
|
670
|
+
private turnErrorSessions = new Set<string>();
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Consume (read-and-clear) the error marker for sessionKey's last turn.
|
|
674
|
+
* Feeds complete's turn_outcome so an error turn's lane members are
|
|
675
|
+
* released for retry instead of no_action-swept (design §3). Consuming
|
|
676
|
+
* (rather than peeking) keeps one-shot fork session keys from accumulating
|
|
677
|
+
* in the set forever.
|
|
678
|
+
*/
|
|
679
|
+
consumeTurnError(sessionKey: string): boolean {
|
|
680
|
+
return this.turnErrorSessions.delete(sessionKey);
|
|
681
|
+
}
|
|
682
|
+
|
|
644
683
|
private async emitDispatchReceived(event: ParallEvent): Promise<void> {
|
|
645
684
|
const sourceType = event.ackSourceType ?? (event.type === 'task' ? 'task_activity' : 'message');
|
|
646
685
|
const sourceId = event.ackSourceId ?? event.messageId;
|
|
@@ -693,7 +732,7 @@ export class ParallAgentGateway {
|
|
|
693
732
|
earlier: ParallEvent[];
|
|
694
733
|
captureText?: string[];
|
|
695
734
|
hasMoreLocal: () => boolean;
|
|
696
|
-
}): Promise<'dispatched' | 'foreign' | 'shutdown'> {
|
|
735
|
+
}): Promise<'dispatched' | 'foreign' | 'shutdown' | 'failed'> {
|
|
697
736
|
return dispatchLaneGroup(this.laneFlowHost(), opts);
|
|
698
737
|
}
|
|
699
738
|
|
|
@@ -1137,6 +1176,7 @@ export class ParallAgentGateway {
|
|
|
1137
1176
|
}
|
|
1138
1177
|
|
|
1139
1178
|
resetDispatchMetrics(sessionKey);
|
|
1179
|
+
this.turnErrorSessions.delete(sessionKey);
|
|
1140
1180
|
return runWithSessionKey(sessionKey, async () => {
|
|
1141
1181
|
let dispatchSpan: ReturnType<typeof startDispatchSpan> = null;
|
|
1142
1182
|
|
|
@@ -1299,6 +1339,9 @@ export class ParallAgentGateway {
|
|
|
1299
1339
|
) {
|
|
1300
1340
|
recordMessageSend(sessionKey, !runtimeEvent.error);
|
|
1301
1341
|
}
|
|
1342
|
+
if (runtimeEvent.type === 'error') {
|
|
1343
|
+
this.turnErrorSessions.add(sessionKey);
|
|
1344
|
+
}
|
|
1302
1345
|
await this.createRuntimeStep(
|
|
1303
1346
|
binding.agentSessionId,
|
|
1304
1347
|
event,
|
|
@@ -1495,13 +1538,29 @@ export class ParallAgentGateway {
|
|
|
1495
1538
|
body: buildForkScopePrefix(last) + buildEventBody(last),
|
|
1496
1539
|
earlier,
|
|
1497
1540
|
captureText: batchText,
|
|
1498
|
-
|
|
1541
|
+
// Per-LANE residue check (parity with the main-buffer path):
|
|
1542
|
+
// the fork queue can hold several lanes (channel + thread of
|
|
1543
|
+
// the same chat). A whole-queue check would defer THIS lane's
|
|
1544
|
+
// complete behind another lane's items and never revisit it —
|
|
1545
|
+
// its members would sit received until lease expiry.
|
|
1546
|
+
hasMoreLocal: () =>
|
|
1547
|
+
fork.queue.some(
|
|
1548
|
+
(it) => this.dispatchGroupKey(it.event) === this.dispatchGroupKey(last),
|
|
1549
|
+
),
|
|
1499
1550
|
});
|
|
1500
1551
|
if (outcome === 'foreign') {
|
|
1501
1552
|
// Another pod owns the lane — the events stay pending server-side.
|
|
1502
1553
|
for (const item of items) item.resolve(false);
|
|
1503
1554
|
break;
|
|
1504
1555
|
}
|
|
1556
|
+
if (outcome === 'failed') {
|
|
1557
|
+
// Error turn: complete(error) already released the members for
|
|
1558
|
+
// redelivery. Do NOT push them to processedEvents — the redrive
|
|
1559
|
+
// must not arrive wearing an "already handled" prefix. Keep
|
|
1560
|
+
// draining; the released work rejoins the next claim.
|
|
1561
|
+
for (const item of items) item.resolve(false);
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1505
1564
|
dispatched = outcome === 'dispatched';
|
|
1506
1565
|
} else {
|
|
1507
1566
|
dispatched = await this.runDispatch(
|
|
@@ -1542,6 +1601,10 @@ export class ParallAgentGateway {
|
|
|
1542
1601
|
remaining.resolve(false);
|
|
1543
1602
|
}
|
|
1544
1603
|
} finally {
|
|
1604
|
+
// One-shot fork keys are never dispatched again — drop any error marker
|
|
1605
|
+
// the legacy (non-ledger) path left unconsumed, or the set grows with
|
|
1606
|
+
// every failed ephemeral fork.
|
|
1607
|
+
this.turnErrorSessions.delete(fork.fork.sessionKey);
|
|
1545
1608
|
if (fork.deadlineTimer) {
|
|
1546
1609
|
clearTimeout(fork.deadlineTimer);
|
|
1547
1610
|
fork.deadlineTimer = null;
|
|
@@ -1654,7 +1717,7 @@ export class ParallAgentGateway {
|
|
|
1654
1717
|
this.opts.runtimeKey,
|
|
1655
1718
|
);
|
|
1656
1719
|
if (this.usesLaneLedger(event)) {
|
|
1657
|
-
let outcome: 'dispatched' | 'foreign' | 'shutdown';
|
|
1720
|
+
let outcome: 'dispatched' | 'foreign' | 'shutdown' | 'failed';
|
|
1658
1721
|
try {
|
|
1659
1722
|
outcome = await this.dispatchLaneGroup({
|
|
1660
1723
|
events,
|
|
@@ -1681,6 +1744,13 @@ export class ParallAgentGateway {
|
|
|
1681
1744
|
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1682
1745
|
continue;
|
|
1683
1746
|
}
|
|
1747
|
+
if (outcome === 'failed') {
|
|
1748
|
+
// Error turn settled with complete(error) — members released for
|
|
1749
|
+
// redelivery. The fork results were not consumed by the failed
|
|
1750
|
+
// turn; keep them for the next real dispatch.
|
|
1751
|
+
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1684
1754
|
// Dispatched — resolution happened server-side (reply cover or
|
|
1685
1755
|
// no_action sweep); no legacy acks.
|
|
1686
1756
|
continue;
|
|
@@ -1769,7 +1839,7 @@ export class ParallAgentGateway {
|
|
|
1769
1839
|
if (this.usesLaneLedger(event)) {
|
|
1770
1840
|
// Ledger flow: claim replaces mark-received; complete/reply replace
|
|
1771
1841
|
// acks. A foreign incumbent leaves the event pending for re-drive.
|
|
1772
|
-
let outcome: 'dispatched' | 'foreign' | 'shutdown' = 'shutdown';
|
|
1842
|
+
let outcome: 'dispatched' | 'foreign' | 'shutdown' | 'failed' = 'shutdown';
|
|
1773
1843
|
try {
|
|
1774
1844
|
try {
|
|
1775
1845
|
outcome = await this.dispatchLaneGroup({
|
|
@@ -1840,10 +1910,17 @@ export class ParallAgentGateway {
|
|
|
1840
1910
|
// fold leaves the event buffered — the drain claims it as its own
|
|
1841
1911
|
// turn. Injection requires an exact lane match (same chat AND same
|
|
1842
1912
|
// thread) — a thread message never rides a channel turn.
|
|
1913
|
+
// The adapter-capability check comes BEFORE the fold: on a
|
|
1914
|
+
// buffer-only adapter (openclaw — no enqueueDuringDispatch) a
|
|
1915
|
+
// folded-but-never-injected member would be broad-covered by the
|
|
1916
|
+
// current turn's reply without the model ever seeing it. Leaving
|
|
1917
|
+
// the event un-folded keeps it buffered; the drain claims it as
|
|
1918
|
+
// its own turn and folds it there.
|
|
1843
1919
|
if (
|
|
1844
1920
|
this.mainCurrentGroupKey === this.dispatchGroupKey(event) &&
|
|
1921
|
+
this.opts.dispatchAdapter.enqueueDuringDispatch != null &&
|
|
1845
1922
|
(await this.laneLedger?.steerLive(event)) &&
|
|
1846
|
-
(await this.opts.dispatchAdapter.enqueueDuringDispatch
|
|
1923
|
+
(await this.opts.dispatchAdapter.enqueueDuringDispatch(
|
|
1847
1924
|
this.opts.runtimeKey,
|
|
1848
1925
|
buildEventBody(event),
|
|
1849
1926
|
))
|
|
@@ -1892,14 +1969,21 @@ export class ParallAgentGateway {
|
|
|
1892
1969
|
return false;
|
|
1893
1970
|
}
|
|
1894
1971
|
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1972
|
+
// Ledger-managed events must never touch the legacy received surface
|
|
1973
|
+
// — the fork's lane claim marks them received. A mark-received here
|
|
1974
|
+
// outruns the claim, strands the row ownerless, and the chat goes
|
|
1975
|
+
// silent (the 0713 black hole; dispatch-convergence-design.md §6).
|
|
1976
|
+
// This was the only unguarded call site of the three.
|
|
1977
|
+
if (!this.usesLaneLedger(event)) {
|
|
1978
|
+
try {
|
|
1979
|
+
await this.emitDispatchReceived(event);
|
|
1980
|
+
} catch (err) {
|
|
1981
|
+
this.opts.log?.warn?.(
|
|
1982
|
+
`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`,
|
|
1983
|
+
);
|
|
1984
|
+
this.dispatchState.mainBuffer.push(event);
|
|
1985
|
+
return false;
|
|
1986
|
+
}
|
|
1903
1987
|
}
|
|
1904
1988
|
const fork = await this.opts.dispatchAdapter.forkSession({
|
|
1905
1989
|
sessionKey: this.opts.runtimeKey,
|
package/src/gateway-lane-flow.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface LaneFlowHost {
|
|
|
37
37
|
disableLedger(reason: string): void;
|
|
38
38
|
usesLaneLedger(event: ParallEvent): boolean;
|
|
39
39
|
emitDispatchReceived(event: ParallEvent): Promise<void>;
|
|
40
|
+
consumeTurnError(sessionKey: string): boolean;
|
|
41
|
+
noteSessionLane(sessionKey: string, laneKey: string | null): void;
|
|
40
42
|
runDispatch(
|
|
41
43
|
event: ParallEvent,
|
|
42
44
|
sessionKey: string,
|
|
@@ -68,7 +70,7 @@ export async function dispatchLaneGroup(
|
|
|
68
70
|
captureText?: string[];
|
|
69
71
|
hasMoreLocal: () => boolean;
|
|
70
72
|
},
|
|
71
|
-
): Promise<'dispatched' | 'foreign' | 'shutdown'> {
|
|
73
|
+
): Promise<'dispatched' | 'foreign' | 'shutdown' | 'failed'> {
|
|
72
74
|
const ledger = host.laneLedger!;
|
|
73
75
|
const event = opts.events[opts.events.length - 1];
|
|
74
76
|
let lane: Awaited<ReturnType<LaneLedger['ensureLane']>>;
|
|
@@ -106,6 +108,9 @@ export async function dispatchLaneGroup(
|
|
|
106
108
|
}
|
|
107
109
|
return 'foreign';
|
|
108
110
|
}
|
|
111
|
+
// Register the session's active lane so external activity signals
|
|
112
|
+
// (touchRuntimeActivity from adapter hooks) renew exactly this lane.
|
|
113
|
+
host.noteSessionLane(opts.sessionKey, lane.laneKey);
|
|
109
114
|
let dispatched = false;
|
|
110
115
|
try {
|
|
111
116
|
dispatched = await host.runDispatch(
|
|
@@ -118,13 +123,50 @@ export async function dispatchLaneGroup(
|
|
|
118
123
|
} catch (err) {
|
|
119
124
|
// Failed turn: hand the members back so the retry (this pod or the
|
|
120
125
|
// next) re-claims immediately instead of waiting out the lease.
|
|
126
|
+
host.noteSessionLane(opts.sessionKey, null);
|
|
121
127
|
await ledger.release(lane.laneKey).catch(() => {});
|
|
122
128
|
throw err;
|
|
129
|
+
} finally {
|
|
130
|
+
if (dispatched) host.noteSessionLane(opts.sessionKey, null);
|
|
123
131
|
}
|
|
124
132
|
if (!dispatched) {
|
|
125
133
|
// Shutdown short-circuit — shutdown() releases all active lanes.
|
|
126
134
|
return 'shutdown';
|
|
127
135
|
}
|
|
136
|
+
if (host.consumeTurnError(opts.sessionKey)) {
|
|
137
|
+
// An error turn must not no_action-sweep its members — settle the lane
|
|
138
|
+
// NOW with an error complete so they release for retry on the redrive
|
|
139
|
+
// budget (dispatch-convergence-design.md §3). Settling immediately (even
|
|
140
|
+
// with same-lane work still buffered) is deliberate: carrying the error
|
|
141
|
+
// across buffered turns would let a later reply broad-cover the failed
|
|
142
|
+
// member, or requeue the later turn's successful work. Released members
|
|
143
|
+
// rejoin the next claim, merged with whatever was buffered. Dropping the
|
|
144
|
+
// local dedupe claims lets the server's re-drive hint retrigger the
|
|
145
|
+
// messages immediately instead of waiting out the renotify pacing.
|
|
146
|
+
ledger.markTurnError(lane.laneKey);
|
|
147
|
+
// Clear dedupe for EVERY lane member, not just this batch: an earlier
|
|
148
|
+
// successful batch may have deferred its complete via hasMoreLocal, so
|
|
149
|
+
// the error complete below releases those members too — their redrive
|
|
150
|
+
// would be permanently blocked by a stale local claim.
|
|
151
|
+
for (const msgId of lane.folded.keys()) {
|
|
152
|
+
host.dispatchedMessages.delete(msgId);
|
|
153
|
+
}
|
|
154
|
+
// Invalidate any armed steer state before settling: a pending injection
|
|
155
|
+
// left over from the failed turn would make the retry claim enter the
|
|
156
|
+
// "already injected" path, produce an empty turn, and no_action-sweep the
|
|
157
|
+
// released members without ever retrying them. abortDispatch is the
|
|
158
|
+
// adapter contract's idempotent "clear pending steer state" hook.
|
|
159
|
+
try {
|
|
160
|
+
host.opts.dispatchAdapter.abortDispatch?.(opts.sessionKey);
|
|
161
|
+
} catch {
|
|
162
|
+
// best-effort — a throwing abort must not block the error settlement
|
|
163
|
+
}
|
|
164
|
+
await ledger.completeIfIdle(lane.laneKey, false);
|
|
165
|
+
// 'failed' — callers must NOT record these events as handled: the server
|
|
166
|
+
// just released them for redelivery, and an "already handled" fork prefix
|
|
167
|
+
// (or a consumed fork summary) on the redrive would be a lie.
|
|
168
|
+
return 'failed';
|
|
169
|
+
}
|
|
128
170
|
const pendingInjections =
|
|
129
171
|
host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
|
|
130
172
|
await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
|
package/src/lane-ledger.ts
CHANGED
|
@@ -19,6 +19,13 @@ export type ActiveLane = {
|
|
|
19
19
|
/** Lease expiry (ms epoch) and TTL from the claim — renewal pacing state. */
|
|
20
20
|
leaseUntilMs?: number;
|
|
21
21
|
leaseTtlMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Sticky error bit: some turn on this lane surfaced a runtime error. Read
|
|
24
|
+
* (and only cleared) by the lane's final complete, so an error outcome
|
|
25
|
+
* survives buffered same-lane turns in between — a later successful turn
|
|
26
|
+
* must not no_action-sweep the failed turn's members.
|
|
27
|
+
*/
|
|
28
|
+
turnError?: boolean;
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
/**
|
|
@@ -122,9 +129,19 @@ export class LaneLedger {
|
|
|
122
129
|
throw err;
|
|
123
130
|
}
|
|
124
131
|
if (!res.claimed || !res.lane) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
if (res.reason === 'empty') {
|
|
133
|
+
// Nothing foldable: resolved elsewhere, or stranded outside the
|
|
134
|
+
// ledger (the reconciler reclaims strays past the received TTL).
|
|
135
|
+
// Post-convergence this should not happen on a message lane — warn
|
|
136
|
+
// so a regression surfaces (design §3).
|
|
137
|
+
this.opts.log?.warn(
|
|
138
|
+
`claim for ${targetUri} came back empty — nothing foldable; leaving to the reconciler`,
|
|
139
|
+
);
|
|
140
|
+
} else {
|
|
141
|
+
this.opts.log?.info(
|
|
142
|
+
`lane for ${targetUri} held by a healthy incumbent — leaving events pending for re-drive`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
128
145
|
return null;
|
|
129
146
|
}
|
|
130
147
|
const leaseUntilMs = Date.parse(res.lease_until ?? '');
|
|
@@ -218,6 +235,18 @@ export class LaneLedger {
|
|
|
218
235
|
* re-drives any same-target pending work. A STALE_LANE answer means a
|
|
219
236
|
* takeover already owns the resource — local state is dropped either way.
|
|
220
237
|
*/
|
|
238
|
+
/**
|
|
239
|
+
* Record that the turn on this lane surfaced a runtime error. The flow
|
|
240
|
+
* settles an errored lane immediately (dispatchLaneGroup returns 'failed'
|
|
241
|
+
* after a forced complete), so the bit normally lives for one turn only —
|
|
242
|
+
* it is the transport between the gateway's per-session error signal and
|
|
243
|
+
* this lane's complete request.
|
|
244
|
+
*/
|
|
245
|
+
markTurnError(laneKey: string): void {
|
|
246
|
+
const lane = this.lanes.get(laneKey);
|
|
247
|
+
if (lane) lane.turnError = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
221
250
|
async completeIfIdle(laneKey: string, hasMoreLocal: boolean): Promise<void> {
|
|
222
251
|
const lane = this.lanes.get(laneKey);
|
|
223
252
|
if (!lane || hasMoreLocal) return;
|
|
@@ -228,6 +257,9 @@ export class LaneLedger {
|
|
|
228
257
|
lane: lane.lane,
|
|
229
258
|
target_uri: lane.targetUri,
|
|
230
259
|
thread_root_id: lane.threadRootId,
|
|
260
|
+
// An error turn releases its members for retry instead of sweeping
|
|
261
|
+
// them as handled (ignored by older servers).
|
|
262
|
+
turn_outcome: lane.turnError ? 'error' : 'ok',
|
|
231
263
|
});
|
|
232
264
|
if (res.swept_no_action > 0 || res.redriven) {
|
|
233
265
|
this.opts.log?.info(
|
|
@@ -244,6 +276,17 @@ export class LaneLedger {
|
|
|
244
276
|
}
|
|
245
277
|
}
|
|
246
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Renew one lane by its key — the external runtime-activity hook for
|
|
281
|
+
* adapters whose tool traffic bypasses the RuntimeEvent stream (openclaw
|
|
282
|
+
* hooks). Scoped to the session's own lane: renewing every lane would let
|
|
283
|
+
* one busy fork keep an unrelated stalled fork's lane leased forever.
|
|
284
|
+
*/
|
|
285
|
+
renewByKey(laneKey: string): void {
|
|
286
|
+
const lane = this.lanes.get(laneKey);
|
|
287
|
+
if (lane) this.maybeRenew(lane);
|
|
288
|
+
}
|
|
289
|
+
|
|
247
290
|
/**
|
|
248
291
|
* Long-turn keepalive: renew the lane's lease on runtime activity, throttled
|
|
249
292
|
* so a chatty turn doesn't spam the server. Without this, a legitimately
|