@parall/parall 1.30.0 → 1.32.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.
Files changed (47) hide show
  1. package/dist/accounts.d.ts +1 -1
  2. package/dist/accounts.js +4 -4
  3. package/dist/channel.d.ts +2 -2
  4. package/dist/channel.d.ts.map +1 -1
  5. package/dist/channel.js +10 -10
  6. package/dist/config-manager.d.ts +1 -1
  7. package/dist/config-manager.d.ts.map +1 -1
  8. package/dist/config-manager.js +70 -30
  9. package/dist/fork.d.ts.map +1 -1
  10. package/dist/fork.js +17 -17
  11. package/dist/gateway.d.ts +3 -3
  12. package/dist/gateway.d.ts.map +1 -1
  13. package/dist/gateway.js +214 -171
  14. package/dist/hooks.d.ts +1 -1
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/hooks.js +52 -26
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +7 -7
  19. package/dist/oc-session.d.ts.map +1 -1
  20. package/dist/oc-session.js +35 -27
  21. package/dist/outbound.d.ts +2 -2
  22. package/dist/outbound.d.ts.map +1 -1
  23. package/dist/outbound.js +13 -14
  24. package/dist/routing.d.ts +2 -2
  25. package/dist/routing.js +1 -1
  26. package/dist/runtime.d.ts +5 -5
  27. package/dist/runtime.d.ts.map +1 -1
  28. package/dist/runtime.js +2 -2
  29. package/dist/session.js +4 -4
  30. package/dist/wiki-helper.d.ts.map +1 -1
  31. package/dist/wiki-helper.js +27 -27
  32. package/openclaw.plugin.json +4 -1
  33. package/package.json +3 -3
  34. package/skills/parall-clips/SKILL.md +48 -0
  35. package/src/accounts.ts +5 -5
  36. package/src/channel.ts +15 -14
  37. package/src/config-manager.ts +79 -34
  38. package/src/fork.ts +26 -22
  39. package/src/gateway.ts +245 -175
  40. package/src/hooks.ts +109 -50
  41. package/src/index.ts +8 -8
  42. package/src/oc-session.ts +43 -35
  43. package/src/outbound.ts +18 -17
  44. package/src/routing.ts +2 -2
  45. package/src/runtime.ts +11 -7
  46. package/src/session.ts +4 -4
  47. package/src/wiki-helper.ts +47 -34
package/src/gateway.ts CHANGED
@@ -1,21 +1,30 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
2
- import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
1
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
2
+ import type { ChannelPlugin } from 'openclaw/plugin-sdk/core';
3
3
 
4
4
  /** Extract ChannelGatewayAdapter from ChannelPlugin (removed from public SDK exports in 2026.3.24). */
