@parall/parall 1.42.1 → 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/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;
@@ -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.42.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.42.1",
20
- "@parall/sdk": "1.42.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",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: parall-platform
3
- description: "Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity, or create another agent. Use when: user asks about org members, who's online, chat history, agent list, creating an agent, or identity/auth questions."
3
+ description: "Parall platform queries and lightweight agent provisioning: list org members, agents, chats, read message history, check identity, create another agent, or walk the prll:// reference graph (resolve URIs, backlinks, multi-hop graph). Use when: user asks about org members, who's online, chat history, agent list, creating an agent, identity/auth questions, or you need to find what an entity is connected to / who references it."
4
4
  ---
5
5
 
6
6
  # Parall Platform
@@ -81,11 +81,11 @@ settled questions or repeat known mistakes. This searches live org data
81
81
  can see.
82
82
 
83
83
  ```bash
84
- # Semantic + keyword search across messages, tasks, and wiki
84
+ # Semantic + keyword search across messages, tasks, wiki, and comments
85
85
  parall search "auth v5 upgrade"
86
86
 
87
- # Restrict entity types (m=message, t=task, w=wiki). --channel narrows the
88
- # MESSAGE hits to one chat (tasks/wiki are unaffected by it).
87
+ # Restrict entity types (m=message, t=task, w=wiki, c=comment). --channel
88
+ # narrows the MESSAGE hits to one chat (tasks/wiki/comments are unaffected).
89
89
  parall search "auth v5 upgrade" --types m,w --channel prll://cht_eng
90
90
 
91
91
  # Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;
@@ -206,6 +206,8 @@ Every entity is addressable with a `prll://` URI. Common prefixes you'll see in
206
206
  | `prll://usr_` | User (human or agent) | parall-platform |
207
207
  | `prll://cht_` | Chat | parall-platform |
208
208
  | `prll://msg_` | Message | parall-platform |
209
+ | `prll://cmt_` | Comment (on tasks, wiki pages, changesets) | by target: task comment → parall-tasks, wiki/changeset comment → parall-wiki |
210
+ | `prll://ase_` | Agent session | parall-platform |
209
211
  | `prll://tsk_` | Task | parall-tasks |
210
212
  | `prll://prj_` | Project | parall-tasks |
211
213
  | `prll://sch_` | Schedule (time trigger) | parall-schedules |
@@ -241,6 +243,23 @@ parall refs graph prll://tsk_xxx --depth 2
241
243
 
242
244
  `refs graph` traverses both directions (inbound + outbound) and returns `nodes`
243
245
  and `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped
244
- the result — narrow it with a smaller `--depth`.
246
+ the result — narrow it with a smaller `--depth`. Edges carry `context` — the
247
+ author's annotation from `[context](prll://...)` — telling you *why* two
248
+ entities are linked, not just that they are.
249
+
250
+ The graph returns bare node URIs (no titles). The usual two-step: `refs graph`
251
+ for topology, then batch-`refs resolve` the node URIs you care about for
252
+ titles/status. If graph rejects your URI with a path/anchor error, strip it to
253
+ the entity root (`prll://wik_xxx/docs/a.md` → `prll://wik_xxx`) and re-query —
254
+ but note this WIDENS the query to the whole entity, not that one file: the
255
+ graph seeds from the wiki id, so a specific file's outbound links may sit
256
+ deeper in the result (or past the size caps). For refs pointing AT one file
257
+ (inbound), `refs backlinks` on the full file URI is precise. There is no
258
+ precise query for one file's OUTBOUND edges today — the widened root graph is
259
+ best-effort for those, or read the file itself for its `prll://` links.
260
+ Wiki-file nodes inside a graph *result* do legitimately carry paths.
261
+
262
+ `refs backlinks` items include a `snippet` of the referencing content — often
263
+ enough to judge relevance without fetching the source entity.
245
264
 
246
265
  CLI success output is JSON. Errors print a JSON line (`{"error","status","code",...}`) and, on a `PERMISSION_DENIED`, may add a plain-text `Request approval:` line — read both.
@@ -1,3 +1,8 @@
1
+ import {
2
+ type AgentCapability,
3
+ extractCapabilities,
4
+ materializeChannelCapabilities,
5
+ } from '@parall/agent-core';
1
6
  import type { ParallClient } from '@parall/sdk';
2
7
  import type { PlatformConfigResponse } from '@parall/sdk';
3
8
  import * as fs from 'node:fs';
@@ -9,6 +14,36 @@ interface CachedPlatformConfig {
9
14
  fetchedAt: string;
10
15
  }
11
16
 
17
+ // Channel-capability snapshot for the OTHER consumers of capability state:
18
+ // the before_prompt_build hook (fragments into the system prompt — evaluated
19
+ // every prompt build, so a refresh reaches the very next turn without any
20
+ // restart) and the gateway's getCapabilityKeys (hint routing). Updated by
21
+ // applyChannelCapabilitySnapshot alongside every config apply, so the shim
22
+ // materialization and the declaration can never diverge within a refresh.
23
+ // Process-global BY THE SAME assumption as runtime.ts's agentIdentity:
24
+ // hosted agents run one-agent-per-pod, so a single snapshot is correct. A
25
+ // future multi-account host would need this keyed per account together with
26
+ // that identity global — do not fix one without the other.
27
+ let currentCapabilities: AgentCapability[] = [];
28
+
29
+ export function getChannelCapabilityFragments(): string[] {
30
+ return currentCapabilities.map((c) => c.fragment);
31
+ }
32
+
33
+ export function getChannelCapabilityKeys(): string[] {
34
+ return currentCapabilities.map((c) => c.key);
35
+ }
36
+
37
+ function applyChannelCapabilitySnapshot(
38
+ stateDir: string,
39
+ config: Record<string, unknown>,
40
+ log?: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void },
41
+ ): void {
42
+ const caps = extractCapabilities(config);
43
+ materializeChannelCapabilities(stateDir, caps, log);
44
+ currentCapabilities = caps;
45
+ }
46
+
12
47
  interface ConfigManagerOpts {
13
48
  client: ParallClient;
14
49
  stateDir: string;
@@ -204,6 +239,7 @@ export async function fetchAndApplyPlatformConfig(opts: ConfigManagerOpts): Prom
204
239
  `platform config fetch failed, using cached version ${cached.version}: ${String(err)}`,
205
240
  );
