@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/dist/gateway.js CHANGED
@@ -1,20 +1,21 @@
1
- import { ParallAgentGateway, parseShutdownDeadlineMs, } from "@parall/agent-core";
2
- import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
3
- import * as crypto from "node:crypto";
4
- import * as os from "node:os";
5
- import * as path from "node:path";
6
- import { resolveParallAccount } from "./accounts.js";
7
- import { getParallRuntime, removeParallAccountState, setAgentIdentity, setAgentSessionBinding, setDispatchGroupKey, setParallAccountState, } from "./runtime.js";
8
- import { buildOrchestratorSessionKey } from "./session.js";
9
- import { fetchAndApplyPlatformConfig } from "./config-manager.js";
10
- import { startWikiHelper } from "./wiki-helper.js";
11
- import { SessionManager } from "./oc-session.js";
12
- import { cleanupForkSession, clearSessionState, forkOrchestratorSession, resolveSessionId, resolveTranscriptFile } from "./fork.js";
1
+ import { ParallAgentGateway, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, initAgentTelemetry, createOtelLogger, } from '@parall/agent-core';
2
+ import { appendPreparedLocalAttachmentRefs, ensureLocalAttachmentGitExclude, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
3
+ import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
4
+ import * as crypto from 'node:crypto';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ import { resolveParallAccount } from './accounts.js';
8
+ import { getParallRuntime, removeParallAccountState, setAgentIdentity, setAgentSessionBinding, setDispatchGroupKey, setParallAccountState, } from './runtime.js';
9
+ import { buildOrchestratorSessionKey } from './session.js';
10
+ import { fetchAndApplyPlatformConfig } from './config-manager.js';
11
+ import { startWikiHelper } from './wiki-helper.js';
12
+ import { SessionManager } from './oc-session.js';
13
+ import { cleanupForkSession, clearSessionState, forkOrchestratorSession, resolveSessionId, resolveTranscriptFile, } from './fork.js';
13
14
  function resolveWsUrl(account) {
14
15
  if (account.config.ws_url)
15
16
  return account.config.ws_url;
16
- const base = account.config.parall_url.replace(/\/$/, "");
17
- const wsBase = base.replace(/^http/, "ws");
17
+ const base = account.config.parall_url.replace(/\/$/, '');
18
+ const wsBase = base.replace(/^http/, 'ws');
18
19
  return `${wsBase}/ws`;
19
20
  }
20
21
  async function getAgentMeWithLegacyFallback(client, orgId) {
@@ -42,15 +43,15 @@ function buildInboundCtx(core, accountId, event, sessionKey, bodyForAgent, earli
42
43
  To: `parall:orchestrator`,
43
44
  SessionKey: sessionKey,
44
45
  AccountId: accountId,
45
- ChatType: event.targetType ?? "unknown",
46
+ ChatType: event.targetType ?? 'unknown',
46
47
  SenderName: event.senderName,
47
48
  SenderId: event.senderId,
48
- Provider: "parall",
49
- Surface: "parall",
49
+ Provider: 'parall',
50
+ Surface: 'parall',
50
51
  MessageSid: event.messageId,
51
52
  Timestamp: Date.now(),
52
53
  CommandAuthorized: true,
53
- OriginatingChannel: "parall",
54
+ OriginatingChannel: 'parall',
54
55
  OriginatingTo: `parall:orchestrator`,
55
56
  });
56
57
  }
@@ -64,13 +65,13 @@ function buildInboundHistory(events) {
64
65
  meta.push(`[Thread: ${event.threadRootId}]`);
65
66
  if (event.attachments?.length) {
66
67
  for (const att of event.attachments) {
67
- const safeMime = att.mimeType.replace(/[\r\n[\]|]/g, " ").trim();
68
+ const safeMime = att.mimeType.replace(/[\r\n[\]|]/g, ' ').trim();
68
69
  meta.push(`[Attachment: prll://${att.id} | ${safeMime}]`);
69
70
  }
70
71
  }
71
72
  return {
72
73
  sender: event.senderName,
73
- body: meta.length > 0 ? `${meta.join(" ")}\n${event.body}` : event.body,
74
+ body: meta.length > 0 ? `${meta.join(' ')}\n${event.body}` : event.body,
74
75
  };
75
76
  });
76
77
  }
@@ -145,69 +146,95 @@ async function waitForOpenClawSessionId(sessionsDir, sessionKey, timeoutMs = 10_
145
146
  return null;
146
147
  }