5
- type ChannelGatewayAdapter<T = unknown> = NonNullable<ChannelPlugin<T>["gateway"]>;
5
+ type ChannelGatewayAdapter<T = unknown> = NonNullable<ChannelPlugin<T>['gateway']>;
6
6
  import {
7
7
  ParallAgentGateway,
8
8
  parseShutdownDeadlineMs,
9
+ parseForkDeadlineMs,
10
+ parseDispatchDeadlineMs,
11
+ initAgentTelemetry,
12
+ createOtelLogger,
9
13
  type DispatchAdapter,
10
14
  type ParallEvent,
11
15
  type RuntimeEvent,
12
- } from "@parall/agent-core";
13
- import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
14
- import type { ParallChannelConfig } from "./types.js";
15
- import * as crypto from "node:crypto";
16
- import * as os from "node:os";
17
- import * as path from "node:path";
18
- import { resolveParallAccount } from "./accounts.js";
16
+ } from '@parall/agent-core';
17
+ import {
18
+ appendPreparedLocalAttachmentRefs,
19
+ ensureLocalAttachmentGitExclude,
20
+ pinLocalAttachmentPaths,
21
+ } from '@parall/agent-core/internal/attachment-input';
22
+ import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
23
+ import type { ParallChannelConfig } from './types.js';
24
+ import * as crypto from 'node:crypto';
25
+ import * as os from 'node:os';
26
+ import * as path from 'node:path';
27
+ import { resolveParallAccount } from './accounts.js';
19
28
  import {
20
29
  getParallRuntime,
21
30
  removeParallAccountState,
@@ -23,18 +32,24 @@ import {
23
32
  setAgentSessionBinding,
24
33
  setDispatchGroupKey,
25
34
  setParallAccountState,
26
- } from "./runtime.js";
27
- import { buildOrchestratorSessionKey } from "./session.js";
28
- import type { ResolvedParallAccount } from "./types.js";
29
- import { fetchAndApplyPlatformConfig } from "./config-manager.js";
30
- import { startWikiHelper } from "./wiki-helper.js";
31
- import { SessionManager } from "./oc-session.js";
32
- import { cleanupForkSession, clearSessionState, forkOrchestratorSession, resolveSessionId, resolveTranscriptFile } from "./fork.js";
35
+ } from './runtime.js';
36
+ import { buildOrchestratorSessionKey } from './session.js';
37
+ import type { ResolvedParallAccount } from './types.js';
38
+ import { fetchAndApplyPlatformConfig } from './config-manager.js';
39
+ import { startWikiHelper } from './wiki-helper.js';
40
+ import { SessionManager } from './oc-session.js';
41
+ import {
42
+ cleanupForkSession,
43
+ clearSessionState,
44
+ forkOrchestratorSession,
45
+ resolveSessionId,
46
+ resolveTranscriptFile,
47
+ } from './fork.js';
33
48
 
34
49
  function resolveWsUrl(account: ResolvedParallAccount): string {
35
50
  if (account.config.ws_url) return account.config.ws_url;
36
- const base = account.config.parall_url.replace(/\/$/, "");
37
- const wsBase = base.replace(/^http/, "ws");
51
+ const base = account.config.parall_url.replace(/\/$/, '');
52
+ const wsBase = base.replace(/^http/, 'ws');
38
53
  return `${wsBase}/ws`;
39
54
  }
40
55
 
@@ -70,15 +85,15 @@ function buildInboundCtx(
70
85
  To: `parall:orchestrator`,
71
86
  SessionKey: sessionKey,
72
87
  AccountId: accountId,
73
- ChatType: event.targetType ?? "unknown",
88
+ ChatType: event.targetType ?? 'unknown',
74
89
  SenderName: event.senderName,
75
90
  SenderId: event.senderId,
76
- Provider: "parall" as const,
77
- Surface: "parall" as const,
91
+ Provider: 'parall' as const,
92
+ Surface: 'parall' as const,
78
93
  MessageSid: event.messageId,
79
94
  Timestamp: Date.now(),
80
95
  CommandAuthorized: true,
81
- OriginatingChannel: "parall" as const,
96
+ OriginatingChannel: 'parall' as const,
82
97
  OriginatingTo: `parall:orchestrator`,
83
98
  });
84
99
  }
@@ -91,13 +106,13 @@ function buildInboundHistory(events: ParallEvent[]): Array<{ sender: string; bod
91
106
  if (event.threadRootId) meta.push(`[Thread: ${event.threadRootId}]`);
92
107
  if (event.attachments?.length) {
93
108
  for (const att of event.attachments) {
94
- const safeMime = att.mimeType.replace(/[\r\n[\]|]/g, " ").trim();
109
+ const safeMime = att.mimeType.replace(/[\r\n[\]|]/g, ' ').trim();
95
110
  meta.push(`[Attachment: prll://${att.id} | ${safeMime}]`);
96
111
  }
97
112
  }
98
113
  return {
99
114
  sender: event.senderName,
100
- body: meta.length > 0 ? `${meta.join(" ")}\n${event.body}` : event.body,
115
+ body: meta.length > 0 ? `${meta.join(' ')}\n${event.body}` : event.body,
101
116
  };
102
117
  });
103
118
  }