206
241
  applyToOpenClawConfig(configPath, cached.config, credentials);
242
+ applyChannelCapabilitySnapshot(stateDir, cached.config, log);
207
243
  return;
208
244
  }
209
245
  // No cache and fetch fails — degrade gracefully
@@ -216,6 +252,7 @@ export async function fetchAndApplyPlatformConfig(opts: ConfigManagerOpts): Prom
216
252
  log?.info('platform config unchanged (304)');
217
253
  if (cached) {
218
254
  applyToOpenClawConfig(configPath, cached.config, credentials);
255
+ applyChannelCapabilitySnapshot(stateDir, cached.config, log);
219
256
  }
220
257
  return;
221
258
  }
@@ -228,6 +265,7 @@ export async function fetchAndApplyPlatformConfig(opts: ConfigManagerOpts): Prom
228
265
  );
229
266
  if (cached) {
230
267
  applyToOpenClawConfig(configPath, cached.config, credentials);
268
+ applyChannelCapabilitySnapshot(stateDir, cached.config, log);
231
269
  }
232
270
  return;
233
271
  }
@@ -235,5 +273,6 @@ export async function fetchAndApplyPlatformConfig(opts: ConfigManagerOpts): Prom
235
273
  // 5. Newer config received — save cache and apply
236
274
  log?.info(`platform config updated to version ${fresh.version}`);
237
275
  saveCachedConfig(stateDir, fresh);
276
+ applyChannelCapabilitySnapshot(stateDir, fresh.config, log);
238
277
  applyToOpenClawConfig(configPath, fresh.config, credentials);
239
278
  }
package/src/gateway.ts CHANGED
@@ -5,6 +5,8 @@ import type { ChannelPlugin } from 'openclaw/plugin-sdk/core';
5
5
  type ChannelGatewayAdapter<T = unknown> = NonNullable<ChannelPlugin<T>['gateway']>;
