@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.
@@ -1,4 +1,6 @@
1
1
  import type { ParallClient } from '@parall/sdk';
2
+ export declare function getChannelCapabilityFragments(): string[];
3
+ export declare function getChannelCapabilityKeys(): string[];
2
4
  interface ConfigManagerOpts {
3
5
  client: ParallClient;
4
6
  stateDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config-manager.d.ts","sourceRoot":"","sources":["../src/config-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAWhD,UAAU,iBAAiB;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAClG;AAwKD;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDxF"}
1
+ {"version":3,"file":"config-manager.d.ts","sourceRoot":"","sources":["../src/config-manager.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuBhD,wBAAgB,6BAA6B,IAAI,MAAM,EAAE,CAExD;AAED,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAEnD;AAYD,UAAU,iBAAiB;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAClG;AAwKD;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDxF"}
@@ -1,5 +1,28 @@
1
+ import { extractCapabilities, materializeChannelCapabilities, } from '@parall/agent-core';
1
2
  import * as fs from 'node:fs';
2
3
  import * as path from 'node:path';
4
+ // Channel-capability snapshot for the OTHER consumers of capability state:
5
+ // the before_prompt_build hook (fragments into the system prompt — evaluated
6
+ // every prompt build, so a refresh reaches the very next turn without any
7
+ // restart) and the gateway's getCapabilityKeys (hint routing). Updated by
8
+ // applyChannelCapabilitySnapshot alongside every config apply, so the shim
9
+ // materialization and the declaration can never diverge within a refresh.
10
+ // Process-global BY THE SAME assumption as runtime.ts's agentIdentity:
11
+ // hosted agents run one-agent-per-pod, so a single snapshot is correct. A
12
+ // future multi-account host would need this keyed per account together with
13
+ // that identity global — do not fix one without the other.
14
+ let currentCapabilities = [];
15
+ export function getChannelCapabilityFragments() {
16
+ return currentCapabilities.map((c) => c.fragment);
17
+ }
18
+ export function getChannelCapabilityKeys() {
19
+ return currentCapabilities.map((c) => c.key);
20
+ }
21
+ function applyChannelCapabilitySnapshot(stateDir, config, log) {
22
+ const caps = extractCapabilities(config);
23
+ materializeChannelCapabilities(stateDir, caps, log);
24
+ currentCapabilities = caps;
25
+ }
3
26
  const CACHE_FILENAME = 'parall-platform-config.json';
4
27
  /** Tool names are no longer registered — all operations go through CLI. */
5
28
  function cachePath(stateDir) {
@@ -164,6 +187,7 @@ export async function fetchAndApplyPlatformConfig(opts) {
164
187
  if (cached) {
165
188
  log?.warn(`platform config fetch failed, using cached version ${cached.version}: ${String(err)}`);
166
189
  applyToOpenClawConfig(configPath, cached.config, credentials);
190
+ applyChannelCapabilitySnapshot(stateDir, cached.config, log);
167
191
  return;
168
192
  }
169
193
  // No cache and fetch fails — degrade gracefully
@@ -175,6 +199,7 @@ export async function fetchAndApplyPlatformConfig(opts) {
175
199
  log?.info('platform config unchanged (304)');
176
200
  if (cached) {
177
201
  applyToOpenClawConfig(configPath, cached.config, credentials);
202
+ applyChannelCapabilitySnapshot(stateDir, cached.config, log);
178
203
  }
179
204
  return;
180
205
  }
@@ -184,11 +209,13 @@ export async function fetchAndApplyPlatformConfig(opts) {
184
209
  log?.error(`platform config schema_version ${fresh.schema_version} is newer than supported (${SUPPORTED_SCHEMA_VERSION}), keeping current config`);
185
210
  if (cached) {
186
211
  applyToOpenClawConfig(configPath, cached.config, credentials);
212
+ applyChannelCapabilitySnapshot(stateDir, cached.config, log);
187
213
  }
188
214
  return;
189
215
  }
190
216
  // 5. Newer config received — save cache and apply
191
217
  log?.info(`platform config updated to version ${fresh.version}`);
192
218
  saveCachedConfig(stateDir, fresh);
219
+ applyChannelCapabilitySnapshot(stateDir, fresh.config, log);
193
220
  applyToOpenClawConfig(configPath, fresh.config, credentials);
194
221
  }
@@ -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,EAOL,KAAK,eAAe,EAGrB,MAAM,oBAAoB,CAAC;AAqB5B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAiKxD,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,CAmJtE,CAAC"}
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, 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';
@@ -7,10 +7,19 @@ import * as path from 'node:path';
7
7
  import { resolveParallAccount } from './accounts.js';
8
8
  import { getParallRuntime, removeParallAccountState, setAgentIdentity, setAgentSessionBinding, setDispatchGroupKey, setParallAccountState, } from './runtime.js';
9
9
  import { buildOrchestratorSessionKey } from './session.js';
10
- import { fetchAndApplyPlatformConfig } from './config-manager.js';
10
+ import { fetchAndApplyPlatformConfig, getChannelCapabilityKeys } from './config-manager.js';
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;
@@ -332,6 +341,16 @@ export const parallGateway = {
332
341
  try {
333
342
  const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || '/data', '.openclaw');
334
343
  const openclawConfigPath = path.join(stateDir, 'openclaw.json');
344
+ // Channel-capability shims resolve ahead of any globally-installed CLI
345
+ // of the same name. The plugin runs INSIDE the openclaw process, whose
346
+ // env every exec-tool child inherits — one idempotent prepend here
347
+ // covers the agent's shell commands for the process lifetime (the
348
+ // DIRECTORY is constant; its content tracks capability grants).
349
+ const shimDir = capabilityBinDir(stateDir);
350
+ const currentPath = process.env.PATH ?? '';
351
+ if (!currentPath.split(path.delimiter).includes(shimDir)) {
352
+ process.env.PATH = currentPath ? `${shimDir}${path.delimiter}${currentPath}` : shimDir;
353
+ }
335
354
  const configManagerOpts = {
336
355
  client,
337
356
  stateDir,
@@ -394,7 +413,22 @@ export const parallGateway = {
394
413
  runtimeKey: orchestratorKey,
395
414
  runtimeRef: { hostname: os.hostname(), pid: process.pid },
396
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),
397
428
  log: otelLog,
429
+ // Live capability view for hint routing: the channel reply hint
430
+ // points at the vendor CLI only while the grant is active.
431
+ getCapabilityKeys: getChannelCapabilityKeys,
398
432
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
399
433
  forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
400
434
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
@@ -415,6 +449,12 @@ export const parallGateway = {
415
449
  wikiMountRoot,
416
450
  ws,
417
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),
418
458
  });
419
459
  },
420
460
  onSessionBinding: async ({ sessionKey, agentSessionId }) => {
@@ -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;AA+G7D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,QA6KzD"}
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
@@ -1,4 +1,5 @@
1
1
  import { PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity, isParallSendCommand, isParallNoReplyCommand, recordToolCall, recordMessageSend, recordNoReply, } from '@parall/agent-core';
2
+ import { getChannelCapabilityFragments } from './config-manager.js';
2
3
  import { extractAccountIdFromSessionKey } from './session.js';
3
4
  import { clearDispatchGroupKey, getAgentIdentity, getAgentSessionBinding, getDispatchGroupKey, getDispatchMessageId, getParallAccountState, getSessionChatId, setAgentSessionBinding, } from './runtime.js';
4
5
  import { resolveSessionId } from './fork.js';
@@ -107,10 +108,55 @@ export function registerParallHooks(api) {
107
108
  PRLL_CHANNEL_CONTEXT,
108
109
  PRLL_BEHAVIOR,
109
110
  PRLL_REFERENCE_GUIDE,
111
+ // Channel-capability declarations (platform-config
112
+ // agents.capabilities[]). Evaluated on EVERY prompt build, so a
113
+ // capability grant/revocation reaches the next turn without any
114
+ // restart — the openclaw analogue of the CLI bridges' prompt-file
115
+ // rewrite + respawn.
116
+ ...getChannelCapabilityFragments(),
110
117
  ].join('\n\n'),
111
118
  };
112
119
  });