@@ -185,68 +200,106 @@ function createOpenClawDispatchAdapter(opts: {
185
200
  cfg: Record<string, unknown>;
186
201
  accountId: string;
187
202
  sessionsDir: string;
203
+ workspaceDir: string;
188
204
  }): DispatchAdapter {
205
+ const activeStreams = new Map<string, ReturnType<typeof createRuntimeEventStream>>();
206
+
189
207
  return {
190
- async *dispatch({ event, earlierEvents = [], bodyForAgent, sessionKey }) {
191
- const stream = createRuntimeEventStream();
192
- let reasoningBuffer = "";
193
- let turnGroupKey = "";
208
+ abortDispatch(sessionKey: string): void {
209
+ activeStreams.get(sessionKey)?.fail(new Error('dispatch deadline exceeded'));
210
+ },
194
211
 
195
- const run = opts.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
196
- ctx: buildInboundCtx(opts.core, opts.accountId, event, sessionKey, bodyForAgent, earlierEvents),
197
- cfg: opts.cfg,
198
- dispatcherOptions: {
199
- deliver: async (payload: { text?: string }) => {
200
- const replyText = payload.text?.trim();
201
- if (!replyText) return;
202
- stream.push({
203
- type: "text",
204
- text: replyText,
205
- project: false,
206
- groupKey: turnGroupKey || undefined,
207
- });
208
- },
209
- onReplyStart: () => {
210
- turnGroupKey = crypto.randomUUID();
211
- setDispatchGroupKey(sessionKey, turnGroupKey);
212
- },
213
- },
214
- replyOptions: {
215
- onReasoningStream: (payload: { text?: string }) => {
216
- reasoningBuffer += payload.text ?? "";
212
+ async *dispatch({ event, earlierEvents = [], bodyForAgent, sessionKey, context }) {
213
+ let promptBody = bodyForAgent;
214
+ let releasePreparedAttachments = () => {};
215
+ try {
216
+ const prepared = await appendPreparedLocalAttachmentRefs(bodyForAgent, event, context, {
217
+ workspaceDir: opts.workspaceDir,
218
+ log: context.log,
219
+ });
220
+ promptBody = prepared.body;
221
+ releasePreparedAttachments = pinLocalAttachmentPaths(prepared.attachments.images);
222
+ } catch (err) {
223
+ context.log?.warn?.(`failed to prepare local attachments: ${String(err)}`);
224
+ }
225
+
226
+ try {
227
+ const stream = createRuntimeEventStream();
228
+ activeStreams.set(sessionKey, stream);
229
+ let reasoningBuffer = '';
230
+ let turnGroupKey = '';
231
+
232
+ const run = opts.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
233
+ ctx: buildInboundCtx(
234
+ opts.core,
235
+ opts.accountId,
236
+ event,
237
+ sessionKey,
238
+ promptBody,
239
+ earlierEvents,
240
+ ),
241
+ cfg: opts.cfg,
242
+ dispatcherOptions: {
243
+ deliver: async (payload: { text?: string }) => {
244
+ const replyText = payload.text?.trim();
245
+ if (!replyText) return;
246
+ stream.push({
247
+ type: 'text',
248
+ text: replyText,
249
+ project: false,
250
+ groupKey: turnGroupKey || undefined,
251
+ });
252
+ },
253
+ onReplyStart: () => {
254
+ turnGroupKey = crypto.randomUUID();
255
+ setDispatchGroupKey(sessionKey, turnGroupKey);
256
+ },
217
257
  },
218
- onReasoningEnd: async () => {
219
- if (!reasoningBuffer) return;
220
- stream.push({
221
- type: "thinking",
222
- text: reasoningBuffer,
223
- groupKey: turnGroupKey || undefined,
224
- });
225
- reasoningBuffer = "";
258
+ replyOptions: {
259
+ onReasoningStream: (payload: { text?: string }) => {
260
+ reasoningBuffer += payload.text ?? '';
261
+ },
262
+ onReasoningEnd: async () => {
263
+ if (!reasoningBuffer) return;
264
+ stream.push({
265
+ type: 'thinking',
266
+ text: reasoningBuffer,
267
+ groupKey: turnGroupKey || undefined,
268
+ });
269
+ reasoningBuffer = '';
270
+ },
226
271
  },
227
- },
228
- });
272
+ });
229
273
 
230
- run.then(() => stream.end()).catch((err) => stream.fail(err));
231
- let sessionId: string | null;
232
- try {
233
- sessionId = await Promise.race([
234
- waitForOpenClawSessionId(opts.sessionsDir, sessionKey),
235
- run.then(() => null as string | null, (err) => { throw err; }),
236
- ]);
237
- } catch {
238
- sessionId = null;
239
- }
240
- if (!sessionId) {
241
- stream.fail(new Error(`OpenClaw did not expose a sessionId for ${sessionKey}`));
242
- } else {
243
- yield {
244
- type: "runtime_session",
245
- runtimeSessionId: sessionId,
246
- runtimeLaneKey: sessionKey,
247
- };
274
+ run.then(() => stream.end()).catch((err) => stream.fail(err));
275
+ let sessionId: string | null;
276
+ try {
277
+ sessionId = await Promise.race([
278
+ waitForOpenClawSessionId(opts.sessionsDir, sessionKey),
279
+ run.then(
280
+ () => null as string | null,
281
+ (err) => {
282
+ throw err;
283
+ },
284
+ ),
285
+ ]);
286
+ } catch {
287
+ sessionId = null;
288
+ }
289
+ if (!sessionId) {
290
+ stream.fail(new Error(`OpenClaw did not expose a sessionId for ${sessionKey}`));
291
+ } else {
292
+ yield {
293
+ type: 'runtime_session',
294
+ runtimeSessionId: sessionId,
295
+ runtimeLaneKey: sessionKey,
296
+ };
297
+ }
298
+ yield* stream.stream();
299
+ } finally {
300
+ activeStreams.delete(sessionKey);
301
+ releasePreparedAttachments();
248
302
  }
249
- yield* stream.stream();
250
303
  },
251
304
 
252
305
  getBranchPoint(sessionKey: string): string | undefined {
@@ -273,7 +326,7 @@ function createOpenClawDispatchAdapter(opts: {
273
326
  },
274
327
 
275
328
  cleanupFork({ fork }) {
276
- if (typeof fork.sessionFile !== "string") return;
329
+ if (typeof fork.sessionFile !== 'string') return;
277
330
  cleanupForkSession({
278
331
  sessionFile: fork.sessionFile,
279
332
  sessionKey: fork.sessionKey,
@@ -287,7 +340,8 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
287
340
  startAccount: async (ctx) => {
288
341
  const account = resolveParallAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
289
342
  if (!account.enabled) return;
290
- if (!account.configured) throw new Error("Parall account is not configured: parall_url/api_key/org_id required");
343
+ if (!account.configured)
344
+ throw new Error('Parall account is not configured: parall_url/api_key/org_id required');
291
345
 
292
346
  const { config } = account;
293
347
  const core = getParallRuntime();
@@ -308,109 +362,125 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
308
362
  });
309
363
  log?.info(`parall[${ctx.accountId}]: authenticated as ${me.display_name} (${agentUserId})`);
310
364
 
311
- const stateDir = process.env.OPENCLAW_STATE_DIR
312
- || path.join(process.env.HOME || "/data", ".openclaw");
313
- const openclawConfigPath = path.join(stateDir, "openclaw.json");
314
-
315
- const configManagerOpts = {
316
- client,
317
- stateDir,
318
- configPath: openclawConfigPath,
319
- credentials: { api_key: config.api_key, parall_url: config.parall_url },
320
- log,
321
- };
322
-
365
+ const telemetry = await initAgentTelemetry('parall-openclaw-agent', 'openclaw');
366
+ const otelLog = createOtelLogger('agent', 'openclaw-agent');
323
367
  try {
324
- await fetchAndApplyPlatformConfig(configManagerOpts);
325
- } catch (err) {
326
- log?.warn(`parall[${ctx.accountId}]: platform config fetch failed: ${String(err)}`);
327
- }
368
+ const stateDir =
369
+ process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || '/data', '.openclaw');
370
+ const openclawConfigPath = path.join(stateDir, 'openclaw.json');
328
371
 
329
- let stopWikiHelper: (() => void) | null = null;
330
- let wikiMountRoot: string | undefined;
331
- try {
332
- const wikiHelper = await startWikiHelper({
333
- accountId: ctx.accountId,
334
- parallUrl: config.parall_url,
335
- apiKey: config.api_key,
336
- orgId: config.org_id,
337
- agentId: agentUserId,
372
+ const configManagerOpts = {
373
+ client,
338
374
  stateDir,
339
- log,
340
- });
341
- wikiMountRoot = wikiHelper.mountRoot;
342
- stopWikiHelper = wikiHelper.stop;
343
- } catch (err) {
344
- log?.warn(`parall[${ctx.accountId}]: wiki helper startup failed: ${String(err)}`);
345
- }
375
+ configPath: openclawConfigPath,
376
+ credentials: { api_key: config.api_key, parall_url: config.parall_url },
377
+ log: otelLog,
378
+ };
346
379
 
347
- const wsUrl = resolveWsUrl(account);
348
- const ws = new ParallWs({
349
- getTicket: () => client.getWsTicket(),
350
- wsUrl,
351
- });
352
- const orchestratorKey = buildOrchestratorSessionKey(ctx.accountId);
353
- const sessionsDir = path.join(stateDir, "agents", "main", "sessions");
354
- const dispatchAdapter = createOpenClawDispatchAdapter({
355
- core,
356
- cfg: ctx.cfg,
357
- accountId: ctx.accountId,
358
- sessionsDir,
359
- });
360
-
361
- const gateway = new ParallAgentGateway({
362
- accountId: ctx.accountId,
363
- client,
364
- ws,
365
- connectionLabel: wsUrl,
366
- config: {
367
- parall_url: config.parall_url,
368
- api_key: config.api_key,
369
- org_id: config.org_id,
370
- },
371
- agentUserId,
372
- runtimeType: "openclaw",
373
- runtimeKey: orchestratorKey,
374
- runtimeRef: { hostname: os.hostname(), pid: process.pid },
375
- dispatchAdapter,
376
- log,
377
- shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
378
- onConfigUpdate: async () => {
380
+ try {
379
381
  await fetchAndApplyPlatformConfig(configManagerOpts);
380
- },
381
- onSessionReady: async ({ activeSessionId }) => {
382
- setParallAccountState(ctx.accountId, {
383
- client,
384
- apiUrl: config.parall_url,
382
+ } catch (err) {
383
+ otelLog.warn(`parall[${ctx.accountId}]: platform config fetch failed: ${String(err)}`);
384
+ }
385
+
386
+ let stopWikiHelper: (() => void) | null = null;
387
+ let wikiMountRoot: string | undefined;
388
+ try {
389
+ const wikiHelper = await startWikiHelper({
390
+ accountId: ctx.accountId,
391
+ parallUrl: config.parall_url,
385
392
  apiKey: config.api_key,
386
393
  orgId: config.org_id,
387
- agentUserId,
388
- activeSessionId,
389
- sessionBindings: new Map<string, string>(),
390
- sessionsDir,
391
- runtimeRef: { hostname: os.hostname(), pid: process.pid },
392
- wikiMountRoot,
393
- ws,
394
- orchestratorSessionKey: orchestratorKey,
394
+ agentId: agentUserId,
395
+ stateDir,
396
+ log: otelLog,
395
397
  });
396
- },
397
- onSessionBinding: async ({ sessionKey, agentSessionId }) => {
398
- setAgentSessionBinding(ctx.accountId, sessionKey, agentSessionId);
399
- },
400
- onBeforeDisconnect: async () => {
401
- stopWikiHelper?.();
402
- removeParallAccountState(ctx.accountId);
403
- },
404
- onNewSession: () => { clearSessionState(sessionsDir, orchestratorKey); },
405
- });
398
+ wikiMountRoot = wikiHelper.mountRoot;
399
+ stopWikiHelper = wikiHelper.stop;
400
+ } catch (err) {
401
+ otelLog.warn(`parall[${ctx.accountId}]: wiki helper startup failed: ${String(err)}`);
402
+ }
406
403
 
407
- try {
408
- await gateway.run(ctx.abortSignal);
409
- } finally {
410
- if (!ctx.abortSignal.aborted) {
411
- stopWikiHelper?.();
412
- removeParallAccountState(ctx.accountId);
404
+ const wsUrl = resolveWsUrl(account);
405
+ const ws = new ParallWs({
406
+ getTicket: () => client.getWsTicket(),
407
+ wsUrl,
408
+ });
409
+ const orchestratorKey = buildOrchestratorSessionKey(ctx.accountId);
410
+ const sessionsDir = path.join(stateDir, 'agents', 'main', 'sessions');
411
+ const workspaceDir = process.cwd();
412
+ ensureLocalAttachmentGitExclude(workspaceDir);
413
+ const dispatchAdapter = createOpenClawDispatchAdapter({
414
+ core,
415
+ cfg: ctx.cfg,
416
+ accountId: ctx.accountId,
417
+ sessionsDir,
418
+ workspaceDir,
419
+ });
420
+
421
+ const gateway = new ParallAgentGateway({
422
+ accountId: ctx.accountId,
423
+ client,
424
+ ws,
425
+ connectionLabel: wsUrl,
426
+ config: {
427
+ parall_url: config.parall_url,
428
+ api_key: config.api_key,
429
+ org_id: config.org_id,
430
+ },
431
+ agentUserId,
432
+ runtimeType: 'openclaw',
433
+ runtimeKey: orchestratorKey,
434
+ runtimeRef: { hostname: os.hostname(), pid: process.pid },
435
+ dispatchAdapter,
436
+ log: otelLog,
437
+ shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
438
+ forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
439
+ dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
440
+ onConfigUpdate: async () => {
441
+ await fetchAndApplyPlatformConfig(configManagerOpts);
442
+ },
443
+ onSessionReady: async ({ activeSessionId }) => {
444
+ setParallAccountState(ctx.accountId, {
445
+ client,
446
+ apiUrl: config.parall_url,
447
+ apiKey: config.api_key,
448
+ orgId: config.org_id,
449
+ agentUserId,
450
+ activeSessionId,
451
+ sessionBindings: new Map<string, string>(),
452
+ sessionsDir,
453
+ runtimeRef: { hostname: os.hostname(), pid: process.pid },
454
+ wikiMountRoot,
455
+ ws,
456
+ orchestratorSessionKey: orchestratorKey,
457
+ });
458
+ },
459
+ onSessionBinding: async ({ sessionKey, agentSessionId }) => {
460
+ setAgentSessionBinding(ctx.accountId, sessionKey, agentSessionId);
461
+ },
462
+ onBeforeDisconnect: async () => {
463
+ stopWikiHelper?.();
464
+ removeParallAccountState(ctx.accountId);
465
+ },
466
+ onNewSession: () => {
467
+ clearSessionState(sessionsDir, orchestratorKey);
468
+ },
469
+ onSessionStale: () => {
470
+ clearSessionState(sessionsDir, orchestratorKey);
471
+ },
472
+ });
473
+
474
+ try {
475
+ await gateway.run(ctx.abortSignal);
476
+ } finally {
477
+ if (!ctx.abortSignal.aborted) {
478
+ stopWikiHelper?.();
479
+ removeParallAccountState(ctx.accountId);
480
+ }
413
481
  }
482
+ } finally {
483
+ await telemetry.shutdown();
414
484
  }
415
485
  },
416
486
  };