@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/config-manager.d.ts +2 -0
- package/dist/config-manager.d.ts.map +1 -1
- package/dist/config-manager.js +27 -0
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +42 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +65 -0
- package/dist/index.bundle.mjs +1957 -1559
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +3 -3
- package/skills/parall-platform/SKILL.md +24 -5
- package/src/config-manager.ts +39 -0
- package/src/gateway.ts +47 -1
- package/src/hooks.ts +67 -0
- package/src/runtime.ts +17 -0
package/dist/config-manager.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-manager.d.ts","sourceRoot":"","sources":["../src/config-manager.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/config-manager.js
CHANGED
|
@@ -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
|
}
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,uGAAuG;AACvG,KAAK,qBAAqB,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACnF,OAAO,
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,uGAAuG;AACvG,KAAK,qBAAqB,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACnF,OAAO,EASL,KAAK,eAAe,EAGrB,MAAM,oBAAoB,CAAC;AAqB5B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA2KxD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,aAAa,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,eAAe,CA0KlB;AAED,eAAO,MAAM,aAAa,EAAE,qBAAqB,CAAC,qBAAqB,CAqLtE,CAAC"}
|
package/dist/gateway.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParallAgentGateway, 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 }) => {
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;
|
|
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
|
}
|