6
6
  import {
7
7
  ParallAgentGateway,
8
+ capabilityBinDir,
9
+ dispatchLaneContextDir,
8
10
  parseShutdownDeadlineMs,
9
11
  parseForkDeadlineMs,
10
12
  parseDispatchDeadlineMs,
@@ -35,7 +37,7 @@ import {
35
37
  } from './runtime.js';
36
38
  import { buildOrchestratorSessionKey } from './session.js';
37
39
  import type { ResolvedParallAccount } from './types.js';
38
- import { fetchAndApplyPlatformConfig } from './config-manager.js';
40
+ import { fetchAndApplyPlatformConfig, getChannelCapabilityKeys } from './config-manager.js';
39
41
  import { startWikiHelper } from './wiki-helper.js';
40
42
  import { SessionManager } from './oc-session.js';
41
43
  import {
@@ -46,6 +48,16 @@ import {
46
48
  resolveTranscriptFile,
47
49
  } from './fork.js';
48
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
+
49
61
  function resolveWsUrl(account: ResolvedParallAccount): string {
50
62
  if (account.config.ws_url) return account.config.ws_url;
51
63
  const base = account.config.parall_url.replace(/\/$/, '');
@@ -406,6 +418,17 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
406
418
  process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || '/data', '.openclaw');
407
419
  const openclawConfigPath = path.join(stateDir, 'openclaw.json');
408
420
 
421
+ // Channel-capability shims resolve ahead of any globally-installed CLI
422
+ // of the same name. The plugin runs INSIDE the openclaw process, whose
423
+ // env every exec-tool child inherits — one idempotent prepend here
424
+ // covers the agent's shell commands for the process lifetime (the
425
+ // DIRECTORY is constant; its content tracks capability grants).
426
+ const shimDir = capabilityBinDir(stateDir);
427
+ const currentPath = process.env.PATH ?? '';
428
+ if (!currentPath.split(path.delimiter).includes(shimDir)) {
429
+ process.env.PATH = currentPath ? `${shimDir}${path.delimiter}${currentPath}` : shimDir;
430
+ }
431
+
409
432
  const configManagerOpts = {
410
433
  client,
411
434
  stateDir,
@@ -470,7 +493,23 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
470
493
  runtimeKey: orchestratorKey,
471
494
  runtimeRef: { hostname: os.hostname(), pid: process.pid },
472
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),
473
509
  log: otelLog,
510
+ // Live capability view for hint routing: the channel reply hint
511
+ // points at the vendor CLI only while the grant is active.
512
+ getCapabilityKeys: getChannelCapabilityKeys,
474
513
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
475
514
  forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
476
515
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
@@ -491,6 +530,13 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
491
530
  wikiMountRoot,
492
531
  ws,
493
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),
494
540
  });
495
541
  },
496
542
  onSessionBinding: async ({ sessionKey, agentSessionId }) => {
package/src/hooks.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  recordMessageSend,
10
10
  recordNoReply,
11
11
  } from '@parall/agent-core';
12
+ import { getChannelCapabilityFragments } from './config-manager.js';
12
13
  import { extractAccountIdFromSessionKey } from './session.js';
13
14
  import {
14
15
  clearDispatchGroupKey,
@@ -127,12 +128,61 @@ export function registerParallHooks(api: OpenClawPluginApi) {
127
128
  PRLL_CHANNEL_CONTEXT,
128
129
  PRLL_BEHAVIOR,
129
130
  PRLL_REFERENCE_GUIDE,
131
+ // Channel-capability declarations (platform-config
132
+ // agents.capabilities[]). Evaluated on EVERY prompt build, so a
133
+ // capability grant/revocation reaches the next turn without any
134
+ // restart — the openclaw analogue of the CLI bridges' prompt-file
135
+ // rewrite + respawn.
136
+ ...getChannelCapabilityFragments(),
130
137
  ].join('\n\n'),
131
138
  };
132
139
  });
133
140
 
134
141
  const pendingSendCalls = new Map<string, boolean>();
135
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
+
136
186
  // before_tool_call -> (1) update dispatch metrics, (2) await step creation, (3) ENV injection
137
187
  // OpenClaw tool calls bypass the RuntimeEvent stream (they go through hooks, not the
138
188
  // gateway-base for-await loop), so dispatch metrics must be updated here as well.
@@ -140,6 +190,9 @@ export function registerParallHooks(api: OpenClawPluginApi) {
140
190
  const sessionKey = ctx.sessionKey;
141
191
  if (sessionKey) {
142
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);
143
196
  if (event.toolName === 'exec' && event.toolCallId) {
144
197
  const command = (event.params as Record<string, unknown>)?.command as string | undefined;
145
198
  if (isParallSendCommand(command)) {
@@ -221,6 +274,19 @@ export function registerParallHooks(api: OpenClawPluginApi) {
221
274
  }
222
275
  if (triggerMsgId) injectedEnv.PRLL_TRIGGER_MESSAGE_ID = triggerMsgId;
223
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);
224
290
 
225
291
  // OpenClaw context (upstream doesn't inject these into exec env yet)
226
292
  injectedEnv.OPENCLAW_SESSION_KEY = sessionKey;
@@ -240,6 +306,7 @@ export function registerParallHooks(api: OpenClawPluginApi) {
240
306
 
241
307
  // after_tool_call -> track CLI outcomes + send tool_result step to AgentSession
242
308
  api.on('after_tool_call', async (event, ctx) => {
309
+ if (ctx.sessionKey) endToolRenewal(ctx.sessionKey);
243
310
  if (ctx.sessionKey && event.toolCallId && pendingSendCalls.delete(event.toolCallId)) {
244
311
  recordMessageSend(ctx.sessionKey, !event.error);
245
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