@alfe.ai/openclaw-chat 0.2.5 → 0.3.1
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/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.d.cts +69 -1
- package/dist/plugin.d.ts +69 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +377 -20
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +366 -21
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as plugin, c as AlfeChannelConfig, l as AlfePluginConfig, s as createAlfeChannelPlugin, u as AlfeResolvedAccount } from "./plugin.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/session-store.d.ts
|
|
4
4
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as plugin, c as AlfeChannelConfig, l as AlfePluginConfig, s as createAlfeChannelPlugin, u as AlfeResolvedAccount } from "./plugin.js";
|
|
2
2
|
|
|
3
3
|
//#region src/session-store.d.ts
|
|
4
4
|
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { i as plugin, o as createAlfeChannelPlugin } from "./plugin2.js";
|
|
2
2
|
export { createAlfeChannelPlugin, plugin as default };
|
package/dist/plugin.cjs
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_plugin = require("./plugin2.cjs");
|
|
6
|
+
exports.admitAgentEvent = require_plugin.admitAgentEvent;
|
|
6
7
|
exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
|
|
8
|
+
exports.buildToolActivity = require_plugin.buildToolActivity;
|
|
7
9
|
exports.default = require_plugin.plugin;
|
|
8
10
|
exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
|
package/dist/plugin.d.cts
CHANGED
|
@@ -268,12 +268,80 @@ interface AgentEventPayload {
|
|
|
268
268
|
data: Record<string, unknown>;
|
|
269
269
|
sessionKey?: string;
|
|
270
270
|
}
|
|
271
|
+
interface RunEventGate {
|
|
272
|
+
runId: string | null;
|
|
273
|
+
sessionKey: string | null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Decide whether an agent-event belongs to THIS turn's run and should be
|
|
277
|
+
* forwarded as a delta / activity frame.
|
|
278
|
+
*
|
|
279
|
+
* Why gate on `runId`, not `sessionKey`:
|
|
280
|
+
* On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
|
|
281
|
+
* with `isControlUiVisible: false` — only the built-in "webchat" surface is
|
|
282
|
+
* treated as control-UI-visible (`isInternalMessageChannel`). The shared
|
|
283
|
+
* agent-event bus then NULLS `sessionKey` on every event of a
|
|
284
|
+
* non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
|
|
285
|
+
* undefined`). So main-run assistant/thinking/tool events arrive with
|
|
286
|
+
* `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
|
|
287
|
+
* silently dropped ALL activity — replies still landed because they come via
|
|
288
|
+
* the `deliver()` callback, not the event bus (the reported "replies arrive,
|
|
289
|
+
* zero activity" symptom). Verified against openclaw@2026.6.10 and
|
|
290
|
+
* back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
|
|
291
|
+
* present on every emitted event.
|
|
292
|
+
*
|
|
293
|
+
* Isolation is preserved: turns to a daemon are serialised
|
|
294
|
+
* (`enqueueAgentTurn`) and the listener is torn down before the next turn
|
|
295
|
+
* dispatches, so the first relevant event locks onto THIS turn's main run.
|
|
296
|
+
* Concurrent heartbeat/cron runs and subagent / side-question runs carry a
|
|
297
|
+
* different `runId` and are excluded — exactly the leakage the gate exists to
|
|
298
|
+
* prevent. When a run IS control-UI-visible and both keys are present, a
|
|
299
|
+
* mismatched `sessionKey` is still rejected as a secondary defence.
|
|
300
|
+
*
|
|
301
|
+
* Exported for unit tests — this gate is the fix's load-bearing logic and is
|
|
302
|
+
* pinned against real 2026.6.10 event shapes.
|
|
303
|
+
*/
|
|
304
|
+
declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
|
|
271
305
|
interface PluginServiceContext {
|
|
272
306
|
config: Record<string, unknown>;
|
|
273
307
|
workspaceDir?: string;
|
|
274
308
|
stateDir: string;
|
|
275
309
|
logger: PluginLogger;
|
|
276
310
|
}
|
|
311
|
+
type ChatActivity = {
|
|
312
|
+
kind: 'thinking';
|
|
313
|
+
text: string;
|
|
314
|
+
delta?: boolean;
|
|
315
|
+
ts?: number;
|
|
316
|
+
seq?: number;
|
|
317
|
+
} | {
|
|
318
|
+
kind: 'tool';
|
|
319
|
+
toolCallId: string;
|
|
320
|
+
name: string;
|
|
321
|
+
phase?: string;
|
|
322
|
+
status: 'running' | 'done' | 'failed';
|
|
323
|
+
summary?: string;
|
|
324
|
+
argsText?: string;
|
|
325
|
+
progressText?: string;
|
|
326
|
+
resultText?: string;
|
|
327
|
+
isError?: boolean;
|
|
328
|
+
durationMs?: number;
|
|
329
|
+
truncated?: {
|
|
330
|
+
args?: boolean;
|
|
331
|
+
progress?: boolean;
|
|
332
|
+
result?: boolean;
|
|
333
|
+
};
|
|
334
|
+
ts?: number;
|
|
335
|
+
seq?: number;
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
339
|
+
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
340
|
+
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
341
|
+
* `(+N steps)` string. `summary` carries OpenClaw's short detail when the
|
|
342
|
+
* event already provides one (title/summary/label/description), trimmed.
|
|
343
|
+
*/
|
|
344
|
+
declare function buildToolActivity(data: Record<string, unknown>): ChatActivity | null;
|
|
277
345
|
declare function stripLeakedToolSummary(text: string): string;
|
|
278
346
|
/**
|
|
279
347
|
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
@@ -328,4 +396,4 @@ declare const plugin: {
|
|
|
328
396
|
deactivate(api: PluginApi): Promise<void>;
|
|
329
397
|
};
|
|
330
398
|
//#endregion
|
|
331
|
-
export {
|
|
399
|
+
export { plugin as a, AlfeChannelConfig as c, buildToolActivity as i, AlfePluginConfig as l, admitAgentEvent as n, stripLeakedToolSummary as o, buildA2ACompletePayload as r, createAlfeChannelPlugin as s, RunEventGate as t, AlfeResolvedAccount as u };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -268,12 +268,80 @@ interface AgentEventPayload {
|
|
|
268
268
|
data: Record<string, unknown>;
|
|
269
269
|
sessionKey?: string;
|
|
270
270
|
}
|
|
271
|
+
interface RunEventGate {
|
|
272
|
+
runId: string | null;
|
|
273
|
+
sessionKey: string | null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Decide whether an agent-event belongs to THIS turn's run and should be
|
|
277
|
+
* forwarded as a delta / activity frame.
|
|
278
|
+
*
|
|
279
|
+
* Why gate on `runId`, not `sessionKey`:
|
|
280
|
+
* On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
|
|
281
|
+
* with `isControlUiVisible: false` — only the built-in "webchat" surface is
|
|
282
|
+
* treated as control-UI-visible (`isInternalMessageChannel`). The shared
|
|
283
|
+
* agent-event bus then NULLS `sessionKey` on every event of a
|
|
284
|
+
* non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
|
|
285
|
+
* undefined`). So main-run assistant/thinking/tool events arrive with
|
|
286
|
+
* `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
|
|
287
|
+
* silently dropped ALL activity — replies still landed because they come via
|
|
288
|
+
* the `deliver()` callback, not the event bus (the reported "replies arrive,
|
|
289
|
+
* zero activity" symptom). Verified against openclaw@2026.6.10 and
|
|
290
|
+
* back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
|
|
291
|
+
* present on every emitted event.
|
|
292
|
+
*
|
|
293
|
+
* Isolation is preserved: turns to a daemon are serialised
|
|
294
|
+
* (`enqueueAgentTurn`) and the listener is torn down before the next turn
|
|
295
|
+
* dispatches, so the first relevant event locks onto THIS turn's main run.
|
|
296
|
+
* Concurrent heartbeat/cron runs and subagent / side-question runs carry a
|
|
297
|
+
* different `runId` and are excluded — exactly the leakage the gate exists to
|
|
298
|
+
* prevent. When a run IS control-UI-visible and both keys are present, a
|
|
299
|
+
* mismatched `sessionKey` is still rejected as a secondary defence.
|
|
300
|
+
*
|
|
301
|
+
* Exported for unit tests — this gate is the fix's load-bearing logic and is
|
|
302
|
+
* pinned against real 2026.6.10 event shapes.
|
|
303
|
+
*/
|
|
304
|
+
declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
|
|
271
305
|
interface PluginServiceContext {
|
|
272
306
|
config: Record<string, unknown>;
|
|
273
307
|
workspaceDir?: string;
|
|
274
308
|
stateDir: string;
|
|
275
309
|
logger: PluginLogger;
|
|
276
310
|
}
|
|
311
|
+
type ChatActivity = {
|
|
312
|
+
kind: 'thinking';
|
|
313
|
+
text: string;
|
|
314
|
+
delta?: boolean;
|
|
315
|
+
ts?: number;
|
|
316
|
+
seq?: number;
|
|
317
|
+
} | {
|
|
318
|
+
kind: 'tool';
|
|
319
|
+
toolCallId: string;
|
|
320
|
+
name: string;
|
|
321
|
+
phase?: string;
|
|
322
|
+
status: 'running' | 'done' | 'failed';
|
|
323
|
+
summary?: string;
|
|
324
|
+
argsText?: string;
|
|
325
|
+
progressText?: string;
|
|
326
|
+
resultText?: string;
|
|
327
|
+
isError?: boolean;
|
|
328
|
+
durationMs?: number;
|
|
329
|
+
truncated?: {
|
|
330
|
+
args?: boolean;
|
|
331
|
+
progress?: boolean;
|
|
332
|
+
result?: boolean;
|
|
333
|
+
};
|
|
334
|
+
ts?: number;
|
|
335
|
+
seq?: number;
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
339
|
+
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
340
|
+
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
341
|
+
* `(+N steps)` string. `summary` carries OpenClaw's short detail when the
|
|
342
|
+
* event already provides one (title/summary/label/description), trimmed.
|
|
343
|
+
*/
|
|
344
|
+
declare function buildToolActivity(data: Record<string, unknown>): ChatActivity | null;
|
|
277
345
|
declare function stripLeakedToolSummary(text: string): string;
|
|
278
346
|
/**
|
|
279
347
|
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
@@ -328,4 +396,4 @@ declare const plugin: {
|
|
|
328
396
|
deactivate(api: PluginApi): Promise<void>;
|
|
329
397
|
};
|
|
330
398
|
//#endregion
|
|
331
|
-
export {
|
|
399
|
+
export { plugin as a, AlfeChannelConfig as c, buildToolActivity as i, AlfePluginConfig as l, admitAgentEvent as n, stripLeakedToolSummary as o, buildA2ACompletePayload as r, createAlfeChannelPlugin as s, RunEventGate as t, AlfeResolvedAccount as u };
|
package/dist/plugin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { a as stripLeakedToolSummary, i as plugin, n as buildA2ACompletePayload, r as buildToolActivity, t as admitAgentEvent } from "./plugin2.js";
|
|
2
|
+
export { admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.cjs
CHANGED
|
@@ -619,6 +619,235 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
619
619
|
};
|
|
620
620
|
}
|
|
621
621
|
//#endregion
|
|
622
|
+
//#region src/activity-serialize.ts
|
|
623
|
+
/**
|
|
624
|
+
* Console-grade tool-activity serialization (Slice 3).
|
|
625
|
+
*
|
|
626
|
+
* The chat plugin forwards a curated activity stream to the chat service.
|
|
627
|
+
* For tool calls we now carry the args / streaming progress / final result
|
|
628
|
+
* so the client can render an OpenClaw-console-grade card. Those payloads
|
|
629
|
+
* arrive from OpenClaw's `onAgentEvent` bus as arbitrary `unknown`-shaped
|
|
630
|
+
* objects (`args` is the raw tool-call arguments; `partialResult` / `result`
|
|
631
|
+
* are tool-result envelopes). We must render them to BOUNDED strings BEFORE
|
|
632
|
+
* they cross the WS to the chat service, because:
|
|
633
|
+
*
|
|
634
|
+
* 1. Daemon WS frames are `maxPayload`-bounded — an uncapped multi-MB tool
|
|
635
|
+
* stdout would drop the WHOLE frame (losing the status transition too),
|
|
636
|
+
* so the cap has to live on the emit side, not the client.
|
|
637
|
+
* 2. The relay is a dumb passthrough and the reducer is pure — neither
|
|
638
|
+
* should have to serialize or sanitize unknown objects. The wire carries
|
|
639
|
+
* display text, never raw objects.
|
|
640
|
+
*
|
|
641
|
+
* OpenClaw's own `sanitizeToolResult` already strips standard image content
|
|
642
|
+
* blocks (`{type:'image', data}` → `{omitted:true}`) and truncates text on
|
|
643
|
+
* the `result`/`partialResult` path — but (a) its helpers are NOT importable
|
|
644
|
+
* from the plugin (openclaw is a runtime peer dep, absent from node_modules),
|
|
645
|
+
* and (b) `args` is NOT sanitized at all and can carry base64 (an inline
|
|
646
|
+
* image arg, a `data:` URI in a write). So we reimplement a defensive
|
|
647
|
+
* strip+cap here and apply it to every field, regardless of upstream
|
|
648
|
+
* sanitization.
|
|
649
|
+
*/
|
|
650
|
+
/** Args render cap — args are usually small; 4KB is generous. */
|
|
651
|
+
const ARGS_CAP = 4 * 1024;
|
|
652
|
+
/** Streaming progress window — keep the newest 4KB (tail-truncated). */
|
|
653
|
+
const PROGRESS_CAP = 4 * 1024;
|
|
654
|
+
/** Final result cap — results can be large (file reads, search); 16KB. */
|
|
655
|
+
const RESULT_CAP = 16 * 1024;
|
|
656
|
+
/**
|
|
657
|
+
* Replace long base64 runs, `data:` URIs, and known image content-block
|
|
658
|
+
* shapes with a placeholder. Runs BEFORE JSON serialization on a deep-cloned
|
|
659
|
+
* structure so we never mutate OpenClaw's live objects.
|
|
660
|
+
*
|
|
661
|
+
* Detection heuristics (all conservative — we only replace clear payloads):
|
|
662
|
+
* - A string that is a `data:` URI → `[data-uri omitted]`.
|
|
663
|
+
* - A string that is a long (≥256 char) run of base64 chars → `[base64 omitted]`.
|
|
664
|
+
* 256 is well above any realistic id/token/hash and below inline media.
|
|
665
|
+
* - An object that looks like an image content block (`type:'image'` with a
|
|
666
|
+
* `data`/`source`/`url` field, OR an Anthropic `{source:{data}}` shape) →
|
|
667
|
+
* the payload field is replaced with `[image omitted]`.
|
|
668
|
+
*/
|
|
669
|
+
const DATA_URI_RE = /^data:[^;,]*(;[^,]*)?,/i;
|
|
670
|
+
const BASE64_BLOB_RE = /^[A-Za-z0-9+/=\r\n]+$/;
|
|
671
|
+
const BASE64_MIN_LEN = 256;
|
|
672
|
+
function looksLikeBase64Blob(s) {
|
|
673
|
+
if (s.length < BASE64_MIN_LEN) return false;
|
|
674
|
+
return s.replace(/[\r\n]/g, "").length >= BASE64_MIN_LEN && BASE64_BLOB_RE.test(s);
|
|
675
|
+
}
|
|
676
|
+
const EMBEDDED_DATA_URI_RE = /data:[^;,\s"'`]{0,64}(?:;[^,\s"'`]{0,32})?,[A-Za-z0-9+/=]{128,}/g;
|
|
677
|
+
const EMBEDDED_BASE64_RE = /[A-Za-z0-9+/]{512,}={0,2}/g;
|
|
678
|
+
/**
|
|
679
|
+
* Post-match filter for EMBEDDED_BASE64_RE: real base64 media is
|
|
680
|
+
* high-entropy — a 512+ char run virtually always mixes lower, upper, and
|
|
681
|
+
* digits. This keeps legitimate long runs (a repeated fill char in test
|
|
682
|
+
* output, an ASCII divider) from being eaten by the strip.
|
|
683
|
+
*/
|
|
684
|
+
function looksHighEntropyBase64(run) {
|
|
685
|
+
return /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run);
|
|
686
|
+
}
|
|
687
|
+
function stripStringMedia(s) {
|
|
688
|
+
if (DATA_URI_RE.test(s)) return "[data-uri omitted]";
|
|
689
|
+
if (looksLikeBase64Blob(s)) return "[base64 omitted]";
|
|
690
|
+
if (s.length >= 128) return s.replace(EMBEDDED_DATA_URI_RE, "[data-uri omitted]").replace(EMBEDDED_BASE64_RE, (m) => looksHighEntropyBase64(m) ? "[base64 omitted]" : m);
|
|
691
|
+
return s;
|
|
692
|
+
}
|
|
693
|
+
const MEDIA_PAYLOAD_KEYS = new Set([
|
|
694
|
+
"data",
|
|
695
|
+
"base64",
|
|
696
|
+
"b64_json",
|
|
697
|
+
"bytes"
|
|
698
|
+
]);
|
|
699
|
+
function isImageBlock(obj) {
|
|
700
|
+
const type = typeof obj.type === "string" ? obj.type.toLowerCase() : "";
|
|
701
|
+
if (type === "image" || type === "image_url" || type === "input_image") return true;
|
|
702
|
+
if (obj.source && typeof obj.source === "object") {
|
|
703
|
+
const src = obj.source;
|
|
704
|
+
if (typeof src.data === "string" || typeof src.url === "string") return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Deep-clone `value` while stripping media. Guards against cycles (returns
|
|
710
|
+
* `'[circular]'`) and caps recursion depth. Non-plain values (functions,
|
|
711
|
+
* symbols) are dropped by JSON semantics later; here we normalize them so the
|
|
712
|
+
* safe-JSON pass can't throw.
|
|
713
|
+
*/
|
|
714
|
+
function stripMediaDeep(value, seen, depth) {
|
|
715
|
+
if (depth > 12) return "[max-depth]";
|
|
716
|
+
if (value === null) return null;
|
|
717
|
+
const t = typeof value;
|
|
718
|
+
if (t === "string") return stripStringMedia(value);
|
|
719
|
+
if (t === "number" || t === "boolean") return value;
|
|
720
|
+
if (t === "bigint") return `${value.toString()}n`;
|
|
721
|
+
if (t === "undefined" || t === "function" || t === "symbol") return void 0;
|
|
722
|
+
const obj = value;
|
|
723
|
+
if (seen.has(obj)) return "[circular]";
|
|
724
|
+
seen.add(obj);
|
|
725
|
+
try {
|
|
726
|
+
if (Array.isArray(obj)) return obj.map((item) => stripMediaDeep(item, seen, depth + 1));
|
|
727
|
+
const record = obj;
|
|
728
|
+
const imageBlock = isImageBlock(record);
|
|
729
|
+
const out = {};
|
|
730
|
+
for (const [key, v] of Object.entries(record)) {
|
|
731
|
+
if (imageBlock && MEDIA_PAYLOAD_KEYS.has(key)) {
|
|
732
|
+
out[key] = "[image omitted]";
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (imageBlock && key === "source" && v && typeof v === "object") {
|
|
736
|
+
const src = { ...v };
|
|
737
|
+
for (const mk of MEDIA_PAYLOAD_KEYS) if (typeof src[mk] === "string") src[mk] = "[image omitted]";
|
|
738
|
+
out[key] = src;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
out[key] = stripMediaDeep(v, seen, depth + 1);
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
} finally {
|
|
745
|
+
seen.delete(obj);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Cycle- and BigInt-safe JSON stringify. Returns `undefined` when there's
|
|
750
|
+
* nothing meaningful to render (null/undefined). A plain string value is
|
|
751
|
+
* returned as-is (unquoted) — args like `"ls -la"` read better raw than
|
|
752
|
+
* `"\"ls -la\""`.
|
|
753
|
+
*/
|
|
754
|
+
function safeJson(value) {
|
|
755
|
+
if (value === null || value === void 0) return void 0;
|
|
756
|
+
const stripped = stripMediaDeep(value, /* @__PURE__ */ new WeakSet(), 0);
|
|
757
|
+
if (stripped === void 0) return void 0;
|
|
758
|
+
if (typeof stripped === "string") return stripped;
|
|
759
|
+
try {
|
|
760
|
+
return JSON.stringify(stripped, null, 2);
|
|
761
|
+
} catch {
|
|
762
|
+
return "[unserializable]";
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** Head-truncate: keep the START of the text (for args/results). */
|
|
766
|
+
function capHead(text, cap) {
|
|
767
|
+
if (text.length <= cap) return {
|
|
768
|
+
text,
|
|
769
|
+
truncated: false
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
text: `${text.slice(0, cap)}\n… [truncated ${String(text.length - cap)} chars]`,
|
|
773
|
+
truncated: true
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Tail-truncate: keep the END of the text (for streaming progress). */
|
|
777
|
+
function capTail(text, cap) {
|
|
778
|
+
if (text.length <= cap) return {
|
|
779
|
+
text,
|
|
780
|
+
truncated: false
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
text: `… [truncated ${String(text.length - cap)} chars]\n${text.slice(text.length - cap)}`,
|
|
784
|
+
truncated: true
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Serialize + strip + head-cap the tool-call `args` (from `phase:start`).
|
|
789
|
+
* Returns `undefined` when there's nothing to show.
|
|
790
|
+
*/
|
|
791
|
+
function serializeArgs(args) {
|
|
792
|
+
const rendered = safeJson(args);
|
|
793
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
794
|
+
return capHead(rendered, ARGS_CAP);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Serialize + strip + head-cap the final tool `result` (from `phase:result`).
|
|
798
|
+
* Prefers rendering the result's text content when it's a standard
|
|
799
|
+
* `{content:[{type:'text',text}]}` envelope (reads far better than the raw
|
|
800
|
+
* JSON), falling back to the full JSON otherwise.
|
|
801
|
+
*/
|
|
802
|
+
function serializeResult(result) {
|
|
803
|
+
if (isEmptyEnvelope(result)) return void 0;
|
|
804
|
+
const rendered = extractResultText(result) ?? safeJson(result);
|
|
805
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
806
|
+
return capHead(stripStringMedia(rendered), RESULT_CAP);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Serialize + strip + TAIL-cap a streaming `partialResult` (from
|
|
810
|
+
* `phase:update`). Tail-truncation keeps the NEWEST output — for a bash/exec
|
|
811
|
+
* stream the tail is what the user cares about. Whether OpenClaw's
|
|
812
|
+
* `partialResult` is cumulative or incremental is version-dependent; treating
|
|
813
|
+
* it as "the latest window" (overwrite, not append — see the reducer) is
|
|
814
|
+
* correct for both: a cumulative stream's newest frame already contains the
|
|
815
|
+
* full window (tail-capped), and an incremental stream's newest frame is the
|
|
816
|
+
* latest chunk.
|
|
817
|
+
*/
|
|
818
|
+
function serializeProgress(partial) {
|
|
819
|
+
if (isEmptyEnvelope(partial)) return void 0;
|
|
820
|
+
const rendered = extractResultText(partial) ?? safeJson(partial);
|
|
821
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
822
|
+
return capTail(stripStringMedia(rendered), PROGRESS_CAP);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Pull the concatenated `text` blocks out of a standard tool-result envelope
|
|
826
|
+
* (`{content:[{type:'text',text}, {type:'image',omitted:true}]}`). Returns
|
|
827
|
+
* `undefined` when the shape isn't a text-bearing envelope, so callers fall
|
|
828
|
+
* back to full-JSON rendering.
|
|
829
|
+
*/
|
|
830
|
+
/** True for a tool-result envelope with an empty `content` array. */
|
|
831
|
+
function isEmptyEnvelope(value) {
|
|
832
|
+
if (!value || typeof value !== "object") return false;
|
|
833
|
+
const content = value.content;
|
|
834
|
+
return Array.isArray(content) && content.length === 0;
|
|
835
|
+
}
|
|
836
|
+
function extractResultText(result) {
|
|
837
|
+
if (!result || typeof result !== "object") return void 0;
|
|
838
|
+
const content = result.content;
|
|
839
|
+
if (!Array.isArray(content)) return void 0;
|
|
840
|
+
const parts = [];
|
|
841
|
+
for (const item of content) {
|
|
842
|
+
if (!item || typeof item !== "object") continue;
|
|
843
|
+
const entry = item;
|
|
844
|
+
if (entry.type === "text" && typeof entry.text === "string") parts.push(entry.text);
|
|
845
|
+
else if (entry.type === "image" || entry.omitted === true) parts.push("[image omitted]");
|
|
846
|
+
}
|
|
847
|
+
if (parts.length === 0) return void 0;
|
|
848
|
+
return parts.join("\n");
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
622
851
|
//#region src/plugin.ts
|
|
623
852
|
/**
|
|
624
853
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -635,6 +864,45 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
635
864
|
*/
|
|
636
865
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
637
866
|
const pkg = require$1("../package.json");
|
|
867
|
+
/**
|
|
868
|
+
* Decide whether an agent-event belongs to THIS turn's run and should be
|
|
869
|
+
* forwarded as a delta / activity frame.
|
|
870
|
+
*
|
|
871
|
+
* Why gate on `runId`, not `sessionKey`:
|
|
872
|
+
* On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
|
|
873
|
+
* with `isControlUiVisible: false` — only the built-in "webchat" surface is
|
|
874
|
+
* treated as control-UI-visible (`isInternalMessageChannel`). The shared
|
|
875
|
+
* agent-event bus then NULLS `sessionKey` on every event of a
|
|
876
|
+
* non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
|
|
877
|
+
* undefined`). So main-run assistant/thinking/tool events arrive with
|
|
878
|
+
* `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
|
|
879
|
+
* silently dropped ALL activity — replies still landed because they come via
|
|
880
|
+
* the `deliver()` callback, not the event bus (the reported "replies arrive,
|
|
881
|
+
* zero activity" symptom). Verified against openclaw@2026.6.10 and
|
|
882
|
+
* back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
|
|
883
|
+
* present on every emitted event.
|
|
884
|
+
*
|
|
885
|
+
* Isolation is preserved: turns to a daemon are serialised
|
|
886
|
+
* (`enqueueAgentTurn`) and the listener is torn down before the next turn
|
|
887
|
+
* dispatches, so the first relevant event locks onto THIS turn's main run.
|
|
888
|
+
* Concurrent heartbeat/cron runs and subagent / side-question runs carry a
|
|
889
|
+
* different `runId` and are excluded — exactly the leakage the gate exists to
|
|
890
|
+
* prevent. When a run IS control-UI-visible and both keys are present, a
|
|
891
|
+
* mismatched `sessionKey` is still rejected as a secondary defence.
|
|
892
|
+
*
|
|
893
|
+
* Exported for unit tests — this gate is the fix's load-bearing logic and is
|
|
894
|
+
* pinned against real 2026.6.10 event shapes.
|
|
895
|
+
*/
|
|
896
|
+
function admitAgentEvent(gate, evt) {
|
|
897
|
+
if (!(evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool")) return false;
|
|
898
|
+
if (gate.runId === null) {
|
|
899
|
+
gate.runId = evt.runId;
|
|
900
|
+
if (evt.sessionKey) gate.sessionKey ??= evt.sessionKey;
|
|
901
|
+
}
|
|
902
|
+
if (evt.runId !== gate.runId) return false;
|
|
903
|
+
if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
638
906
|
function asString(v) {
|
|
639
907
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
640
908
|
}
|
|
@@ -668,17 +936,58 @@ function buildToolActivity(data) {
|
|
|
668
936
|
const rawStatus = (phase ?? "").toLowerCase();
|
|
669
937
|
let status;
|
|
670
938
|
if (rawStatus === "error" || rawStatus === "failed" || data.error != null || data.isError === true) status = "failed";
|
|
671
|
-
else if (rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
939
|
+
else if (rawStatus === "result" || rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
672
940
|
else status = "running";
|
|
673
941
|
const summaryRaw = asString(data.summary) ?? asString(data.title) ?? asString(data.label) ?? asString(data.description);
|
|
674
942
|
const summary = summaryRaw ? summaryRaw.slice(0, 200) : void 0;
|
|
943
|
+
const truncated = {};
|
|
944
|
+
let argsText;
|
|
945
|
+
let progressText;
|
|
946
|
+
let resultText;
|
|
947
|
+
let isError;
|
|
948
|
+
if (rawStatus === "start" || rawStatus === "running" || rawStatus === "") {
|
|
949
|
+
const a = serializeArgs(data.args ?? data.arguments ?? data.input);
|
|
950
|
+
if (a) {
|
|
951
|
+
argsText = a.text;
|
|
952
|
+
if (a.truncated) truncated.args = true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const partial = data.partialResult ?? data.partial ?? data.delta;
|
|
956
|
+
if (partial !== void 0) {
|
|
957
|
+
const p = serializeProgress(partial);
|
|
958
|
+
if (p) {
|
|
959
|
+
progressText = p.text;
|
|
960
|
+
if (p.truncated) truncated.progress = true;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const resultRaw = data.result ?? data.output;
|
|
964
|
+
if (resultRaw !== void 0 || status === "done" || status === "failed") {
|
|
965
|
+
if (resultRaw !== void 0) {
|
|
966
|
+
const r = serializeResult(resultRaw);
|
|
967
|
+
if (r) {
|
|
968
|
+
resultText = r.text;
|
|
969
|
+
if (r.truncated) truncated.result = true;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (data.isError === true || status === "failed") isError = true;
|
|
973
|
+
else if (data.isError === false) isError = false;
|
|
974
|
+
}
|
|
975
|
+
const durationRaw = data.durationMs ?? data.duration;
|
|
976
|
+
const durationMs = typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : void 0;
|
|
977
|
+
const hasTruncated = truncated.args === true || truncated.progress === true || truncated.result === true;
|
|
675
978
|
return {
|
|
676
979
|
kind: "tool",
|
|
677
980
|
toolCallId,
|
|
678
981
|
name,
|
|
679
982
|
...phase ? { phase } : {},
|
|
680
983
|
status,
|
|
681
|
-
...summary ? { summary } : {}
|
|
984
|
+
...summary ? { summary } : {},
|
|
985
|
+
...argsText !== void 0 ? { argsText } : {},
|
|
986
|
+
...progressText !== void 0 ? { progressText } : {},
|
|
987
|
+
...resultText !== void 0 ? { resultText } : {},
|
|
988
|
+
...isError !== void 0 ? { isError } : {},
|
|
989
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
990
|
+
...hasTruncated ? { truncated } : {}
|
|
682
991
|
};
|
|
683
992
|
}
|
|
684
993
|
/**
|
|
@@ -858,11 +1167,25 @@ async function handleAgentRequest(request, log) {
|
|
|
858
1167
|
const sessionId = conversationId ?? legacySessionKey;
|
|
859
1168
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
860
1169
|
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
861
|
-
|
|
1170
|
+
const runGate = {
|
|
1171
|
+
runId: null,
|
|
1172
|
+
sessionKey: null
|
|
1173
|
+
};
|
|
1174
|
+
const UPDATE_THROTTLE_MS = 250;
|
|
1175
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
1176
|
+
const flushToolUpdate = (toolCallId) => {
|
|
1177
|
+
const pending = pendingUpdates.get(toolCallId);
|
|
1178
|
+
if (!pending) return;
|
|
1179
|
+
clearTimeout(pending.timer);
|
|
1180
|
+
pendingUpdates.delete(toolCallId);
|
|
1181
|
+
pending.send();
|
|
1182
|
+
};
|
|
1183
|
+
const clearAllToolUpdates = () => {
|
|
1184
|
+
for (const pending of pendingUpdates.values()) clearTimeout(pending.timer);
|
|
1185
|
+
pendingUpdates.clear();
|
|
1186
|
+
};
|
|
862
1187
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
863
|
-
if (!evt
|
|
864
|
-
if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
|
|
865
|
-
if (evt.sessionKey !== resolvedOpenClawKey) return;
|
|
1188
|
+
if (!admitAgentEvent(runGate, evt)) return;
|
|
866
1189
|
if (evt.stream === "assistant") {
|
|
867
1190
|
chatClient?.sendEvent("chat", {
|
|
868
1191
|
runId: evt.runId,
|
|
@@ -890,17 +1213,38 @@ async function handleAgentRequest(request, log) {
|
|
|
890
1213
|
}
|
|
891
1214
|
if (evt.stream === "tool") {
|
|
892
1215
|
const activity = buildToolActivity(evt.data);
|
|
893
|
-
if (activity
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1216
|
+
if (activity?.kind !== "tool") return;
|
|
1217
|
+
const toolCallId = activity.toolCallId;
|
|
1218
|
+
const send = () => {
|
|
1219
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1220
|
+
runId: evt.runId,
|
|
1221
|
+
sessionKey: legacySessionKey,
|
|
1222
|
+
seq: evt.seq,
|
|
1223
|
+
state: "activity",
|
|
1224
|
+
activity: {
|
|
1225
|
+
...activity,
|
|
1226
|
+
ts: evt.ts,
|
|
1227
|
+
seq: evt.seq
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
};
|
|
1231
|
+
const rawPhase = (activity.phase ?? "").toLowerCase();
|
|
1232
|
+
if (rawPhase === "update" || activity.status === "running" && rawPhase !== "start" && rawPhase !== "") {
|
|
1233
|
+
const existing = pendingUpdates.get(toolCallId);
|
|
1234
|
+
if (existing) existing.send = send;
|
|
1235
|
+
else {
|
|
1236
|
+
const timer = setTimeout(() => {
|
|
1237
|
+
flushToolUpdate(toolCallId);
|
|
1238
|
+
}, UPDATE_THROTTLE_MS);
|
|
1239
|
+
pendingUpdates.set(toolCallId, {
|
|
1240
|
+
timer,
|
|
1241
|
+
send
|
|
1242
|
+
});
|
|
902
1243
|
}
|
|
903
|
-
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
flushToolUpdate(toolCallId);
|
|
1247
|
+
send();
|
|
904
1248
|
return;
|
|
905
1249
|
}
|
|
906
1250
|
});
|
|
@@ -919,7 +1263,7 @@ async function handleAgentRequest(request, log) {
|
|
|
919
1263
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
920
1264
|
let a2aResponseBuffer = "";
|
|
921
1265
|
if (isA2A) resetA2AEndSignal();
|
|
922
|
-
|
|
1266
|
+
runGate.sessionKey = (await dispatchInbound({
|
|
923
1267
|
cfg,
|
|
924
1268
|
runtime: { channel: runtime.channel },
|
|
925
1269
|
channel: "alfe",
|
|
@@ -972,7 +1316,7 @@ async function handleAgentRequest(request, log) {
|
|
|
972
1316
|
chatClient?.notify("agent-message", {
|
|
973
1317
|
conversationId: conversationId ?? legacySessionKey,
|
|
974
1318
|
text: responseText,
|
|
975
|
-
sessionKey:
|
|
1319
|
+
sessionKey: runGate.sessionKey ?? legacySessionKey,
|
|
976
1320
|
messageId: chatMessageId ?? request.id,
|
|
977
1321
|
...mediaUrls.length ? { mediaUrls } : {}
|
|
978
1322
|
});
|
|
@@ -993,14 +1337,15 @@ async function handleAgentRequest(request, log) {
|
|
|
993
1337
|
resolved: a2aResolved
|
|
994
1338
|
}));
|
|
995
1339
|
}
|
|
996
|
-
chatClient?.sendResponse(request.id, true, { sessionKey:
|
|
997
|
-
log.info(`Agent dispatch complete: sessionKey=${
|
|
1340
|
+
chatClient?.sendResponse(request.id, true, { sessionKey: runGate.sessionKey });
|
|
1341
|
+
log.info(`Agent dispatch complete: sessionKey=${runGate.sessionKey}`);
|
|
998
1342
|
} catch (err) {
|
|
999
1343
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1000
1344
|
log.error(`Agent dispatch failed: ${errMsg}`);
|
|
1001
1345
|
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
1002
1346
|
} finally {
|
|
1003
1347
|
unsubscribe();
|
|
1348
|
+
clearAllToolUpdates();
|
|
1004
1349
|
if (isA2A) disarmA2AEndSignal();
|
|
1005
1350
|
}
|
|
1006
1351
|
}
|
|
@@ -1229,12 +1574,24 @@ const plugin = {
|
|
|
1229
1574
|
}
|
|
1230
1575
|
};
|
|
1231
1576
|
//#endregion
|
|
1577
|
+
Object.defineProperty(exports, "admitAgentEvent", {
|
|
1578
|
+
enumerable: true,
|
|
1579
|
+
get: function() {
|
|
1580
|
+
return admitAgentEvent;
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1232
1583
|
Object.defineProperty(exports, "buildA2ACompletePayload", {
|
|
1233
1584
|
enumerable: true,
|
|
1234
1585
|
get: function() {
|
|
1235
1586
|
return buildA2ACompletePayload;
|
|
1236
1587
|
}
|
|
1237
1588
|
});
|
|
1589
|
+
Object.defineProperty(exports, "buildToolActivity", {
|
|
1590
|
+
enumerable: true,
|
|
1591
|
+
get: function() {
|
|
1592
|
+
return buildToolActivity;
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1238
1595
|
Object.defineProperty(exports, "createAlfeChannelPlugin", {
|
|
1239
1596
|
enumerable: true,
|
|
1240
1597
|
get: function() {
|
package/dist/plugin2.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { a as plugin, i as buildToolActivity, n as admitAgentEvent, o as stripLeakedToolSummary, r as buildA2ACompletePayload, t as RunEventGate } from "./plugin.cjs";
|
|
2
|
+
export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { a as plugin, i as buildToolActivity, n as admitAgentEvent, o as stripLeakedToolSummary, r as buildA2ACompletePayload, t as RunEventGate } from "./plugin.js";
|
|
2
|
+
export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.js
CHANGED
|
@@ -619,6 +619,235 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
619
619
|
};
|
|
620
620
|
}
|
|
621
621
|
//#endregion
|
|
622
|
+
//#region src/activity-serialize.ts
|
|
623
|
+
/**
|
|
624
|
+
* Console-grade tool-activity serialization (Slice 3).
|
|
625
|
+
*
|
|
626
|
+
* The chat plugin forwards a curated activity stream to the chat service.
|
|
627
|
+
* For tool calls we now carry the args / streaming progress / final result
|
|
628
|
+
* so the client can render an OpenClaw-console-grade card. Those payloads
|
|
629
|
+
* arrive from OpenClaw's `onAgentEvent` bus as arbitrary `unknown`-shaped
|
|
630
|
+
* objects (`args` is the raw tool-call arguments; `partialResult` / `result`
|
|
631
|
+
* are tool-result envelopes). We must render them to BOUNDED strings BEFORE
|
|
632
|
+
* they cross the WS to the chat service, because:
|
|
633
|
+
*
|
|
634
|
+
* 1. Daemon WS frames are `maxPayload`-bounded — an uncapped multi-MB tool
|
|
635
|
+
* stdout would drop the WHOLE frame (losing the status transition too),
|
|
636
|
+
* so the cap has to live on the emit side, not the client.
|
|
637
|
+
* 2. The relay is a dumb passthrough and the reducer is pure — neither
|
|
638
|
+
* should have to serialize or sanitize unknown objects. The wire carries
|
|
639
|
+
* display text, never raw objects.
|
|
640
|
+
*
|
|
641
|
+
* OpenClaw's own `sanitizeToolResult` already strips standard image content
|
|
642
|
+
* blocks (`{type:'image', data}` → `{omitted:true}`) and truncates text on
|
|
643
|
+
* the `result`/`partialResult` path — but (a) its helpers are NOT importable
|
|
644
|
+
* from the plugin (openclaw is a runtime peer dep, absent from node_modules),
|
|
645
|
+
* and (b) `args` is NOT sanitized at all and can carry base64 (an inline
|
|
646
|
+
* image arg, a `data:` URI in a write). So we reimplement a defensive
|
|
647
|
+
* strip+cap here and apply it to every field, regardless of upstream
|
|
648
|
+
* sanitization.
|
|
649
|
+
*/
|
|
650
|
+
/** Args render cap — args are usually small; 4KB is generous. */
|
|
651
|
+
const ARGS_CAP = 4 * 1024;
|
|
652
|
+
/** Streaming progress window — keep the newest 4KB (tail-truncated). */
|
|
653
|
+
const PROGRESS_CAP = 4 * 1024;
|
|
654
|
+
/** Final result cap — results can be large (file reads, search); 16KB. */
|
|
655
|
+
const RESULT_CAP = 16 * 1024;
|
|
656
|
+
/**
|
|
657
|
+
* Replace long base64 runs, `data:` URIs, and known image content-block
|
|
658
|
+
* shapes with a placeholder. Runs BEFORE JSON serialization on a deep-cloned
|
|
659
|
+
* structure so we never mutate OpenClaw's live objects.
|
|
660
|
+
*
|
|
661
|
+
* Detection heuristics (all conservative — we only replace clear payloads):
|
|
662
|
+
* - A string that is a `data:` URI → `[data-uri omitted]`.
|
|
663
|
+
* - A string that is a long (≥256 char) run of base64 chars → `[base64 omitted]`.
|
|
664
|
+
* 256 is well above any realistic id/token/hash and below inline media.
|
|
665
|
+
* - An object that looks like an image content block (`type:'image'` with a
|
|
666
|
+
* `data`/`source`/`url` field, OR an Anthropic `{source:{data}}` shape) →
|
|
667
|
+
* the payload field is replaced with `[image omitted]`.
|
|
668
|
+
*/
|
|
669
|
+
const DATA_URI_RE = /^data:[^;,]*(;[^,]*)?,/i;
|
|
670
|
+
const BASE64_BLOB_RE = /^[A-Za-z0-9+/=\r\n]+$/;
|
|
671
|
+
const BASE64_MIN_LEN = 256;
|
|
672
|
+
function looksLikeBase64Blob(s) {
|
|
673
|
+
if (s.length < BASE64_MIN_LEN) return false;
|
|
674
|
+
return s.replace(/[\r\n]/g, "").length >= BASE64_MIN_LEN && BASE64_BLOB_RE.test(s);
|
|
675
|
+
}
|
|
676
|
+
const EMBEDDED_DATA_URI_RE = /data:[^;,\s"'`]{0,64}(?:;[^,\s"'`]{0,32})?,[A-Za-z0-9+/=]{128,}/g;
|
|
677
|
+
const EMBEDDED_BASE64_RE = /[A-Za-z0-9+/]{512,}={0,2}/g;
|
|
678
|
+
/**
|
|
679
|
+
* Post-match filter for EMBEDDED_BASE64_RE: real base64 media is
|
|
680
|
+
* high-entropy — a 512+ char run virtually always mixes lower, upper, and
|
|
681
|
+
* digits. This keeps legitimate long runs (a repeated fill char in test
|
|
682
|
+
* output, an ASCII divider) from being eaten by the strip.
|
|
683
|
+
*/
|
|
684
|
+
function looksHighEntropyBase64(run) {
|
|
685
|
+
return /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run);
|
|
686
|
+
}
|
|
687
|
+
function stripStringMedia(s) {
|
|
688
|
+
if (DATA_URI_RE.test(s)) return "[data-uri omitted]";
|
|
689
|
+
if (looksLikeBase64Blob(s)) return "[base64 omitted]";
|
|
690
|
+
if (s.length >= 128) return s.replace(EMBEDDED_DATA_URI_RE, "[data-uri omitted]").replace(EMBEDDED_BASE64_RE, (m) => looksHighEntropyBase64(m) ? "[base64 omitted]" : m);
|
|
691
|
+
return s;
|
|
692
|
+
}
|
|
693
|
+
const MEDIA_PAYLOAD_KEYS = new Set([
|
|
694
|
+
"data",
|
|
695
|
+
"base64",
|
|
696
|
+
"b64_json",
|
|
697
|
+
"bytes"
|
|
698
|
+
]);
|
|
699
|
+
function isImageBlock(obj) {
|
|
700
|
+
const type = typeof obj.type === "string" ? obj.type.toLowerCase() : "";
|
|
701
|
+
if (type === "image" || type === "image_url" || type === "input_image") return true;
|
|
702
|
+
if (obj.source && typeof obj.source === "object") {
|
|
703
|
+
const src = obj.source;
|
|
704
|
+
if (typeof src.data === "string" || typeof src.url === "string") return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Deep-clone `value` while stripping media. Guards against cycles (returns
|
|
710
|
+
* `'[circular]'`) and caps recursion depth. Non-plain values (functions,
|
|
711
|
+
* symbols) are dropped by JSON semantics later; here we normalize them so the
|
|
712
|
+
* safe-JSON pass can't throw.
|
|
713
|
+
*/
|
|
714
|
+
function stripMediaDeep(value, seen, depth) {
|
|
715
|
+
if (depth > 12) return "[max-depth]";
|
|
716
|
+
if (value === null) return null;
|
|
717
|
+
const t = typeof value;
|
|
718
|
+
if (t === "string") return stripStringMedia(value);
|
|
719
|
+
if (t === "number" || t === "boolean") return value;
|
|
720
|
+
if (t === "bigint") return `${value.toString()}n`;
|
|
721
|
+
if (t === "undefined" || t === "function" || t === "symbol") return void 0;
|
|
722
|
+
const obj = value;
|
|
723
|
+
if (seen.has(obj)) return "[circular]";
|
|
724
|
+
seen.add(obj);
|
|
725
|
+
try {
|
|
726
|
+
if (Array.isArray(obj)) return obj.map((item) => stripMediaDeep(item, seen, depth + 1));
|
|
727
|
+
const record = obj;
|
|
728
|
+
const imageBlock = isImageBlock(record);
|
|
729
|
+
const out = {};
|
|
730
|
+
for (const [key, v] of Object.entries(record)) {
|
|
731
|
+
if (imageBlock && MEDIA_PAYLOAD_KEYS.has(key)) {
|
|
732
|
+
out[key] = "[image omitted]";
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (imageBlock && key === "source" && v && typeof v === "object") {
|
|
736
|
+
const src = { ...v };
|
|
737
|
+
for (const mk of MEDIA_PAYLOAD_KEYS) if (typeof src[mk] === "string") src[mk] = "[image omitted]";
|
|
738
|
+
out[key] = src;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
out[key] = stripMediaDeep(v, seen, depth + 1);
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
} finally {
|
|
745
|
+
seen.delete(obj);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Cycle- and BigInt-safe JSON stringify. Returns `undefined` when there's
|
|
750
|
+
* nothing meaningful to render (null/undefined). A plain string value is
|
|
751
|
+
* returned as-is (unquoted) — args like `"ls -la"` read better raw than
|
|
752
|
+
* `"\"ls -la\""`.
|
|
753
|
+
*/
|
|
754
|
+
function safeJson(value) {
|
|
755
|
+
if (value === null || value === void 0) return void 0;
|
|
756
|
+
const stripped = stripMediaDeep(value, /* @__PURE__ */ new WeakSet(), 0);
|
|
757
|
+
if (stripped === void 0) return void 0;
|
|
758
|
+
if (typeof stripped === "string") return stripped;
|
|
759
|
+
try {
|
|
760
|
+
return JSON.stringify(stripped, null, 2);
|
|
761
|
+
} catch {
|
|
762
|
+
return "[unserializable]";
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** Head-truncate: keep the START of the text (for args/results). */
|
|
766
|
+
function capHead(text, cap) {
|
|
767
|
+
if (text.length <= cap) return {
|
|
768
|
+
text,
|
|
769
|
+
truncated: false
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
text: `${text.slice(0, cap)}\n… [truncated ${String(text.length - cap)} chars]`,
|
|
773
|
+
truncated: true
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Tail-truncate: keep the END of the text (for streaming progress). */
|
|
777
|
+
function capTail(text, cap) {
|
|
778
|
+
if (text.length <= cap) return {
|
|
779
|
+
text,
|
|
780
|
+
truncated: false
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
text: `… [truncated ${String(text.length - cap)} chars]\n${text.slice(text.length - cap)}`,
|
|
784
|
+
truncated: true
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Serialize + strip + head-cap the tool-call `args` (from `phase:start`).
|
|
789
|
+
* Returns `undefined` when there's nothing to show.
|
|
790
|
+
*/
|
|
791
|
+
function serializeArgs(args) {
|
|
792
|
+
const rendered = safeJson(args);
|
|
793
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
794
|
+
return capHead(rendered, ARGS_CAP);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Serialize + strip + head-cap the final tool `result` (from `phase:result`).
|
|
798
|
+
* Prefers rendering the result's text content when it's a standard
|
|
799
|
+
* `{content:[{type:'text',text}]}` envelope (reads far better than the raw
|
|
800
|
+
* JSON), falling back to the full JSON otherwise.
|
|
801
|
+
*/
|
|
802
|
+
function serializeResult(result) {
|
|
803
|
+
if (isEmptyEnvelope(result)) return void 0;
|
|
804
|
+
const rendered = extractResultText(result) ?? safeJson(result);
|
|
805
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
806
|
+
return capHead(stripStringMedia(rendered), RESULT_CAP);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Serialize + strip + TAIL-cap a streaming `partialResult` (from
|
|
810
|
+
* `phase:update`). Tail-truncation keeps the NEWEST output — for a bash/exec
|
|
811
|
+
* stream the tail is what the user cares about. Whether OpenClaw's
|
|
812
|
+
* `partialResult` is cumulative or incremental is version-dependent; treating
|
|
813
|
+
* it as "the latest window" (overwrite, not append — see the reducer) is
|
|
814
|
+
* correct for both: a cumulative stream's newest frame already contains the
|
|
815
|
+
* full window (tail-capped), and an incremental stream's newest frame is the
|
|
816
|
+
* latest chunk.
|
|
817
|
+
*/
|
|
818
|
+
function serializeProgress(partial) {
|
|
819
|
+
if (isEmptyEnvelope(partial)) return void 0;
|
|
820
|
+
const rendered = extractResultText(partial) ?? safeJson(partial);
|
|
821
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
822
|
+
return capTail(stripStringMedia(rendered), PROGRESS_CAP);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Pull the concatenated `text` blocks out of a standard tool-result envelope
|
|
826
|
+
* (`{content:[{type:'text',text}, {type:'image',omitted:true}]}`). Returns
|
|
827
|
+
* `undefined` when the shape isn't a text-bearing envelope, so callers fall
|
|
828
|
+
* back to full-JSON rendering.
|
|
829
|
+
*/
|
|
830
|
+
/** True for a tool-result envelope with an empty `content` array. */
|
|
831
|
+
function isEmptyEnvelope(value) {
|
|
832
|
+
if (!value || typeof value !== "object") return false;
|
|
833
|
+
const content = value.content;
|
|
834
|
+
return Array.isArray(content) && content.length === 0;
|
|
835
|
+
}
|
|
836
|
+
function extractResultText(result) {
|
|
837
|
+
if (!result || typeof result !== "object") return void 0;
|
|
838
|
+
const content = result.content;
|
|
839
|
+
if (!Array.isArray(content)) return void 0;
|
|
840
|
+
const parts = [];
|
|
841
|
+
for (const item of content) {
|
|
842
|
+
if (!item || typeof item !== "object") continue;
|
|
843
|
+
const entry = item;
|
|
844
|
+
if (entry.type === "text" && typeof entry.text === "string") parts.push(entry.text);
|
|
845
|
+
else if (entry.type === "image" || entry.omitted === true) parts.push("[image omitted]");
|
|
846
|
+
}
|
|
847
|
+
if (parts.length === 0) return void 0;
|
|
848
|
+
return parts.join("\n");
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
622
851
|
//#region src/plugin.ts
|
|
623
852
|
/**
|
|
624
853
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -635,6 +864,45 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
635
864
|
*/
|
|
636
865
|
const require = createRequire(import.meta.url);
|
|
637
866
|
const pkg = require("../package.json");
|
|
867
|
+
/**
|
|
868
|
+
* Decide whether an agent-event belongs to THIS turn's run and should be
|
|
869
|
+
* forwarded as a delta / activity frame.
|
|
870
|
+
*
|
|
871
|
+
* Why gate on `runId`, not `sessionKey`:
|
|
872
|
+
* On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
|
|
873
|
+
* with `isControlUiVisible: false` — only the built-in "webchat" surface is
|
|
874
|
+
* treated as control-UI-visible (`isInternalMessageChannel`). The shared
|
|
875
|
+
* agent-event bus then NULLS `sessionKey` on every event of a
|
|
876
|
+
* non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
|
|
877
|
+
* undefined`). So main-run assistant/thinking/tool events arrive with
|
|
878
|
+
* `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
|
|
879
|
+
* silently dropped ALL activity — replies still landed because they come via
|
|
880
|
+
* the `deliver()` callback, not the event bus (the reported "replies arrive,
|
|
881
|
+
* zero activity" symptom). Verified against openclaw@2026.6.10 and
|
|
882
|
+
* back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
|
|
883
|
+
* present on every emitted event.
|
|
884
|
+
*
|
|
885
|
+
* Isolation is preserved: turns to a daemon are serialised
|
|
886
|
+
* (`enqueueAgentTurn`) and the listener is torn down before the next turn
|
|
887
|
+
* dispatches, so the first relevant event locks onto THIS turn's main run.
|
|
888
|
+
* Concurrent heartbeat/cron runs and subagent / side-question runs carry a
|
|
889
|
+
* different `runId` and are excluded — exactly the leakage the gate exists to
|
|
890
|
+
* prevent. When a run IS control-UI-visible and both keys are present, a
|
|
891
|
+
* mismatched `sessionKey` is still rejected as a secondary defence.
|
|
892
|
+
*
|
|
893
|
+
* Exported for unit tests — this gate is the fix's load-bearing logic and is
|
|
894
|
+
* pinned against real 2026.6.10 event shapes.
|
|
895
|
+
*/
|
|
896
|
+
function admitAgentEvent(gate, evt) {
|
|
897
|
+
if (!(evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool")) return false;
|
|
898
|
+
if (gate.runId === null) {
|
|
899
|
+
gate.runId = evt.runId;
|
|
900
|
+
if (evt.sessionKey) gate.sessionKey ??= evt.sessionKey;
|
|
901
|
+
}
|
|
902
|
+
if (evt.runId !== gate.runId) return false;
|
|
903
|
+
if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
638
906
|
function asString(v) {
|
|
639
907
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
640
908
|
}
|
|
@@ -668,17 +936,58 @@ function buildToolActivity(data) {
|
|
|
668
936
|
const rawStatus = (phase ?? "").toLowerCase();
|
|
669
937
|
let status;
|
|
670
938
|
if (rawStatus === "error" || rawStatus === "failed" || data.error != null || data.isError === true) status = "failed";
|
|
671
|
-
else if (rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
939
|
+
else if (rawStatus === "result" || rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
672
940
|
else status = "running";
|
|
673
941
|
const summaryRaw = asString(data.summary) ?? asString(data.title) ?? asString(data.label) ?? asString(data.description);
|
|
674
942
|
const summary = summaryRaw ? summaryRaw.slice(0, 200) : void 0;
|
|
943
|
+
const truncated = {};
|
|
944
|
+
let argsText;
|
|
945
|
+
let progressText;
|
|
946
|
+
let resultText;
|
|
947
|
+
let isError;
|
|
948
|
+
if (rawStatus === "start" || rawStatus === "running" || rawStatus === "") {
|
|
949
|
+
const a = serializeArgs(data.args ?? data.arguments ?? data.input);
|
|
950
|
+
if (a) {
|
|
951
|
+
argsText = a.text;
|
|
952
|
+
if (a.truncated) truncated.args = true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const partial = data.partialResult ?? data.partial ?? data.delta;
|
|
956
|
+
if (partial !== void 0) {
|
|
957
|
+
const p = serializeProgress(partial);
|
|
958
|
+
if (p) {
|
|
959
|
+
progressText = p.text;
|
|
960
|
+
if (p.truncated) truncated.progress = true;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const resultRaw = data.result ?? data.output;
|
|
964
|
+
if (resultRaw !== void 0 || status === "done" || status === "failed") {
|
|
965
|
+
if (resultRaw !== void 0) {
|
|
966
|
+
const r = serializeResult(resultRaw);
|
|
967
|
+
if (r) {
|
|
968
|
+
resultText = r.text;
|
|
969
|
+
if (r.truncated) truncated.result = true;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (data.isError === true || status === "failed") isError = true;
|
|
973
|
+
else if (data.isError === false) isError = false;
|
|
974
|
+
}
|
|
975
|
+
const durationRaw = data.durationMs ?? data.duration;
|
|
976
|
+
const durationMs = typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : void 0;
|
|
977
|
+
const hasTruncated = truncated.args === true || truncated.progress === true || truncated.result === true;
|
|
675
978
|
return {
|
|
676
979
|
kind: "tool",
|
|
677
980
|
toolCallId,
|
|
678
981
|
name,
|
|
679
982
|
...phase ? { phase } : {},
|
|
680
983
|
status,
|
|
681
|
-
...summary ? { summary } : {}
|
|
984
|
+
...summary ? { summary } : {},
|
|
985
|
+
...argsText !== void 0 ? { argsText } : {},
|
|
986
|
+
...progressText !== void 0 ? { progressText } : {},
|
|
987
|
+
...resultText !== void 0 ? { resultText } : {},
|
|
988
|
+
...isError !== void 0 ? { isError } : {},
|
|
989
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
990
|
+
...hasTruncated ? { truncated } : {}
|
|
682
991
|
};
|
|
683
992
|
}
|
|
684
993
|
/**
|
|
@@ -858,11 +1167,25 @@ async function handleAgentRequest(request, log) {
|
|
|
858
1167
|
const sessionId = conversationId ?? legacySessionKey;
|
|
859
1168
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
860
1169
|
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
861
|
-
|
|
1170
|
+
const runGate = {
|
|
1171
|
+
runId: null,
|
|
1172
|
+
sessionKey: null
|
|
1173
|
+
};
|
|
1174
|
+
const UPDATE_THROTTLE_MS = 250;
|
|
1175
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
1176
|
+
const flushToolUpdate = (toolCallId) => {
|
|
1177
|
+
const pending = pendingUpdates.get(toolCallId);
|
|
1178
|
+
if (!pending) return;
|
|
1179
|
+
clearTimeout(pending.timer);
|
|
1180
|
+
pendingUpdates.delete(toolCallId);
|
|
1181
|
+
pending.send();
|
|
1182
|
+
};
|
|
1183
|
+
const clearAllToolUpdates = () => {
|
|
1184
|
+
for (const pending of pendingUpdates.values()) clearTimeout(pending.timer);
|
|
1185
|
+
pendingUpdates.clear();
|
|
1186
|
+
};
|
|
862
1187
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
863
|
-
if (!evt
|
|
864
|
-
if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
|
|
865
|
-
if (evt.sessionKey !== resolvedOpenClawKey) return;
|
|
1188
|
+
if (!admitAgentEvent(runGate, evt)) return;
|
|
866
1189
|
if (evt.stream === "assistant") {
|
|
867
1190
|
chatClient?.sendEvent("chat", {
|
|
868
1191
|
runId: evt.runId,
|
|
@@ -890,17 +1213,38 @@ async function handleAgentRequest(request, log) {
|
|
|
890
1213
|
}
|
|
891
1214
|
if (evt.stream === "tool") {
|
|
892
1215
|
const activity = buildToolActivity(evt.data);
|
|
893
|
-
if (activity
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1216
|
+
if (activity?.kind !== "tool") return;
|
|
1217
|
+
const toolCallId = activity.toolCallId;
|
|
1218
|
+
const send = () => {
|
|
1219
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1220
|
+
runId: evt.runId,
|
|
1221
|
+
sessionKey: legacySessionKey,
|
|
1222
|
+
seq: evt.seq,
|
|
1223
|
+
state: "activity",
|
|
1224
|
+
activity: {
|
|
1225
|
+
...activity,
|
|
1226
|
+
ts: evt.ts,
|
|
1227
|
+
seq: evt.seq
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
};
|
|
1231
|
+
const rawPhase = (activity.phase ?? "").toLowerCase();
|
|
1232
|
+
if (rawPhase === "update" || activity.status === "running" && rawPhase !== "start" && rawPhase !== "") {
|
|
1233
|
+
const existing = pendingUpdates.get(toolCallId);
|
|
1234
|
+
if (existing) existing.send = send;
|
|
1235
|
+
else {
|
|
1236
|
+
const timer = setTimeout(() => {
|
|
1237
|
+
flushToolUpdate(toolCallId);
|
|
1238
|
+
}, UPDATE_THROTTLE_MS);
|
|
1239
|
+
pendingUpdates.set(toolCallId, {
|
|
1240
|
+
timer,
|
|
1241
|
+
send
|
|
1242
|
+
});
|
|
902
1243
|
}
|
|
903
|
-
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
flushToolUpdate(toolCallId);
|
|
1247
|
+
send();
|
|
904
1248
|
return;
|
|
905
1249
|
}
|
|
906
1250
|
});
|
|
@@ -919,7 +1263,7 @@ async function handleAgentRequest(request, log) {
|
|
|
919
1263
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
920
1264
|
let a2aResponseBuffer = "";
|
|
921
1265
|
if (isA2A) resetA2AEndSignal();
|
|
922
|
-
|
|
1266
|
+
runGate.sessionKey = (await dispatchInbound({
|
|
923
1267
|
cfg,
|
|
924
1268
|
runtime: { channel: runtime.channel },
|
|
925
1269
|
channel: "alfe",
|
|
@@ -972,7 +1316,7 @@ async function handleAgentRequest(request, log) {
|
|
|
972
1316
|
chatClient?.notify("agent-message", {
|
|
973
1317
|
conversationId: conversationId ?? legacySessionKey,
|
|
974
1318
|
text: responseText,
|
|
975
|
-
sessionKey:
|
|
1319
|
+
sessionKey: runGate.sessionKey ?? legacySessionKey,
|
|
976
1320
|
messageId: chatMessageId ?? request.id,
|
|
977
1321
|
...mediaUrls.length ? { mediaUrls } : {}
|
|
978
1322
|
});
|
|
@@ -993,14 +1337,15 @@ async function handleAgentRequest(request, log) {
|
|
|
993
1337
|
resolved: a2aResolved
|
|
994
1338
|
}));
|
|
995
1339
|
}
|
|
996
|
-
chatClient?.sendResponse(request.id, true, { sessionKey:
|
|
997
|
-
log.info(`Agent dispatch complete: sessionKey=${
|
|
1340
|
+
chatClient?.sendResponse(request.id, true, { sessionKey: runGate.sessionKey });
|
|
1341
|
+
log.info(`Agent dispatch complete: sessionKey=${runGate.sessionKey}`);
|
|
998
1342
|
} catch (err) {
|
|
999
1343
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1000
1344
|
log.error(`Agent dispatch failed: ${errMsg}`);
|
|
1001
1345
|
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
1002
1346
|
} finally {
|
|
1003
1347
|
unsubscribe();
|
|
1348
|
+
clearAllToolUpdates();
|
|
1004
1349
|
if (isA2A) disarmA2AEndSignal();
|
|
1005
1350
|
}
|
|
1006
1351
|
}
|
|
@@ -1229,4 +1574,4 @@ const plugin = {
|
|
|
1229
1574
|
}
|
|
1230
1575
|
};
|
|
1231
1576
|
//#endregion
|
|
1232
|
-
export {
|
|
1577
|
+
export { stripLeakedToolSummary as a, plugin as i, buildA2ACompletePayload as n, createAlfeChannelPlugin as o, buildToolActivity as r, admitAgentEvent as t };
|