147
148
  function createOpenClawDispatchAdapter(opts) {
149
+ const activeStreams = new Map();
148
150
  return {
149
- async *dispatch({ event, earlierEvents = [], bodyForAgent, sessionKey }) {
150
- const stream = createRuntimeEventStream();
151
- let reasoningBuffer = "";
152
- let turnGroupKey = "";
153
- const run = opts.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
154
- ctx: buildInboundCtx(opts.core, opts.accountId, event, sessionKey, bodyForAgent, earlierEvents),
155
- cfg: opts.cfg,
156
- dispatcherOptions: {
157
- deliver: async (payload) => {
158
- const replyText = payload.text?.trim();
159
- if (!replyText)
160
- return;
161
- stream.push({
162
- type: "text",
163
- text: replyText,
164
- project: false,
165
- groupKey: turnGroupKey || undefined,
166
- });
167
- },
168
- onReplyStart: () => {
169
- turnGroupKey = crypto.randomUUID();
170
- setDispatchGroupKey(sessionKey, turnGroupKey);
171
- },
172
- },
173
- replyOptions: {
174
- onReasoningStream: (payload) => {
175
- reasoningBuffer += payload.text ?? "";
176
- },
177
- onReasoningEnd: async () => {
178
- if (!reasoningBuffer)
179
- return;
180
- stream.push({
181
- type: "thinking",
182
- text: reasoningBuffer,
183
- groupKey: turnGroupKey || undefined,
184
- });
185
- reasoningBuffer = "";
186
- },
187
- },
188
- });
189
- run.then(() => stream.end()).catch((err) => stream.fail(err));
190
- let sessionId;
151
+ abortDispatch(sessionKey) {
152
+ activeStreams.get(sessionKey)?.fail(new Error('dispatch deadline exceeded'));
153
+ },
154
+ async *dispatch({ event, earlierEvents = [], bodyForAgent, sessionKey, context }) {
155
+ let promptBody = bodyForAgent;
156
+ let releasePreparedAttachments = () => { };
191
157
  try {
192
- sessionId = await Promise.race([
193
- waitForOpenClawSessionId(opts.sessionsDir, sessionKey),
194
- run.then(() => null, (err) => { throw err; }),
195
- ]);
158
+ const prepared = await appendPreparedLocalAttachmentRefs(bodyForAgent, event, context, {
159
+ workspaceDir: opts.workspaceDir,
160
+ log: context.log,
161
+ });
162
+ promptBody = prepared.body;
163
+ releasePreparedAttachments = pinLocalAttachmentPaths(prepared.attachments.images);
196
164
  }
197
- catch {
198
- sessionId = null;
165
+ catch (err) {
166
+ context.log?.warn?.(`failed to prepare local attachments: ${String(err)}`);
199
167
  }
200
- if (!sessionId) {
201
- stream.fail(new Error(`OpenClaw did not expose a sessionId for ${sessionKey}`));
168
+ try {
169
+ const stream = createRuntimeEventStream();
170
+ activeStreams.set(sessionKey, stream);
171
+ let reasoningBuffer = '';
172
+ let turnGroupKey = '';
173
+ const run = opts.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
174
+ ctx: buildInboundCtx(opts.core, opts.accountId, event, sessionKey, promptBody, earlierEvents),
175
+ cfg: opts.cfg,
176
+ dispatcherOptions: {
177
+ deliver: async (payload) => {
178
+ const replyText = payload.text?.trim();
179
+ if (!replyText)
180
+ return;
181
+ stream.push({
182
+ type: 'text',
183
+ text: replyText,
184
+ project: false,
185
+ groupKey: turnGroupKey || undefined,
186
+ });
187
+ },
188
+ onReplyStart: () => {
189
+ turnGroupKey = crypto.randomUUID();
190
+ setDispatchGroupKey(sessionKey, turnGroupKey);
191
+ },
192
+ },
193
+ replyOptions: {
194
+ onReasoningStream: (payload) => {
195
+ reasoningBuffer += payload.text ?? '';
196
+ },
197
+ onReasoningEnd: async () => {
198
+ if (!reasoningBuffer)
199
+ return;
200
+ stream.push({
201
+ type: 'thinking',
202
+ text: reasoningBuffer,
203
+ groupKey: turnGroupKey || undefined,
204
+ });
205
+ reasoningBuffer = '';
206
+ },
207
+ },
208
+ });
209
+ run.then(() => stream.end()).catch((err) => stream.fail(err));
210
+ let sessionId;
211
+ try {
212
+ sessionId = await Promise.race([
213
+ waitForOpenClawSessionId(opts.sessionsDir, sessionKey),
214
+ run.then(() => null, (err) => {
215
+ throw err;
216
+ }),
217
+ ]);
218
+ }
219
+ catch {
220
+ sessionId = null;
221
+ }
222
+ if (!sessionId) {
223
+ stream.fail(new Error(`OpenClaw did not expose a sessionId for ${sessionKey}`));
224
+ }
225
+ else {
226
+ yield {
227
+ type: 'runtime_session',
228
+ runtimeSessionId: sessionId,
229
+ runtimeLaneKey: sessionKey,
230
+ };
231
+ }
232
+ yield* stream.stream();
202
233
  }
203
- else {
204
- yield {
205
- type: "runtime_session",
206
- runtimeSessionId: sessionId,
207
- runtimeLaneKey: sessionKey,
208
- };
234
+ finally {
235
+ activeStreams.delete(sessionKey);
236
+ releasePreparedAttachments();
209
237
  }
210
- yield* stream.stream();
211
238
  },
212
239
  getBranchPoint(sessionKey) {
213
240
  const transcriptFile = resolveTranscriptFile(opts.sessionsDir, sessionKey);
@@ -234,7 +261,7 @@ function createOpenClawDispatchAdapter(opts) {
234
261
  });
235
262
  },
236
263
  cleanupFork({ fork }) {
237
- if (typeof fork.sessionFile !== "string")
264
+ if (typeof fork.sessionFile !== 'string')
238
265
  return;
239
266
  cleanupForkSession({
240
267
  sessionFile: fork.sessionFile,
@@ -250,7 +277,7 @@ export const parallGateway = {
250
277
  if (!account.enabled)
251
278
  return;
252
279
  if (!account.configured)
253
- throw new Error("Parall account is not configured: parall_url/api_key/org_id required");
280
+ throw new Error('Parall account is not configured: parall_url/api_key/org_id required');
254
281
  const { config } = account;
255
282
  const core = getParallRuntime();
256
283
  const log = ctx.log;
@@ -267,106 +294,122 @@ export const parallGateway = {
267
294
  description: me.agent_profile?.description ?? undefined,
268
295
  });
269
296
  log?.info(`parall[${ctx.accountId}]: authenticated as ${me.display_name} (${agentUserId})`);
270
- const stateDir = process.env.OPENCLAW_STATE_DIR
271
- || path.join(process.env.HOME || "/data", ".openclaw");
272
- const openclawConfigPath = path.join(stateDir, "openclaw.json");
273
- const configManagerOpts = {
274
- client,
275
- stateDir,
276
- configPath: openclawConfigPath,
277
- credentials: { api_key: config.api_key, parall_url: config.parall_url },
278
- log,
279
- };
297
+ const telemetry = await initAgentTelemetry('parall-openclaw-agent', 'openclaw');
298
+ const otelLog = createOtelLogger('agent', 'openclaw-agent');
280
299
  try {
281
- await fetchAndApplyPlatformConfig(configManagerOpts);
282
- }
283
- catch (err) {
284
- log?.warn(`parall[${ctx.accountId}]: platform config fetch failed: ${String(err)}`);
285
- }
286
- let stopWikiHelper = null;
287
- let wikiMountRoot;
288
- try {
289
- const wikiHelper = await startWikiHelper({
290
- accountId: ctx.accountId,
291
- parallUrl: config.parall_url,
292
- apiKey: config.api_key,
293
- orgId: config.org_id,
294
- agentId: agentUserId,
300
+ const stateDir = process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || '/data', '.openclaw');
301
+ const openclawConfigPath = path.join(stateDir, 'openclaw.json');
302
+ const configManagerOpts = {
303
+ client,
295
304
  stateDir,
296
- log,
297
- });
298
- wikiMountRoot = wikiHelper.mountRoot;
299
- stopWikiHelper = wikiHelper.stop;
300
- }
301
- catch (err) {
302
- log?.warn(`parall[${ctx.accountId}]: wiki helper startup failed: ${String(err)}`);
303
- }
304
- const wsUrl = resolveWsUrl(account);
305
- const ws = new ParallWs({
306
- getTicket: () => client.getWsTicket(),
307
- wsUrl,
308
- });
309
- const orchestratorKey = buildOrchestratorSessionKey(ctx.accountId);
310
- const sessionsDir = path.join(stateDir, "agents", "main", "sessions");
311
- const dispatchAdapter = createOpenClawDispatchAdapter({
312
- core,
313
- cfg: ctx.cfg,
314
- accountId: ctx.accountId,
315
- sessionsDir,
316
- });
317
- const gateway = new ParallAgentGateway({
318
- accountId: ctx.accountId,
319
- client,
320
- ws,
321
- connectionLabel: wsUrl,
322
- config: {
323
- parall_url: config.parall_url,
324
- api_key: config.api_key,
325
- org_id: config.org_id,
326
- },
327
- agentUserId,
328
- runtimeType: "openclaw",
329
- runtimeKey: orchestratorKey,
330
- runtimeRef: { hostname: os.hostname(), pid: process.pid },
331
- dispatchAdapter,
332
- log,
333
- shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
334
- onConfigUpdate: async () => {
305
+ configPath: openclawConfigPath,
306
+ credentials: { api_key: config.api_key, parall_url: config.parall_url },
307
+ log: otelLog,
308
+ };
309
+ try {
335
310
  await fetchAndApplyPlatformConfig(configManagerOpts);
336
- },
337
- onSessionReady: async ({ activeSessionId }) => {
338
- setParallAccountState(ctx.accountId, {
339
- client,
340
- apiUrl: config.parall_url,
311
+ }
312
+ catch (err) {
313
+ otelLog.warn(`parall[${ctx.accountId}]: platform config fetch failed: ${String(err)}`);
314
+ }
315
+ let stopWikiHelper = null;
316
+ let wikiMountRoot;
317
+ try {
318
+ const wikiHelper = await startWikiHelper({
319
+ accountId: ctx.accountId,
320
+ parallUrl: config.parall_url,
341
321
  apiKey: config.api_key,
342
322
  orgId: config.org_id,
343
- agentUserId,
344
- activeSessionId,
345
- sessionBindings: new Map(),
346
- sessionsDir,
347
- runtimeRef: { hostname: os.hostname(), pid: process.pid },
348
- wikiMountRoot,
349
- ws,
350
- orchestratorSessionKey: orchestratorKey,
323
+ agentId: agentUserId,
324
+ stateDir,
325
+ log: otelLog,
351
326
  });
352
- },
353
- onSessionBinding: async ({ sessionKey, agentSessionId }) => {
354
- setAgentSessionBinding(ctx.accountId, sessionKey, agentSessionId);
355
- },
356
- onBeforeDisconnect: async () => {
357
- stopWikiHelper?.();
358
- removeParallAccountState(ctx.accountId);
359
- },
360
- onNewSession: () => { clearSessionState(sessionsDir, orchestratorKey); },
361
- });
362
- try {
363
- await gateway.run(ctx.abortSignal);
327
+ wikiMountRoot = wikiHelper.mountRoot;
328
+ stopWikiHelper = wikiHelper.stop;
329
+ }
330
+ catch (err) {
331
+ otelLog.warn(`parall[${ctx.accountId}]: wiki helper startup failed: ${String(err)}`);
332
+ }
333
+ const wsUrl = resolveWsUrl(account);
334
+ const ws = new ParallWs({
335
+ getTicket: () => client.getWsTicket(),
336
+ wsUrl,
337
+ });
338
+ const orchestratorKey = buildOrchestratorSessionKey(ctx.accountId);
339
+ const sessionsDir = path.join(stateDir, 'agents', 'main', 'sessions');
340
+ const workspaceDir = process.cwd();
341
+ ensureLocalAttachmentGitExclude(workspaceDir);
342
+ const dispatchAdapter = createOpenClawDispatchAdapter({
343
+ core,
344
+ cfg: ctx.cfg,
345
+ accountId: ctx.accountId,
346
+ sessionsDir,
347
+ workspaceDir,
348
+ });
349
+ const gateway = new ParallAgentGateway({
350
+ accountId: ctx.accountId,
351
+ client,
352
+ ws,
353
+ connectionLabel: wsUrl,
354
+ config: {
355
+ parall_url: config.parall_url,
356
+ api_key: config.api_key,
357
+ org_id: config.org_id,
358
+ },
359
+ agentUserId,
360
+ runtimeType: 'openclaw',
361
+ runtimeKey: orchestratorKey,
362
+ runtimeRef: { hostname: os.hostname(), pid: process.pid },
363
+ dispatchAdapter,
364
+ log: otelLog,
365
+ shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
366
+ forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
367
+ dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
368
+ onConfigUpdate: async () => {
369
+ await fetchAndApplyPlatformConfig(configManagerOpts);
370
+ },
371
+ onSessionReady: async ({ activeSessionId }) => {
372
+ setParallAccountState(ctx.accountId, {
373
+ client,
374
+ apiUrl: config.parall_url,
375
+ apiKey: config.api_key,
376
+ orgId: config.org_id,
377
+ agentUserId,
378
+ activeSessionId,
379
+ sessionBindings: new Map(),
380
+ sessionsDir,
381
+ runtimeRef: { hostname: os.hostname(), pid: process.pid },
382
+ wikiMountRoot,
383
+ ws,
384
+ orchestratorSessionKey: orchestratorKey,
385
+ });
386
+ },
387
+ onSessionBinding: async ({ sessionKey, agentSessionId }) => {
388
+ setAgentSessionBinding(ctx.accountId, sessionKey, agentSessionId);
389
+ },
390
+ onBeforeDisconnect: async () => {
391
+ stopWikiHelper?.();
392
+ removeParallAccountState(ctx.accountId);
393
+ },
394
+ onNewSession: () => {
395
+ clearSessionState(sessionsDir, orchestratorKey);
396
+ },
397
+ onSessionStale: () => {
398
+ clearSessionState(sessionsDir, orchestratorKey);
399
+ },
400
+ });
401
+ try {
402
+ await gateway.run(ctx.abortSignal);
403
+ }
404
+ finally {
405
+ if (!ctx.abortSignal.aborted) {
406
+ stopWikiHelper?.();
407
+ removeParallAccountState(ctx.accountId);
408
+ }
409
+ }
364
410
  }
365
411
  finally {
366
- if (!ctx.abortSignal.aborted) {
367
- stopWikiHelper?.();
368
- removeParallAccountState(ctx.accountId);
369
- }
412
+ await telemetry.shutdown();
370
413
  }
371
414
  },
372
415
  };
package/dist/hooks.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
2
  export declare function registerParallHooks(api: OpenClawPluginApi): void;
3
3
  //# sourceMappingURL=hooks.d.ts.map
@@ -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;AA4F7D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,QAqIzD"}
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"}
package/dist/hooks.js CHANGED
@@ -1,20 +1,20 @@
1
- import { PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity } from "@parall/agent-core";
2
- import { extractAccountIdFromSessionKey } from "./session.js";
3
- import { clearDispatchGroupKey, getAgentIdentity, getAgentSessionBinding, getDispatchGroupKey, getDispatchMessageId, getParallAccountState, getSessionChatId, setAgentSessionBinding } from "./runtime.js";
4
- import { resolveSessionId } from "./fork.js";
1
+ import { PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity, isParallSendCommand, isParallNoReplyCommand, recordToolCall, recordMessageSend, recordNoReply, } from '@parall/agent-core';
2
+ import { extractAccountIdFromSessionKey } from './session.js';
3
+ import { clearDispatchGroupKey, getAgentIdentity, getAgentSessionBinding, getDispatchGroupKey, getDispatchMessageId, getParallAccountState, getSessionChatId, setAgentSessionBinding, } from './runtime.js';
4
+ import { resolveSessionId } from './fork.js';
5
5
  /**
6
6
  * Map the session's stored target ID (whatever the current dispatch routed
7
7
  * to — chat, task, or schedule) to the `target_type` field on agent_step.
8
8
  * Keeps tool_call / tool_result attribution correct across event types.
9
9
  */
10
10
  function stepTargetType(targetId) {
11
- if (targetId.startsWith("cht_"))
12
- return "chat";
13
- if (targetId.startsWith("tsk_"))
14
- return "task";
15
- if (targetId.startsWith("sch_"))
16
- return "schedule";
17
- return "";
11
+ if (targetId.startsWith('cht_'))
12
+ return 'chat';
13
+ if (targetId.startsWith('tsk_'))
14
+ return 'task';
15
+ if (targetId.startsWith('sch_'))
16
+ return 'schedule';
17
+ return '';
18
18
  }
19
19
  /**
20
20
  * Resolve the ParallClient + orgId + sessionId for a hook context.
@@ -46,7 +46,7 @@ async function resolveClientForHook(sessionKey) {
46
46
  : undefined;
47
47
  try {
48
48
  const session = await state.client.createAgentSession(state.orgId, state.agentUserId, {
49
- runtime_type: "openclaw",
49
+ runtime_type: 'openclaw',
50
50
  runtime_key: sessionKey,
51
51
  runtime_lane_key: sessionKey,
52
52
  runtime_session_id: runtimeSessionId,
@@ -100,14 +100,34 @@ export function registerParallHooks(api) {
100
100
  // registration time, only after the gateway authenticates via getMe().
101
101
  // Uses process-global identity because hosted agents run one-agent-per-pod;
102
102
  // before_prompt_build doesn't receive session context anyway.
103
- api.on("before_prompt_build", () => {
103
+ api.on('before_prompt_build', () => {
104
104
  return {
105
- appendSystemContext: [buildIdentity(getAgentIdentity()), PRLL_CHANNEL_CONTEXT, PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE].join("\n\n"),
105
+ appendSystemContext: [
106
+ buildIdentity(getAgentIdentity()),
107
+ PRLL_CHANNEL_CONTEXT,
108
+ PRLL_BEHAVIOR,
109
+ PRLL_REFERENCE_GUIDE,
110
+ ].join('\n\n'),
106
111
  };
107
112
  });
108
- // before_tool_call -> (1) await step creation to get step ID, (2) ENV injection for exec tool
109
- api.on("before_tool_call", async (event, ctx) => {
113
+ const pendingSendCalls = new Map();
114
+ // before_tool_call -> (1) update dispatch metrics, (2) await step creation, (3) ENV injection
115
+ // OpenClaw tool calls bypass the RuntimeEvent stream (they go through hooks, not the
116
+ // gateway-base for-await loop), so dispatch metrics must be updated here as well.
117
+ api.on('before_tool_call', async (event, ctx) => {
110
118
  const sessionKey = ctx.sessionKey;
119
+ if (sessionKey) {
120
+ recordToolCall(sessionKey);
121
+ if (event.toolName === 'exec' && event.toolCallId) {
122
+ const command = event.params?.command;
123
+ if (isParallSendCommand(command)) {
124
+ pendingSendCalls.set(event.toolCallId, true);
125
+ }
126
+ else if (isParallNoReplyCommand(command)) {
127
+ recordNoReply(sessionKey);
128
+ }
129
+ }
130
+ }
111
131
  // (1) Create tool_call step and await to get step ID
112
132
  let stepId;
113
133
  const resolved = await resolveClientForHook(sessionKey);
@@ -115,14 +135,14 @@ export function registerParallHooks(api) {
115
135
  const groupKey = sessionKey ? getDispatchGroupKey(sessionKey) : undefined;
116
136
  try {
117
137
  const step = await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.sessionId, {
118
- step_type: "tool_call",
138
+ step_type: 'tool_call',
119
139
  target_type: stepTargetType(resolved.chatId),
120
140
  target_id: resolved.chatId || undefined,
121
141
  content: {
122
142
  call_id: event.toolCallId,
123
143
  tool_name: event.toolName,
124
144
  tool_input: event.params ?? {},
125
- status: "running",
145
+ status: 'running',
126
146
  started_at: new Date().toISOString(),
127
147
  },
128
148
  group_key: groupKey,
@@ -135,7 +155,7 @@ export function registerParallHooks(api) {
135
155
  }
136
156
  }
137
157
  // (2) ENV injection — only for the exec (Bash) tool
138
- if (event.toolName !== "exec")
158
+ if (event.toolName !== 'exec')
139
159
  return;
140
160
  if (!sessionKey)
141
161
  return;
@@ -162,7 +182,7 @@ export function registerParallHooks(api) {
162
182
  if (stepId)
163
183
  injectedEnv.PRLL_STEP_ID = stepId;
164
184
  if (chatId) {
165
- if (chatId.startsWith("sch_")) {
185
+ if (chatId.startsWith('sch_')) {
166
186
  // Schedule-triggered dispatch: expose PRLL_SCHEDULE_ID so the agent
167
187
  // can resolve the schedule; deliberately do NOT set PRLL_CHAT_ID,
168
188
  // because schedule events have no chat to reply into, and leaving
@@ -194,21 +214,27 @@ export function registerParallHooks(api) {
194
214
  },
195
215
  };
196
216
  });
197
- // after_tool_call -> send tool_result step to AgentSession
198
- api.on("after_tool_call", async (event, ctx) => {
217
+ // after_tool_call -> track CLI outcomes + send tool_result step to AgentSession
218
+ api.on('after_tool_call', async (event, ctx) => {
219
+ if (ctx.sessionKey && event.toolCallId && pendingSendCalls.delete(event.toolCallId)) {
220
+ recordMessageSend(ctx.sessionKey, !event.error);
221
+ }
199
222
  const resolved = await resolveClientForHook(ctx.sessionKey);
200
223
  if (!resolved?.sessionId || !event.toolCallId)
201
224
  return;
202
225
  try {
203
226
  await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.sessionId, {
204
- step_type: "tool_result",
227
+ step_type: 'tool_result',
205
228
  target_type: stepTargetType(resolved.chatId),
206
229
  target_id: resolved.chatId || undefined,
207
230
  content: {
208
231
  call_id: event.toolCallId,
209
232
  tool_name: event.toolName,
210
- status: event.error ? "error" : "success",
211
- output: event.error ?? (typeof event.result === "string" ? event.result : JSON.stringify(event.result ?? "")),
233
+ status: event.error ? 'error' : 'success',
234
+ output: event.error ??
235
+ (typeof event.result === 'string'
236
+ ? event.result
237
+ : JSON.stringify(event.result ?? '')),
212
238
  duration_ms: event.durationMs ?? 0,
213
239
  collapsible: true,
214
240
  },
@@ -221,7 +247,7 @@ export function registerParallHooks(api) {
221
247
  // agent_end -> end of a dispatch cycle, NOT end of session.
222
248
  // Session lives across dispatches — don't update session status here.
223
249
  // Dispatch-specific IDs are cleaned up in the generic gateway; hooks only own the group key.
224
- api.on("agent_end", async (event, ctx) => {
250
+ api.on('agent_end', async (event, ctx) => {
225
251
  if (ctx.sessionKey) {
226
252
  clearDispatchGroupKey(ctx.sessionKey);
227
253
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
2
  declare const plugin: {
3
3
  id: string;
4
4
  name: string;
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
2
- import { parallPlugin } from "./channel.js";
3
- import { setParallRuntime } from "./runtime.js";
4
- import { registerParallHooks } from "./hooks.js";
1
+ import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk/core';
2
+ import { parallPlugin } from './channel.js';
3
+ import { setParallRuntime } from './runtime.js';
4
+ import { registerParallHooks } from './hooks.js';
5
5
  const plugin = {
6
- id: "parall",
7
- name: "Parall",
8
- description: "Parall IM channel plugin — Agent-Native messaging with tool call visualization.",
6
+ id: 'parall',
7
+ name: 'Parall',
8
+ description: 'Parall IM channel plugin — Agent-Native messaging with tool call visualization.',
9
9
  configSchema: emptyPluginConfigSchema(),
10
10
  register(api) {
11
11
  setParallRuntime(api.runtime);
@@ -1 +1 @@
1
- {"version":3,"file":"oc-session.d.ts","sourceRoot":"","sources":["../src/oc-session.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAkHzC,qBAAa,cAAc;IACzB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,IAAI,CAAmC;IAC/C,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO;IAeP,OAAO,CAAC,IAAI;IA+BZ,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,UAAU;IAsBlB,OAAO,CAAC,OAAO;IAOf,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,MAAM,IAAI,MAAM;IAIhB,aAAa,IAAI,MAAM;IAIvB,YAAY,IAAI,MAAM;IAItB,cAAc,IAAI,MAAM,GAAG,SAAS;IAMpC,OAAO,CAAC,SAAS;IAWjB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA+DzD,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;CAO1C"}
1
+ {"version":3,"file":"oc-session.d.ts","sourceRoot":"","sources":["../src/oc-session.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAsHzC,qBAAa,cAAc;IACzB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,IAAI,CAAmC;IAC/C,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO;IAeP,OAAO,CAAC,IAAI;IA+BZ,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,UAAU;IAsBlB,OAAO,CAAC,OAAO;IAUf,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,MAAM,IAAI,MAAM;IAIhB,aAAa,IAAI,MAAM;IAIvB,YAAY,IAAI,MAAM;IAItB,cAAc,IAAI,MAAM,GAAG,SAAS;IAMpC,OAAO,CAAC,SAAS;IAWjB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA+DzD,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;CAO1C"}