113
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
+ };
114
160
  // before_tool_call -> (1) update dispatch metrics, (2) await step creation, (3) ENV injection
115
161
  // OpenClaw tool calls bypass the RuntimeEvent stream (they go through hooks, not the
116
162
  // gateway-base for-await loop), so dispatch metrics must be updated here as well.
@@ -118,6 +164,9 @@ export function registerParallHooks(api) {
118
164
  const sessionKey = ctx.sessionKey;
119
165
  if (sessionKey) {
120
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);
121
170
  if (event.toolName === 'exec' && event.toolCallId) {
122
171
  const command = event.params?.command;
123
172
  if (isParallSendCommand(command)) {
@@ -202,6 +251,20 @@ export function registerParallHooks(api) {
202
251
  injectedEnv.PRLL_TRIGGER_MESSAGE_ID = triggerMsgId;
203
252
  if (state.wikiMountRoot)
204
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);
205
268
  // OpenClaw context (upstream doesn't inject these into exec env yet)
206
269
  injectedEnv.OPENCLAW_SESSION_KEY = sessionKey;
207
270
  if (event.toolCallId)
@@ -216,6 +279,8 @@ export function registerParallHooks(api) {
216
279
  });
217
280
  // after_tool_call -> track CLI outcomes + send tool_result step to AgentSession
218
281
  api.on('after_tool_call', async (event, ctx) => {
282
+ if (ctx.sessionKey)
283
+ endToolRenewal(ctx.sessionKey);
219
284
  if (ctx.sessionKey && event.toolCallId && pendingSendCalls.delete(event.toolCallId)) {
220
285
  recordMessageSend(ctx.sessionKey, !event.error);
221
286
  }