@parall/parall 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.d.ts.map +1 -1
- package/dist/gateway.js +28 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +58 -0
- package/dist/index.bundle.mjs +185 -12
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/gateway.ts +31 -0
- package/src/hooks.ts +60 -0
- package/src/runtime.ts +17 -0
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,uGAAuG;AACvG,KAAK,qBAAqB,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACnF,OAAO,
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,uGAAuG;AACvG,KAAK,qBAAqB,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACnF,OAAO,EASL,KAAK,eAAe,EAGrB,MAAM,oBAAoB,CAAC;AAqB5B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA2KxD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,aAAa,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,eAAe,CA0KlB;AAED,eAAO,MAAM,aAAa,EAAE,qBAAqB,CAAC,qBAAqB,CAqLtE,CAAC"}
|
package/dist/gateway.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParallAgentGateway, capabilityBinDir, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, initAgentTelemetry, createOtelLogger, } from '@parall/agent-core';
|
|
1
|
+
import { ParallAgentGateway, capabilityBinDir, dispatchLaneContextDir, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, initAgentTelemetry, createOtelLogger, } from '@parall/agent-core';
|
|
2
2
|
import { appendPreparedLocalAttachmentRefs, ensureLocalAttachmentGitExclude, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
|
|
3
3
|
import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
|
|
4
4
|
import * as crypto from 'node:crypto';
|
|
@@ -11,6 +11,15 @@ import { fetchAndApplyPlatformConfig, getChannelCapabilityKeys } from './config-
|
|
|
11
11
|
import { startWikiHelper } from './wiki-helper.js';
|
|
12
12
|
import { SessionManager } from './oc-session.js';
|
|
13
13
|
import { cleanupForkSession, clearSessionState, forkOrchestratorSession, resolveSessionId, resolveTranscriptFile, } from './fork.js';
|
|
14
|
+
/**
|
|
15
|
+
* Per-session dispatch context file (PRLL_CONTEXT_FILE contract) — same
|
|
16
|
+
* encoding as the claude/codex bridges: base64url(sessionKey) under the
|
|
17
|
+
* state dir's dispatch-context/.
|
|
18
|
+
*/
|
|
19
|
+
function sessionContextFilePath(stateDir, sessionKey) {
|
|
20
|
+
const fileName = Buffer.from(sessionKey).toString('base64url');
|
|
21
|
+
return path.join(stateDir, 'dispatch-context', `${fileName}.json`);
|
|
22
|
+
}
|
|
14
23
|
function resolveWsUrl(account) {
|
|
15
24
|
if (account.config.ws_url)
|
|
16
25
|
return account.config.ws_url;
|
|
@@ -404,6 +413,18 @@ export const parallGateway = {
|
|
|
404
413
|
runtimeKey: orchestratorKey,
|
|
405
414
|
runtimeRef: { hostname: os.hostname(), pid: process.pid },
|
|
406
415
|
dispatchAdapter,
|
|
416
|
+
// Opts openclaw into the dispatch ledger (claim/fold/complete +
|
|
417
|
+
// idempotent reply effects) — the same shared-gateway machinery
|
|
418
|
+
// claude/codex ride; openclaw stays buffer-only (no mid-turn steer),
|
|
419
|
+
// which the ledger does not require. Replies already flow through
|
|
420
|
+
// @parall/cli, which reads PRLL_CONTEXT_DIR (injected in hooks.ts)
|
|
421
|
+
// to bind dispatch_lane + reply effect keys.
|
|
422
|
+
// Design: docs/engineering-design/dispatch-convergence-design.md §7 (S2).
|
|
423
|
+
dispatchContextDir: dispatchLaneContextDir(stateDir),
|
|
424
|
+
// Per-session context file (PRLL_CONTEXT_FILE contract) — the CLI's
|
|
425
|
+
// TYPED dispatch binding (parall tasks update → task_update effect)
|
|
426
|
+
// reads lane/event/task fields from this file, not the lane dir.
|
|
427
|
+
contextFilePathForSession: (sessionKey) => sessionContextFilePath(stateDir, sessionKey),
|
|
407
428
|
log: otelLog,
|
|
408
429
|
// Live capability view for hint routing: the channel reply hint
|
|
409
430
|
// points at the vendor CLI only while the grant is active.
|
|
@@ -428,6 +449,12 @@ export const parallGateway = {
|
|
|
428
449
|
wikiMountRoot,
|
|
429
450
|
ws,
|
|
430
451
|
orchestratorSessionKey: orchestratorKey,
|
|
452
|
+
dispatchContextDir: dispatchLaneContextDir(stateDir),
|
|
453
|
+
contextFilePathForSession: (sessionKey) => sessionContextFilePath(stateDir, sessionKey),
|
|
454
|
+
// Lease-renewal bridge: openclaw tool calls flow through hooks,
|
|
455
|
+
// not the RuntimeEvent stream, so hook activity must renew the
|
|
456
|
+
// ledger lanes or a long exec outlives the lease (STALE_LANE).
|
|
457
|
+
touchRuntimeActivity: (sessionKey) => gateway.touchRuntimeActivity(sessionKey),
|
|
431
458
|
});
|
|
432
459
|
},
|
|
433
460
|
onSessionBinding: async ({ sessionKey, agentSessionId }) => {
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAgH7D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAgH7D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,QA+OzD"}
|
package/dist/hooks.js
CHANGED
|
@@ -118,6 +118,45 @@ export function registerParallHooks(api) {
|
|
|
118
118
|
};
|
|
119
119
|
});
|
|
120
120
|
const pendingSendCalls = new Map();
|
|
121
|
+
// Lane lease keepalive across the tool-call lifecycle. OpenClaw tool calls
|
|
122
|
+
// bypass the RuntimeEvent stream, so lane renewal must ride these hooks —
|
|
123
|
+
// and a ONE-SHOT touch at tool start is not enough: a single exec running
|
|
124
|
+
// past the lane TTL (10min server-side) would still be dethroned mid-call
|
|
125
|
+
// and the eventual CLI reply would die with STALE_LANE. While any tool call
|
|
126
|
+
// is in flight for a session, an unref'd interval keeps touching; the
|
|
127
|
+
// half-TTL throttle in the ledger decides when a real heartbeat is sent.
|
|
128
|
+
const laneRenewals = new Map();
|
|
129
|
+
const LANE_RENEW_INTERVAL_MS = 180_000;
|
|
130
|
+
const beginToolRenewal = (sessionKey) => {
|
|
131
|
+
const accountId = extractAccountIdFromSessionKey(sessionKey);
|
|
132
|
+
const state = accountId ? getParallAccountState(accountId) : undefined;
|
|
133
|
+
if (!state?.touchRuntimeActivity)
|
|
134
|
+
return;
|
|
135
|
+
state.touchRuntimeActivity(sessionKey);
|
|
136
|
+
const entry = laneRenewals.get(sessionKey) ?? { count: 0 };
|
|
137
|
+
entry.count++;
|
|
138
|
+
if (!entry.timer) {
|
|
139
|
+
entry.timer = setInterval(() => state.touchRuntimeActivity?.(sessionKey), LANE_RENEW_INTERVAL_MS);
|
|
140
|
+
entry.timer.unref?.();
|
|
141
|
+
}
|
|
142
|
+
laneRenewals.set(sessionKey, entry);
|
|
143
|
+
};
|
|
144
|
+
const endToolRenewal = (sessionKey) => {
|
|
145
|
+
const entry = laneRenewals.get(sessionKey);
|
|
146
|
+
if (entry) {
|
|
147
|
+
entry.count--;
|
|
148
|
+
if (entry.count <= 0) {
|
|
149
|
+
if (entry.timer)
|
|
150
|
+
clearInterval(entry.timer);
|
|
151
|
+
laneRenewals.delete(sessionKey);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const accountId = extractAccountIdFromSessionKey(sessionKey);
|
|
155
|
+
const state = accountId ? getParallAccountState(accountId) : undefined;
|
|
156
|
+
// Final touch on completion — covers the reply window right after a
|
|
157
|
+
// long tool call ends.
|
|
158
|
+
state?.touchRuntimeActivity?.(sessionKey);
|
|
159
|
+
};
|
|
121
160
|
// before_tool_call -> (1) update dispatch metrics, (2) await step creation, (3) ENV injection
|
|
122
161
|
// OpenClaw tool calls bypass the RuntimeEvent stream (they go through hooks, not the
|
|
123
162
|
// gateway-base for-await loop), so dispatch metrics must be updated here as well.
|
|
@@ -125,6 +164,9 @@ export function registerParallHooks(api) {
|
|
|
125
164
|
const sessionKey = ctx.sessionKey;
|
|
126
165
|
if (sessionKey) {
|
|
127
166
|
recordToolCall(sessionKey);
|
|
167
|
+
// Every tool (not just exec) keeps the ledger lane alive for its
|
|
168
|
+
// whole lifetime — see laneRenewals above.
|
|
169
|
+
beginToolRenewal(sessionKey);
|
|
128
170
|
if (event.toolName === 'exec' && event.toolCallId) {
|
|
129
171
|
const command = event.params?.command;
|
|
130
172
|
if (isParallSendCommand(command)) {
|
|
@@ -209,6 +251,20 @@ export function registerParallHooks(api) {
|
|
|
209
251
|
injectedEnv.PRLL_TRIGGER_MESSAGE_ID = triggerMsgId;
|
|
210
252
|
if (state.wikiMountRoot)
|
|
211
253
|
injectedEnv.PRLL_WIKI_MOUNT_ROOT = state.wikiMountRoot;
|
|
254
|
+
// Dispatch-ledger contract: the CLI keys into this directory by send
|
|
255
|
+
// target to bind dispatch_lane + the reply:<dsp> effect key (idempotent
|
|
256
|
+
// replies). Set only when the gateway runs the ledger.
|
|
257
|
+
if (state.dispatchContextDir)
|
|
258
|
+
injectedEnv.PRLL_CONTEXT_DIR = state.dispatchContextDir;
|
|
259
|
+
// Per-session context file — the CLI's TYPED binding (parall tasks
|
|
260
|
+
// update → task_update:<dsp> effect) reads lane/event/task from here.
|
|
261
|
+
if (state.contextFilePathForSession) {
|
|
262
|
+
injectedEnv.PRLL_CONTEXT_FILE = state.contextFilePathForSession(sessionKey);
|
|
263
|
+
}
|
|
264
|
+
// Long tool calls bypass the RuntimeEvent stream — forward activity so
|
|
265
|
+
// the ledger lane lease renews (a >TTL exec would otherwise be dethroned
|
|
266
|
+
// mid-turn and the eventual CLI reply would die with STALE_LANE).
|
|
267
|
+
state.touchRuntimeActivity?.(sessionKey);
|
|
212
268
|
// OpenClaw context (upstream doesn't inject these into exec env yet)
|
|
213
269
|
injectedEnv.OPENCLAW_SESSION_KEY = sessionKey;
|
|
214
270
|
if (event.toolCallId)
|
|
@@ -223,6 +279,8 @@ export function registerParallHooks(api) {
|
|
|
223
279
|
});
|
|
224
280
|
// after_tool_call -> track CLI outcomes + send tool_result step to AgentSession
|
|
225
281
|
api.on('after_tool_call', async (event, ctx) => {
|
|
282
|
+
if (ctx.sessionKey)
|
|
283
|
+
endToolRenewal(ctx.sessionKey);
|
|
226
284
|
if (ctx.sessionKey && event.toolCallId && pendingSendCalls.delete(event.toolCallId)) {
|
|
227
285
|
recordMessageSend(ctx.sessionKey, !event.error);
|
|
228
286
|
}
|
package/dist/index.bundle.mjs
CHANGED
|
@@ -26479,6 +26479,9 @@ function laneKeyForTarget(targetUri, threadRootId) {
|
|
|
26479
26479
|
return Buffer.from(`${targetUri}
|
|
26480
26480
|
${threadRootId ?? ""}`, "utf8").toString("base64url");
|
|
26481
26481
|
}
|
|
26482
|
+
function dispatchLaneContextDir(stateDir) {
|
|
26483
|
+
return path.join(stateDir, "dispatch-lane-context");
|
|
26484
|
+
}
|
|
26482
26485
|
function laneContextFilePath(contextDir, targetUri, threadRootId) {
|
|
26483
26486
|
return path.join(contextDir, `${laneKeyForTarget(targetUri, threadRootId)}.json`);
|
|
26484
26487
|
}
|
|
@@ -27940,7 +27943,6 @@ var ParallClient = class _ParallClient {
|
|
|
27940
27943
|
async steerDispatch(orgId, req) {
|
|
27941
27944
|
return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
|
|
27942
27945
|
}
|
|
27943
|
-
/** End a turn: no_action sweep of the lane's members + lane release + re-drive check. */
|
|
27944
27946
|
async completeDispatch(orgId, req) {
|
|
27945
27947
|
return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
|
|
27946
27948
|
}
|
|
@@ -27948,6 +27950,10 @@ var ParallClient = class _ParallClient {
|
|
|
27948
27950
|
* End a turn for a lane-less runtime: resolve the turn's folded WorkItems
|
|
27949
27951
|
* by source — broad-cover to the turn's reply Effect when one exists,
|
|
27950
27952
|
* no_action sweep otherwise. Idempotent.
|
|
27953
|
+
*
|
|
27954
|
+
* @deprecated Legacy ok-only alias — use {@link completeDispatch} with the
|
|
27955
|
+
* `sources` form, which also carries `turn_outcome`. The endpoint retires
|
|
27956
|
+
* at S3b (dispatch-convergence-design.md §3).
|
|
27951
27957
|
*/
|
|
27952
27958
|
async completeDispatchSources(orgId, req) {
|
|
27953
27959
|
return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE_SOURCES(orgId), req);
|
|
@@ -29153,7 +29159,11 @@ var LaneLedger = class {
|
|
|
29153
29159
|
throw err;
|
|
29154
29160
|
}
|
|
29155
29161
|
if (!res.claimed || !res.lane) {
|
|
29156
|
-
|
|
29162
|
+
if (res.reason === "empty") {
|
|
29163
|
+
this.opts.log?.warn(`claim for ${targetUri} came back empty \u2014 nothing foldable; leaving to the reconciler`);
|
|
29164
|
+
} else {
|
|
29165
|
+
this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent \u2014 leaving events pending for re-drive`);
|
|
29166
|
+
}
|
|
29157
29167
|
return null;
|
|
29158
29168
|
}
|
|
29159
29169
|
const leaseUntilMs = Date.parse(res.lease_until ?? "");
|
|
@@ -29230,6 +29240,18 @@ var LaneLedger = class {
|
|
|
29230
29240
|
* re-drives any same-target pending work. A STALE_LANE answer means a
|
|
29231
29241
|
* takeover already owns the resource — local state is dropped either way.
|
|
29232
29242
|
*/
|
|
29243
|
+
/**
|
|
29244
|
+
* Record that the turn on this lane surfaced a runtime error. The flow
|
|
29245
|
+
* settles an errored lane immediately (dispatchLaneGroup returns 'failed'
|
|
29246
|
+
* after a forced complete), so the bit normally lives for one turn only —
|
|
29247
|
+
* it is the transport between the gateway's per-session error signal and
|
|
29248
|
+
* this lane's complete request.
|
|
29249
|
+
*/
|
|
29250
|
+
markTurnError(laneKey) {
|
|
29251
|
+
const lane = this.lanes.get(laneKey);
|
|
29252
|
+
if (lane)
|
|
29253
|
+
lane.turnError = true;
|
|
29254
|
+
}
|
|
29233
29255
|
async completeIfIdle(laneKey, hasMoreLocal) {
|
|
29234
29256
|
const lane = this.lanes.get(laneKey);
|
|
29235
29257
|
if (!lane || hasMoreLocal)
|
|
@@ -29240,7 +29262,10 @@ var LaneLedger = class {
|
|
|
29240
29262
|
const res = await this.opts.client.completeDispatch(this.opts.orgId, {
|
|
29241
29263
|
lane: lane.lane,
|
|
29242
29264
|
target_uri: lane.targetUri,
|
|
29243
|
-
thread_root_id: lane.threadRootId
|
|
29265
|
+
thread_root_id: lane.threadRootId,
|
|
29266
|
+
// An error turn releases its members for retry instead of sweeping
|
|
29267
|
+
// them as handled (ignored by older servers).
|
|
29268
|
+
turn_outcome: lane.turnError ? "error" : "ok"
|
|
29244
29269
|
});
|
|
29245
29270
|
if (res.swept_no_action > 0 || res.redriven) {
|
|
29246
29271
|
this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
|
|
@@ -29253,6 +29278,17 @@ var LaneLedger = class {
|
|
|
29253
29278
|
this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
|
|
29254
29279
|
}
|
|
29255
29280
|
}
|
|
29281
|
+
/**
|
|
29282
|
+
* Renew one lane by its key — the external runtime-activity hook for
|
|
29283
|
+
* adapters whose tool traffic bypasses the RuntimeEvent stream (openclaw
|
|
29284
|
+
* hooks). Scoped to the session's own lane: renewing every lane would let
|
|
29285
|
+
* one busy fork keep an unrelated stalled fork's lane leased forever.
|
|
29286
|
+
*/
|
|
29287
|
+
renewByKey(laneKey) {
|
|
29288
|
+
const lane = this.lanes.get(laneKey);
|
|
29289
|
+
if (lane)
|
|
29290
|
+
this.maybeRenew(lane);
|
|
29291
|
+
}
|
|
29256
29292
|
/**
|
|
29257
29293
|
* Long-turn keepalive: renew the lane's lease on runtime activity, throttled
|
|
29258
29294
|
* so a chatty turn doesn't spam the server. Without this, a legitimately
|
|
@@ -29394,17 +29430,34 @@ async function dispatchLaneGroup(host, opts) {
|
|
|
29394
29430
|
}
|
|
29395
29431
|
return "foreign";
|
|
29396
29432
|
}
|
|
29433
|
+
host.noteSessionLane(opts.sessionKey, lane.laneKey);
|
|
29397
29434
|
let dispatched = false;
|
|
29398
29435
|
try {
|
|
29399
29436
|
dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
|
|
29400
29437
|
} catch (err) {
|
|
29438
|
+
host.noteSessionLane(opts.sessionKey, null);
|
|
29401
29439
|
await ledger.release(lane.laneKey).catch(() => {
|
|
29402
29440
|
});
|
|
29403
29441
|
throw err;
|
|
29442
|
+
} finally {
|
|
29443
|
+
if (dispatched)
|
|
29444
|
+
host.noteSessionLane(opts.sessionKey, null);
|
|
29404
29445
|
}
|
|
29405
29446
|
if (!dispatched) {
|
|
29406
29447
|
return "shutdown";
|
|
29407
29448
|
}
|
|
29449
|
+
if (host.consumeTurnError(opts.sessionKey)) {
|
|
29450
|
+
ledger.markTurnError(lane.laneKey);
|
|
29451
|
+
for (const msgId of lane.folded.keys()) {
|
|
29452
|
+
host.dispatchedMessages.delete(msgId);
|
|
29453
|
+
}
|
|
29454
|
+
try {
|
|
29455
|
+
host.opts.dispatchAdapter.abortDispatch?.(opts.sessionKey);
|
|
29456
|
+
} catch {
|
|
29457
|
+
}
|
|
29458
|
+
await ledger.completeIfIdle(lane.laneKey, false);
|
|
29459
|
+
return "failed";
|
|
29460
|
+
}
|
|
29408
29461
|
const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
|
|
29409
29462
|
await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
|
|
29410
29463
|
return "dispatched";
|
|
@@ -30728,6 +30781,44 @@ var ParallAgentGateway = class {
|
|
|
30728
30781
|
this.dispatchedMessages.add(id);
|
|
30729
30782
|
return true;
|
|
30730
30783
|
}
|
|
30784
|
+
/**
|
|
30785
|
+
* Lane currently being dispatched per session — lets external activity
|
|
30786
|
+
* signals renew exactly the caller's lane (renewing all lanes would keep
|
|
30787
|
+
* an unrelated stalled fork's lane leased forever).
|
|
30788
|
+
*/
|
|
30789
|
+
sessionActiveLanes = /* @__PURE__ */ new Map();
|
|
30790
|
+
noteSessionLane(sessionKey, laneKey) {
|
|
30791
|
+
if (laneKey == null)
|
|
30792
|
+
this.sessionActiveLanes.delete(sessionKey);
|
|
30793
|
+
else
|
|
30794
|
+
this.sessionActiveLanes.set(sessionKey, laneKey);
|
|
30795
|
+
}
|
|
30796
|
+
/**
|
|
30797
|
+
* External runtime-activity signal for adapters whose tool activity does
|
|
30798
|
+
* not flow through the RuntimeEvent stream (openclaw hooks call this from
|
|
30799
|
+
* the tool-call lifecycle): renews the session's OWN active ledger lane so
|
|
30800
|
+
* a long tool call cannot outlive the lease and get dethroned mid-turn.
|
|
30801
|
+
* No-op without an active ledger lane for the session.
|
|
30802
|
+
*/
|
|
30803
|
+
touchRuntimeActivity(sessionKey) {
|
|
30804
|
+
if (this.ledgerDisabled)
|
|
30805
|
+
return;
|
|
30806
|
+
const laneKey = this.sessionActiveLanes.get(sessionKey);
|
|
30807
|
+
if (laneKey)
|
|
30808
|
+
this.laneLedger?.renewByKey(laneKey);
|
|
30809
|
+
}
|
|
30810
|
+
/** Sessions whose in-flight turn surfaced a runtime error event. */
|
|
30811
|
+
turnErrorSessions = /* @__PURE__ */ new Set();
|
|
30812
|
+
/**
|
|
30813
|
+
* Consume (read-and-clear) the error marker for sessionKey's last turn.
|
|
30814
|
+
* Feeds complete's turn_outcome so an error turn's lane members are
|
|
30815
|
+
* released for retry instead of no_action-swept (design §3). Consuming
|
|
30816
|
+
* (rather than peeking) keeps one-shot fork session keys from accumulating
|
|
30817
|
+
* in the set forever.
|
|
30818
|
+
*/
|
|
30819
|
+
consumeTurnError(sessionKey) {
|
|
30820
|
+
return this.turnErrorSessions.delete(sessionKey);
|
|
30821
|
+
}
|
|
30731
30822
|
async emitDispatchReceived(event) {
|
|
30732
30823
|
const sourceType = event.ackSourceType ?? (event.type === "task" ? "task_activity" : "message");
|
|
30733
30824
|
const sourceId = event.ackSourceId ?? event.messageId;
|
|
@@ -31077,6 +31168,7 @@ var ParallAgentGateway = class {
|
|
|
31077
31168
|
this.pendingRestartNotification = null;
|
|
31078
31169
|
}
|
|
31079
31170
|
resetDispatchMetrics(sessionKey);
|
|
31171
|
+
this.turnErrorSessions.delete(sessionKey);
|
|
31080
31172
|
return runWithSessionKey(sessionKey, async () => {
|
|
31081
31173
|
let dispatchSpan = null;
|
|
31082
31174
|
setSessionChatId(sessionKey, event.targetId);
|
|
@@ -31184,6 +31276,9 @@ var ParallAgentGateway = class {
|
|
|
31184
31276
|
} else if (runtimeEvent.type === "tool_result" && pendingSendCallIds.delete(runtimeEvent.callId)) {
|
|
31185
31277
|
recordMessageSend(sessionKey, !runtimeEvent.error);
|
|
31186
31278
|
}
|
|
31279
|
+
if (runtimeEvent.type === "error") {
|
|
31280
|
+
this.turnErrorSessions.add(sessionKey);
|
|
31281
|
+
}
|
|
31187
31282
|
await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
|
|
31188
31283
|
}
|
|
31189
31284
|
if (!binding) {
|
|
@@ -31331,13 +31426,23 @@ var ParallAgentGateway = class {
|
|
|
31331
31426
|
body: buildForkScopePrefix(last) + buildEventBody(last),
|
|
31332
31427
|
earlier,
|
|
31333
31428
|
captureText: batchText,
|
|
31334
|
-
|
|
31429
|
+
// Per-LANE residue check (parity with the main-buffer path):
|
|
31430
|
+
// the fork queue can hold several lanes (channel + thread of
|
|
31431
|
+
// the same chat). A whole-queue check would defer THIS lane's
|
|
31432
|
+
// complete behind another lane's items and never revisit it —
|
|
31433
|
+
// its members would sit received until lease expiry.
|
|
31434
|
+
hasMoreLocal: () => fork.queue.some((it) => this.dispatchGroupKey(it.event) === this.dispatchGroupKey(last))
|
|
31335
31435
|
});
|
|
31336
31436
|
if (outcome === "foreign") {
|
|
31337
31437
|
for (const item of items)
|
|
31338
31438
|
item.resolve(false);
|
|
31339
31439
|
break;
|
|
31340
31440
|
}
|
|
31441
|
+
if (outcome === "failed") {
|
|
31442
|
+
for (const item of items)
|
|
31443
|
+
item.resolve(false);
|
|
31444
|
+
continue;
|
|
31445
|
+
}
|
|
31341
31446
|
dispatched = outcome === "dispatched";
|
|
31342
31447
|
} else {
|
|
31343
31448
|
dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
|
|
@@ -31371,6 +31476,7 @@ var ParallAgentGateway = class {
|
|
|
31371
31476
|
remaining.resolve(false);
|
|
31372
31477
|
}
|
|
31373
31478
|
} finally {
|
|
31479
|
+
this.turnErrorSessions.delete(fork.fork.sessionKey);
|
|
31374
31480
|
if (fork.deadlineTimer) {
|
|
31375
31481
|
clearTimeout(fork.deadlineTimer);
|
|
31376
31482
|
fork.deadlineTimer = null;
|
|
@@ -31483,6 +31589,10 @@ var ParallAgentGateway = class {
|
|
|
31483
31589
|
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
31484
31590
|
continue;
|
|
31485
31591
|
}
|
|
31592
|
+
if (outcome === "failed") {
|
|
31593
|
+
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
31594
|
+
continue;
|
|
31595
|
+
}
|
|
31486
31596
|
continue;
|
|
31487
31597
|
}
|
|
31488
31598
|
try {
|
|
@@ -31586,7 +31696,7 @@ var ParallAgentGateway = class {
|
|
|
31586
31696
|
}
|
|
31587
31697
|
this.dispatchState.mainBuffer.push(event);
|
|
31588
31698
|
if (this.usesLaneLedger(event)) {
|
|
31589
|
-
if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch
|
|
31699
|
+
if (this.mainCurrentGroupKey === this.dispatchGroupKey(event) && this.opts.dispatchAdapter.enqueueDuringDispatch != null && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch(this.opts.runtimeKey, buildEventBody(event))) {
|
|
31590
31700
|
this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
|
|
31591
31701
|
}
|
|
31592
31702
|
} else if (this.dispatchState.mainCurrentTargetId === event.targetId && await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
|
|
@@ -31613,12 +31723,14 @@ var ParallAgentGateway = class {
|
|
|
31613
31723
|
this.dispatchState.mainBuffer.push(event);
|
|
31614
31724
|
return false;
|
|
31615
31725
|
}
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
|
|
31619
|
-
|
|
31620
|
-
|
|
31621
|
-
|
|
31726
|
+
if (!this.usesLaneLedger(event)) {
|
|
31727
|
+
try {
|
|
31728
|
+
await this.emitDispatchReceived(event);
|
|
31729
|
+
} catch (err) {
|
|
31730
|
+
this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
|
|
31731
|
+
this.dispatchState.mainBuffer.push(event);
|
|
31732
|
+
return false;
|
|
31733
|
+
}
|
|
31622
31734
|
}
|
|
31623
31735
|
const fork = await this.opts.dispatchAdapter.forkSession({
|
|
31624
31736
|
sessionKey: this.opts.runtimeKey,
|
|
@@ -33873,6 +33985,10 @@ function cleanupForkSession(opts) {
|
|
|
33873
33985
|
}
|
|
33874
33986
|
|
|
33875
33987
|
// dist/gateway.js
|
|
33988
|
+
function sessionContextFilePath(stateDir, sessionKey) {
|
|
33989
|
+
const fileName = Buffer.from(sessionKey).toString("base64url");
|
|
33990
|
+
return path8.join(stateDir, "dispatch-context", `${fileName}.json`);
|
|
33991
|
+
}
|
|
33876
33992
|
function resolveWsUrl(account) {
|
|
33877
33993
|
if (account.config.ws_url)
|
|
33878
33994
|
return account.config.ws_url;
|
|
@@ -34242,6 +34358,18 @@ var parallGateway = {
|
|
|
34242
34358
|
runtimeKey: orchestratorKey,
|
|
34243
34359
|
runtimeRef: { hostname: os2.hostname(), pid: process.pid },
|
|
34244
34360
|
dispatchAdapter,
|
|
34361
|
+
// Opts openclaw into the dispatch ledger (claim/fold/complete +
|
|
34362
|
+
// idempotent reply effects) — the same shared-gateway machinery
|
|
34363
|
+
// claude/codex ride; openclaw stays buffer-only (no mid-turn steer),
|
|
34364
|
+
// which the ledger does not require. Replies already flow through
|
|
34365
|
+
// @parall/cli, which reads PRLL_CONTEXT_DIR (injected in hooks.ts)
|
|
34366
|
+
// to bind dispatch_lane + reply effect keys.
|
|
34367
|
+
// Design: docs/engineering-design/dispatch-convergence-design.md §7 (S2).
|
|
34368
|
+
dispatchContextDir: dispatchLaneContextDir(stateDir),
|
|
34369
|
+
// Per-session context file (PRLL_CONTEXT_FILE contract) — the CLI's
|
|
34370
|
+
// TYPED dispatch binding (parall tasks update → task_update effect)
|
|
34371
|
+
// reads lane/event/task fields from this file, not the lane dir.
|
|
34372
|
+
contextFilePathForSession: (sessionKey) => sessionContextFilePath(stateDir, sessionKey),
|
|
34245
34373
|
log: otelLog,
|
|
34246
34374
|
// Live capability view for hint routing: the channel reply hint
|
|
34247
34375
|
// points at the vendor CLI only while the grant is active.
|
|
@@ -34265,7 +34393,13 @@ var parallGateway = {
|
|
|
34265
34393
|
runtimeRef: { hostname: os2.hostname(), pid: process.pid },
|
|
34266
34394
|
wikiMountRoot,
|
|
34267
34395
|
ws,
|
|
34268
|
-
orchestratorSessionKey: orchestratorKey
|
|
34396
|
+
orchestratorSessionKey: orchestratorKey,
|
|
34397
|
+
dispatchContextDir: dispatchLaneContextDir(stateDir),
|
|
34398
|
+
contextFilePathForSession: (sessionKey) => sessionContextFilePath(stateDir, sessionKey),
|
|
34399
|
+
// Lease-renewal bridge: openclaw tool calls flow through hooks,
|
|
34400
|
+
// not the RuntimeEvent stream, so hook activity must renew the
|
|
34401
|
+
// ledger lanes or a long exec outlives the lease (STALE_LANE).
|
|
34402
|
+
touchRuntimeActivity: (sessionKey) => gateway.touchRuntimeActivity(sessionKey)
|
|
34269
34403
|
});
|
|
34270
34404
|
},
|
|
34271
34405
|
onSessionBinding: async ({ sessionKey, agentSessionId }) => {
|
|
@@ -34450,10 +34584,41 @@ function registerParallHooks(api) {
|
|
|
34450
34584
|
};
|
|
34451
34585
|
});
|
|
34452
34586
|
const pendingSendCalls = /* @__PURE__ */ new Map();
|
|
34587
|
+
const laneRenewals = /* @__PURE__ */ new Map();
|
|
34588
|
+
const LANE_RENEW_INTERVAL_MS = 18e4;
|
|
34589
|
+
const beginToolRenewal = (sessionKey) => {
|
|
34590
|
+
const accountId = extractAccountIdFromSessionKey(sessionKey);
|
|
34591
|
+
const state = accountId ? getParallAccountState(accountId) : void 0;
|
|
34592
|
+
if (!state?.touchRuntimeActivity)
|
|
34593
|
+
return;
|
|
34594
|
+
state.touchRuntimeActivity(sessionKey);
|
|
34595
|
+
const entry = laneRenewals.get(sessionKey) ?? { count: 0 };
|
|
34596
|
+
entry.count++;
|
|
34597
|
+
if (!entry.timer) {
|
|
34598
|
+
entry.timer = setInterval(() => state.touchRuntimeActivity?.(sessionKey), LANE_RENEW_INTERVAL_MS);
|
|
34599
|
+
entry.timer.unref?.();
|
|
34600
|
+
}
|
|
34601
|
+
laneRenewals.set(sessionKey, entry);
|
|
34602
|
+
};
|
|
34603
|
+
const endToolRenewal = (sessionKey) => {
|
|
34604
|
+
const entry = laneRenewals.get(sessionKey);
|
|
34605
|
+
if (entry) {
|
|
34606
|
+
entry.count--;
|
|
34607
|
+
if (entry.count <= 0) {
|
|
34608
|
+
if (entry.timer)
|
|
34609
|
+
clearInterval(entry.timer);
|
|
34610
|
+
laneRenewals.delete(sessionKey);
|
|
34611
|
+
}
|
|
34612
|
+
}
|
|
34613
|
+
const accountId = extractAccountIdFromSessionKey(sessionKey);
|
|
34614
|
+
const state = accountId ? getParallAccountState(accountId) : void 0;
|
|
34615
|
+
state?.touchRuntimeActivity?.(sessionKey);
|
|
34616
|
+
};
|
|
34453
34617
|
api.on("before_tool_call", async (event, ctx) => {
|
|
34454
34618
|
const sessionKey = ctx.sessionKey;
|
|
34455
34619
|
if (sessionKey) {
|
|
34456
34620
|
recordToolCall(sessionKey);
|
|
34621
|
+
beginToolRenewal(sessionKey);
|
|
34457
34622
|
if (event.toolName === "exec" && event.toolCallId) {
|
|
34458
34623
|
const command = event.params?.command;
|
|
34459
34624
|
if (isParallSendCommand(command)) {
|
|
@@ -34522,6 +34687,12 @@ function registerParallHooks(api) {
|
|
|
34522
34687
|
injectedEnv.PRLL_TRIGGER_MESSAGE_ID = triggerMsgId;
|
|
34523
34688
|
if (state.wikiMountRoot)
|
|
34524
34689
|
injectedEnv.PRLL_WIKI_MOUNT_ROOT = state.wikiMountRoot;
|
|
34690
|
+
if (state.dispatchContextDir)
|
|
34691
|
+
injectedEnv.PRLL_CONTEXT_DIR = state.dispatchContextDir;
|
|
34692
|
+
if (state.contextFilePathForSession) {
|
|
34693
|
+
injectedEnv.PRLL_CONTEXT_FILE = state.contextFilePathForSession(sessionKey);
|
|
34694
|
+
}
|
|
34695
|
+
state.touchRuntimeActivity?.(sessionKey);
|
|
34525
34696
|
injectedEnv.OPENCLAW_SESSION_KEY = sessionKey;
|
|
34526
34697
|
if (event.toolCallId)
|
|
34527
34698
|
injectedEnv.OPENCLAW_TOOL_CALL_ID = event.toolCallId;
|
|
@@ -34533,6 +34704,8 @@ function registerParallHooks(api) {
|
|
|
34533
34704
|
};
|
|
34534
34705
|
});
|
|
34535
34706
|
api.on("after_tool_call", async (event, ctx) => {
|
|
34707
|
+
if (ctx.sessionKey)
|
|
34708
|
+
endToolRenewal(ctx.sessionKey);
|
|
34536
34709
|
if (ctx.sessionKey && event.toolCallId && pendingSendCalls.delete(event.toolCallId)) {
|
|
34537
34710
|
recordMessageSend(ctx.sessionKey, !event.error);
|
|
34538
34711
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -16,6 +16,23 @@ export type ParallAccountState = {
|
|
|
16
16
|
wikiMountRoot?: string;
|
|
17
17
|
ws?: ParallWs;
|
|
18
18
|
orchestratorSessionKey?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Per-lane dispatch context dir (PRLL_CONTEXT_DIR contract) — set when the
|
|
21
|
+
* gateway runs the dispatch ledger; hooks inject it into exec env so the
|
|
22
|
+
* CLI binds replies to their dispatch lane.
|
|
23
|
+
*/
|
|
24
|
+
dispatchContextDir?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Per-session dispatch context file resolver (PRLL_CONTEXT_FILE contract)
|
|
27
|
+
* — the CLI's typed dispatch binding (task_update effects) reads from it.
|
|
28
|
+
*/
|
|
29
|
+
contextFilePathForSession?: (sessionKey: string) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Lease-renewal bridge into the gateway: openclaw tool activity flows
|
|
32
|
+
* through hooks (not the RuntimeEvent stream), so hooks must forward it or
|
|
33
|
+
* a long exec outlives the lane lease.
|
|
34
|
+
*/
|
|
35
|
+
touchRuntimeActivity?: (sessionKey: string) => void;
|
|
19
36
|
[key: string]: unknown;
|
|
20
37
|
};
|
|
21
38
|
export declare function setParallRuntime(next: PluginRuntime): void;
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAIF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,QAEnD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD;AAKD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,QAEjF;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,QAEzD;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAEvF;AAED,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,QAOvB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEhG;AAED,wBAAgB,yBAAyB,IAAI,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAEnF;AAID,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,QAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,SAAS,CAE5D"}
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAIF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,QAEnD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD;AAKD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,QAEjF;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,QAEzD;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAEvF;AAED,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,QAOvB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEhG;AAED,wBAAgB,yBAAyB,IAAI,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAEnF;AAID,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,QAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,SAAS,CAE5D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/parall",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "OpenClaw channel plugin for Parall IM",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"openclaw.plugin.json"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@parall/agent-core": "1.
|
|
20
|
-
"@parall/sdk": "1.
|
|
19
|
+
"@parall/agent-core": "1.44.0",
|
|
20
|
+
"@parall/sdk": "1.44.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^22.0.0",
|
package/src/gateway.ts
CHANGED
|
@@ -6,6 +6,7 @@ type ChannelGatewayAdapter<T = unknown> = NonNullable<ChannelPlugin<T>['gateway'
|
|
|
6
6
|
import {
|
|
7
7
|
ParallAgentGateway,
|
|
8
8
|
capabilityBinDir,
|
|
9
|
+
dispatchLaneContextDir,
|
|
9
10
|
parseShutdownDeadlineMs,
|
|
10
11
|
parseForkDeadlineMs,
|
|
11
12
|
parseDispatchDeadlineMs,
|
|
@@ -47,6 +48,16 @@ import {
|
|
|
47
48
|
resolveTranscriptFile,
|
|
48
49
|
} from './fork.js';
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Per-session dispatch context file (PRLL_CONTEXT_FILE contract) — same
|
|
53
|
+
* encoding as the claude/codex bridges: base64url(sessionKey) under the
|
|
54
|
+
* state dir's dispatch-context/.
|
|
55
|
+
*/
|
|
56
|
+
function sessionContextFilePath(stateDir: string, sessionKey: string): string {
|
|
57
|
+
const fileName = Buffer.from(sessionKey).toString('base64url');
|
|
58
|
+
return path.join(stateDir, 'dispatch-context', `${fileName}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
function resolveWsUrl(account: ResolvedParallAccount): string {
|
|
51
62
|
if (account.config.ws_url) return account.config.ws_url;
|
|
52
63
|
const base = account.config.parall_url.replace(/\/$/, '');
|
|
@@ -482,6 +493,19 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
|
|
|
482
493
|
runtimeKey: orchestratorKey,
|
|
483
494
|
runtimeRef: { hostname: os.hostname(), pid: process.pid },
|
|
484
495
|
dispatchAdapter,
|
|
496
|
+
// Opts openclaw into the dispatch ledger (claim/fold/complete +
|
|
497
|
+
// idempotent reply effects) — the same shared-gateway machinery
|
|
498
|
+
// claude/codex ride; openclaw stays buffer-only (no mid-turn steer),
|
|
499
|
+
// which the ledger does not require. Replies already flow through
|
|
500
|
+
// @parall/cli, which reads PRLL_CONTEXT_DIR (injected in hooks.ts)
|
|
501
|
+
// to bind dispatch_lane + reply effect keys.
|
|
502
|
+
// Design: docs/engineering-design/dispatch-convergence-design.md §7 (S2).
|
|
503
|
+
dispatchContextDir: dispatchLaneContextDir(stateDir),
|
|
504
|
+
// Per-session context file (PRLL_CONTEXT_FILE contract) — the CLI's
|
|
505
|
+
// TYPED dispatch binding (parall tasks update → task_update effect)
|
|
506
|
+
// reads lane/event/task fields from this file, not the lane dir.
|
|
507
|
+
contextFilePathForSession: (sessionKey: string) =>
|
|
508
|
+
sessionContextFilePath(stateDir, sessionKey),
|
|
485
509
|
log: otelLog,
|
|
486
510
|
// Live capability view for hint routing: the channel reply hint
|
|
487
511
|
// points at the vendor CLI only while the grant is active.
|
|
@@ -506,6 +530,13 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
|
|
|
506
530
|
wikiMountRoot,
|
|
507
531
|
ws,
|
|
508
532
|
orchestratorSessionKey: orchestratorKey,
|
|
533
|
+
dispatchContextDir: dispatchLaneContextDir(stateDir),
|
|
534
|
+
contextFilePathForSession: (sessionKey: string) =>
|
|
535
|
+
sessionContextFilePath(stateDir, sessionKey),
|
|
536
|
+
// Lease-renewal bridge: openclaw tool calls flow through hooks,
|
|
537
|
+
// not the RuntimeEvent stream, so hook activity must renew the
|
|
538
|
+
// ledger lanes or a long exec outlives the lease (STALE_LANE).
|
|
539
|
+
touchRuntimeActivity: (sessionKey: string) => gateway.touchRuntimeActivity(sessionKey),
|
|
509
540
|
});
|
|
510
541
|
},
|
|
511
542
|
onSessionBinding: async ({ sessionKey, agentSessionId }) => {
|
package/src/hooks.ts
CHANGED
|
@@ -140,6 +140,49 @@ export function registerParallHooks(api: OpenClawPluginApi) {
|
|
|
140
140
|
|
|
141
141
|
const pendingSendCalls = new Map<string, boolean>();
|
|
142
142
|
|
|
143
|
+
// Lane lease keepalive across the tool-call lifecycle. OpenClaw tool calls
|
|
144
|
+
// bypass the RuntimeEvent stream, so lane renewal must ride these hooks —
|
|
145
|
+
// and a ONE-SHOT touch at tool start is not enough: a single exec running
|
|
146
|
+
// past the lane TTL (10min server-side) would still be dethroned mid-call
|
|
147
|
+
// and the eventual CLI reply would die with STALE_LANE. While any tool call
|
|
148
|
+
// is in flight for a session, an unref'd interval keeps touching; the
|
|
149
|
+
// half-TTL throttle in the ledger decides when a real heartbeat is sent.
|
|
150
|
+
const laneRenewals = new Map<string, { count: number; timer?: ReturnType<typeof setInterval> }>();
|
|
151
|
+
const LANE_RENEW_INTERVAL_MS = 180_000;
|
|
152
|
+
|
|
153
|
+
const beginToolRenewal = (sessionKey: string) => {
|
|
154
|
+
const accountId = extractAccountIdFromSessionKey(sessionKey);
|
|
155
|
+
const state = accountId ? getParallAccountState(accountId) : undefined;
|
|
156
|
+
if (!state?.touchRuntimeActivity) return;
|
|
157
|
+
state.touchRuntimeActivity(sessionKey);
|
|
158
|
+
const entry = laneRenewals.get(sessionKey) ?? { count: 0 };
|
|
159
|
+
entry.count++;
|
|
160
|
+
if (!entry.timer) {
|
|
161
|
+
entry.timer = setInterval(
|
|
162
|
+
() => state.touchRuntimeActivity?.(sessionKey),
|
|
163
|
+
LANE_RENEW_INTERVAL_MS,
|
|
164
|
+
);
|
|
165
|
+
entry.timer.unref?.();
|
|
166
|
+
}
|
|
167
|
+
laneRenewals.set(sessionKey, entry);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const endToolRenewal = (sessionKey: string) => {
|
|
171
|
+
const entry = laneRenewals.get(sessionKey);
|
|
172
|
+
if (entry) {
|
|
173
|
+
entry.count--;
|
|
174
|
+
if (entry.count <= 0) {
|
|
175
|
+
if (entry.timer) clearInterval(entry.timer);
|
|
176
|
+
laneRenewals.delete(sessionKey);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const accountId = extractAccountIdFromSessionKey(sessionKey);
|
|
180
|
+
const state = accountId ? getParallAccountState(accountId) : undefined;
|
|
181
|
+
// Final touch on completion — covers the reply window right after a
|
|
182
|
+
// long tool call ends.
|
|
183
|
+
state?.touchRuntimeActivity?.(sessionKey);
|
|
184
|
+
};
|
|
185
|
+
|
|
143
186
|
// before_tool_call -> (1) update dispatch metrics, (2) await step creation, (3) ENV injection
|
|
144
187
|
// OpenClaw tool calls bypass the RuntimeEvent stream (they go through hooks, not the
|
|
145
188
|
// gateway-base for-await loop), so dispatch metrics must be updated here as well.
|
|
@@ -147,6 +190,9 @@ export function registerParallHooks(api: OpenClawPluginApi) {
|
|
|
147
190
|
const sessionKey = ctx.sessionKey;
|
|
148
191
|
if (sessionKey) {
|
|
149
192
|
recordToolCall(sessionKey);
|
|
193
|
+
// Every tool (not just exec) keeps the ledger lane alive for its
|
|
194
|
+
// whole lifetime — see laneRenewals above.
|
|
195
|
+
beginToolRenewal(sessionKey);
|
|
150
196
|
if (event.toolName === 'exec' && event.toolCallId) {
|
|
151
197
|
const command = (event.params as Record<string, unknown>)?.command as string | undefined;
|
|
152
198
|
if (isParallSendCommand(command)) {
|
|
@@ -228,6 +274,19 @@ export function registerParallHooks(api: OpenClawPluginApi) {
|
|
|
228
274
|
}
|
|
229
275
|
if (triggerMsgId) injectedEnv.PRLL_TRIGGER_MESSAGE_ID = triggerMsgId;
|
|
230
276
|
if (state.wikiMountRoot) injectedEnv.PRLL_WIKI_MOUNT_ROOT = state.wikiMountRoot;
|
|
277
|
+
// Dispatch-ledger contract: the CLI keys into this directory by send
|
|
278
|
+
// target to bind dispatch_lane + the reply:<dsp> effect key (idempotent
|
|
279
|
+
// replies). Set only when the gateway runs the ledger.
|
|
280
|
+
if (state.dispatchContextDir) injectedEnv.PRLL_CONTEXT_DIR = state.dispatchContextDir;
|
|
281
|
+
// Per-session context file — the CLI's TYPED binding (parall tasks
|
|
282
|
+
// update → task_update:<dsp> effect) reads lane/event/task from here.
|
|
283
|
+
if (state.contextFilePathForSession) {
|
|
284
|
+
injectedEnv.PRLL_CONTEXT_FILE = state.contextFilePathForSession(sessionKey);
|
|
285
|
+
}
|
|
286
|
+
// Long tool calls bypass the RuntimeEvent stream — forward activity so
|
|
287
|
+
// the ledger lane lease renews (a >TTL exec would otherwise be dethroned
|
|
288
|
+
// mid-turn and the eventual CLI reply would die with STALE_LANE).
|
|
289
|
+
state.touchRuntimeActivity?.(sessionKey);
|
|
231
290
|
|
|
232
291
|
// OpenClaw context (upstream doesn't inject these into exec env yet)
|
|
233
292
|
injectedEnv.OPENCLAW_SESSION_KEY = sessionKey;
|
|
@@ -247,6 +306,7 @@ export function registerParallHooks(api: OpenClawPluginApi) {
|
|
|
247
306
|
|
|
248
307
|
// after_tool_call -> track CLI outcomes + send tool_result step to AgentSession
|
|
249
308
|
api.on('after_tool_call', async (event, ctx) => {
|
|
309
|
+
if (ctx.sessionKey) endToolRenewal(ctx.sessionKey);
|
|
250
310
|
if (ctx.sessionKey && event.toolCallId && pendingSendCalls.delete(event.toolCallId)) {
|
|
251
311
|
recordMessageSend(ctx.sessionKey, !event.error);
|
|
252
312
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -32,6 +32,23 @@ export type ParallAccountState = {
|
|
|
32
32
|
wikiMountRoot?: string;
|
|
33
33
|
ws?: ParallWs;
|
|
34
34
|
orchestratorSessionKey?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Per-lane dispatch context dir (PRLL_CONTEXT_DIR contract) — set when the
|
|
37
|
+
* gateway runs the dispatch ledger; hooks inject it into exec env so the
|
|
38
|
+
* CLI binds replies to their dispatch lane.
|
|
39
|
+
*/
|
|
40
|
+
dispatchContextDir?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Per-session dispatch context file resolver (PRLL_CONTEXT_FILE contract)
|
|
43
|
+
* — the CLI's typed dispatch binding (task_update effects) reads from it.
|
|
44
|
+
*/
|
|
45
|
+
contextFilePathForSession?: (sessionKey: string) => string;
|
|
46
|
+
/**
|
|
47
|
+
* Lease-renewal bridge into the gateway: openclaw tool activity flows
|
|
48
|
+
* through hooks (not the RuntimeEvent stream), so hooks must forward it or
|
|
49
|
+
* a long exec outlives the lane lease.
|
|
50
|
+
*/
|
|
51
|
+
touchRuntimeActivity?: (sessionKey: string) => void;
|
|
35
52
|
[key: string]: unknown;
|
|
36
53
|
};
|
|
37
54
|
|