@alfe.ai/openclaw-chat 0.8.1 → 0.9.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/index.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as AlfeResolvedAccount, b as SessionSummary, g as AlfePluginConfig, h as AlfeChannelConfig, m as createAlfeChannelPlugin, u as plugin, v as ChatMessage, y as SessionData } from "./plugin.cjs";
1
+ import { S as SessionSummary, _ as AlfeChannelConfig, b as ChatMessage, f as plugin, g as createAlfeChannelPlugin, v as AlfePluginConfig, x as SessionData, y as AlfeResolvedAccount } from "./plugin.cjs";
2
2
  export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as AlfeResolvedAccount, b as SessionSummary, g as AlfePluginConfig, h as AlfeChannelConfig, m as createAlfeChannelPlugin, u as plugin, v as ChatMessage, y as SessionData } from "./plugin.js";
1
+ import { S as SessionSummary, _ as AlfeChannelConfig, b as ChatMessage, f as plugin, g as createAlfeChannelPlugin, v as AlfePluginConfig, x as SessionData, y as AlfeResolvedAccount } from "./plugin.js";
2
2
  export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { c as plugin, f as createAlfeChannelPlugin } from "./plugin2.js";
1
+ import { m as createAlfeChannelPlugin, u as plugin } from "./plugin2.js";
2
2
  export { createAlfeChannelPlugin, plugin as default };
package/dist/plugin.cjs CHANGED
@@ -3,11 +3,13 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_plugin = require("./plugin2.cjs");
6
+ exports.VOICE_REPLY_SYSTEM_PROMPT = require_plugin.VOICE_REPLY_SYSTEM_PROMPT;
6
7
  exports.__agentTurnQueueForTest = require_plugin.__agentTurnQueueForTest;
7
8
  exports.__resetAgentTurnQueueForTest = require_plugin.__resetAgentTurnQueueForTest;
8
9
  exports.__setActiveRunForTest = require_plugin.__setActiveRunForTest;
9
10
  exports.admitAgentEvent = require_plugin.admitAgentEvent;
10
11
  exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
12
+ exports.buildOriginEnvelopeContext = require_plugin.buildOriginEnvelopeContext;
11
13
  exports.buildToolActivity = require_plugin.buildToolActivity;
12
14
  exports.default = require_plugin.plugin;
13
15
  exports.joinAssistantText = require_plugin.joinAssistantText;
package/dist/plugin.d.cts CHANGED
@@ -597,6 +597,38 @@ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
597
597
  * never abort another session's run on a shared daemon.
598
598
  */
599
599
  declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
600
+ /**
601
+ * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
602
+ * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
603
+ * the reply is read aloud by TTS, so a huge markdown paragraph reads terribly.
604
+ * This biases STYLE only — explicitly NOT a hard cap: it must not suppress tool
605
+ * use or make the agent omit information the user actually needs.
606
+ */
607
+ declare const VOICE_REPLY_SYSTEM_PROMPT: string;
608
+ /**
609
+ * Per-turn envelope context derived from the RPC `origin` flag. For a live
610
+ * voice call it returns a `GroupSystemPrompt` block, which is the PROVEN
611
+ * runtime-rendered seam for per-turn system-prompt guidance: dispatchInbound
612
+ * spreads `extraContext` into `finalizeInboundContext` → `sessionCtx.GroupSystemPrompt`
613
+ * → `extraSystemPrompt` → the model's system prompt (rendered under a
614
+ * "## Group Chat Context" heading; read unconditionally, NOT gated on group
615
+ * chats). Returns `{}` for every other origin, so text/A2A turns are unchanged.
616
+ *
617
+ * Why `GroupSystemPrompt` and not a bespoke key like `A2ASystemPrompt`: the
618
+ * OpenClaw runtime does NOT read arbitrary `extraContext` keys — a custom key
619
+ * only reaches the model if a persona/AGENTS.md template interpolates it, so it
620
+ * silently rots into a dead wire otherwise (the `thinking` param is the
621
+ * cautionary precedent). `GroupSystemPrompt` is verified to reach the system
622
+ * prompt via the runtime itself.
623
+ *
624
+ * Transcript stays clean: this only feeds the model's system prompt for THIS
625
+ * turn. The persisted user message (`addMessage(sessionId, 'user', message)`)
626
+ * and everything `sessions.get` returns are the raw user text — the guidance is
627
+ * never stored or broadcast.
628
+ *
629
+ * Exported pure for unit testing (mirrors the codebase's pure-helper pattern).
630
+ */
631
+ declare function buildOriginEnvelopeContext(origin: string | undefined): Record<string, unknown>;
600
632
  declare const plugin: {
601
633
  id: string;
602
634
  name: string;
@@ -606,4 +638,4 @@ declare const plugin: {
606
638
  deactivate(api: PluginApi): Promise<void>;
607
639
  };
608
640
  //#endregion
609
- export { AlfeResolvedAccount as _, __setActiveRunForTest as a, SessionSummary as b, buildToolActivity as c, resolveAbortTargetKeys as d, rewriteAssistantText as f, AlfePluginConfig as g, AlfeChannelConfig as h, __resetAgentTurnQueueForTest as i, joinAssistantText as l, createAlfeChannelPlugin as m, RunEventGate as n, admitAgentEvent as o, stripLeakedToolSummary as p, __agentTurnQueueForTest as r, buildA2ACompletePayload as s, ActiveRunRef as t, plugin as u, ChatMessage as v, SessionData as y };
641
+ export { SessionSummary as S, AlfeChannelConfig as _, __resetAgentTurnQueueForTest as a, ChatMessage as b, buildA2ACompletePayload as c, joinAssistantText as d, plugin as f, createAlfeChannelPlugin as g, stripLeakedToolSummary as h, __agentTurnQueueForTest as i, buildOriginEnvelopeContext as l, rewriteAssistantText as m, RunEventGate as n, __setActiveRunForTest as o, resolveAbortTargetKeys as p, VOICE_REPLY_SYSTEM_PROMPT as r, admitAgentEvent as s, ActiveRunRef as t, buildToolActivity as u, AlfePluginConfig as v, SessionData as x, AlfeResolvedAccount as y };
package/dist/plugin.d.ts CHANGED
@@ -597,6 +597,38 @@ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
597
597
  * never abort another session's run on a shared daemon.
598
598
  */
599
599
  declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
600
+ /**
601
+ * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
602
+ * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
603
+ * the reply is read aloud by TTS, so a huge markdown paragraph reads terribly.
604
+ * This biases STYLE only — explicitly NOT a hard cap: it must not suppress tool
605
+ * use or make the agent omit information the user actually needs.
606
+ */
607
+ declare const VOICE_REPLY_SYSTEM_PROMPT: string;
608
+ /**
609
+ * Per-turn envelope context derived from the RPC `origin` flag. For a live
610
+ * voice call it returns a `GroupSystemPrompt` block, which is the PROVEN
611
+ * runtime-rendered seam for per-turn system-prompt guidance: dispatchInbound
612
+ * spreads `extraContext` into `finalizeInboundContext` → `sessionCtx.GroupSystemPrompt`
613
+ * → `extraSystemPrompt` → the model's system prompt (rendered under a
614
+ * "## Group Chat Context" heading; read unconditionally, NOT gated on group
615
+ * chats). Returns `{}` for every other origin, so text/A2A turns are unchanged.
616
+ *
617
+ * Why `GroupSystemPrompt` and not a bespoke key like `A2ASystemPrompt`: the
618
+ * OpenClaw runtime does NOT read arbitrary `extraContext` keys — a custom key
619
+ * only reaches the model if a persona/AGENTS.md template interpolates it, so it
620
+ * silently rots into a dead wire otherwise (the `thinking` param is the
621
+ * cautionary precedent). `GroupSystemPrompt` is verified to reach the system
622
+ * prompt via the runtime itself.
623
+ *
624
+ * Transcript stays clean: this only feeds the model's system prompt for THIS
625
+ * turn. The persisted user message (`addMessage(sessionId, 'user', message)`)
626
+ * and everything `sessions.get` returns are the raw user text — the guidance is
627
+ * never stored or broadcast.
628
+ *
629
+ * Exported pure for unit testing (mirrors the codebase's pure-helper pattern).
630
+ */
631
+ declare function buildOriginEnvelopeContext(origin: string | undefined): Record<string, unknown>;
600
632
  declare const plugin: {
601
633
  id: string;
602
634
  name: string;
@@ -606,4 +638,4 @@ declare const plugin: {
606
638
  deactivate(api: PluginApi): Promise<void>;
607
639
  };
608
640
  //#endregion
609
- export { AlfeResolvedAccount as _, __setActiveRunForTest as a, SessionSummary as b, buildToolActivity as c, resolveAbortTargetKeys as d, rewriteAssistantText as f, AlfePluginConfig as g, AlfeChannelConfig as h, __resetAgentTurnQueueForTest as i, joinAssistantText as l, createAlfeChannelPlugin as m, RunEventGate as n, admitAgentEvent as o, stripLeakedToolSummary as p, __agentTurnQueueForTest as r, buildA2ACompletePayload as s, ActiveRunRef as t, plugin as u, ChatMessage as v, SessionData as y };
641
+ export { SessionSummary as S, AlfeChannelConfig as _, __resetAgentTurnQueueForTest as a, ChatMessage as b, buildA2ACompletePayload as c, joinAssistantText as d, plugin as f, createAlfeChannelPlugin as g, stripLeakedToolSummary as h, __agentTurnQueueForTest as i, buildOriginEnvelopeContext as l, rewriteAssistantText as m, RunEventGate as n, __setActiveRunForTest as o, resolveAbortTargetKeys as p, VOICE_REPLY_SYSTEM_PROMPT as r, admitAgentEvent as s, ActiveRunRef as t, buildToolActivity as u, AlfePluginConfig as v, SessionData as x, AlfeResolvedAccount as y };
package/dist/plugin.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as buildA2ACompletePayload, c as plugin, d as stripLeakedToolSummary, i as admitAgentEvent, l as resolveAbortTargetKeys, n as __resetAgentTurnQueueForTest, o as buildToolActivity, r as __setActiveRunForTest, s as joinAssistantText, t as __agentTurnQueueForTest, u as rewriteAssistantText } from "./plugin2.js";
2
- export { __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { a as admitAgentEvent, c as buildToolActivity, d as resolveAbortTargetKeys, f as rewriteAssistantText, i as __setActiveRunForTest, l as joinAssistantText, n as __agentTurnQueueForTest, o as buildA2ACompletePayload, p as stripLeakedToolSummary, r as __resetAgentTurnQueueForTest, s as buildOriginEnvelopeContext, t as VOICE_REPLY_SYSTEM_PROMPT, u as plugin } from "./plugin2.js";
2
+ export { VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.cjs CHANGED
@@ -106,9 +106,9 @@ function extensionOf(p) {
106
106
  function isRemoteUrl(ref) {
107
107
  return /^https?:\/\//i.test(ref);
108
108
  }
109
- /** Non-http(s) URI schemes (data:, blob:, media://inbound claim-checks…) —
110
- * not local paths, nothing we can upload. Windows drive letters (C:\) are
111
- * paths, not schemes. */
109
+ /** Non-http(s) URI schemes (data:, blob:, …) — not local paths, nothing we
110
+ * can upload. Windows drive letters (C:\) are paths, not schemes.
111
+ * `media://inbound/<id>` is handled separately (resolveOpenClawMediaPath). */
112
112
  function hasNonPathScheme(ref) {
113
113
  if (/^file:\/\//i.test(ref)) return false;
114
114
  if (/^[a-zA-Z]:[\\/]/.test(ref)) return false;
@@ -120,6 +120,34 @@ function resolveLocalMediaPath(ref) {
120
120
  if (p === "~" || p.startsWith("~/")) p = (0, node_path.join)((0, node_os.homedir)(), p.slice(1));
121
121
  return (0, node_path.isAbsolute)(p) ? p : (0, node_path.resolve)(process.cwd(), p);
122
122
  }
123
+ const MEDIA_INBOUND_RE = /^media:\/\/inbound\/([^/\\]+)$/i;
124
+ /** Resolve a `media://inbound/<id>` URI to its media-store file, or null if
125
+ * the ref isn't one (or the id is unsafe). Traversal-hardened: the id must
126
+ * be a single path segment. */
127
+ function resolveOpenClawMediaPath(ref) {
128
+ const match = MEDIA_INBOUND_RE.exec(ref.trim());
129
+ if (!match) return null;
130
+ let id = match[1];
131
+ try {
132
+ id = decodeURIComponent(id);
133
+ } catch {}
134
+ if (!id || id === "." || id === ".." || /[/\\\0]/.test(id)) return null;
135
+ const stateDirOverride = process.env.OPENCLAW_STATE_DIR?.trim();
136
+ const home = process.env.OPENCLAW_HOME?.trim();
137
+ return (0, node_path.join)(stateDirOverride?.length ? stateDirOverride : (0, node_path.join)(home?.length ? home : (0, node_os.homedir)(), ".openclaw"), "media", "inbound", id);
138
+ }
139
+ /**
140
+ * Map an outbound ref to the local file we would upload: media-store path
141
+ * for `media://inbound/…`, filesystem path for path-shaped refs, null for
142
+ * remote URLs and unsupported schemes (data:, blob:, …).
143
+ */
144
+ function toLocalUploadPath(ref) {
145
+ if (isRemoteUrl(ref)) return null;
146
+ const mediaStorePath = resolveOpenClawMediaPath(ref);
147
+ if (mediaStorePath) return mediaStorePath;
148
+ if (hasNonPathScheme(ref)) return null;
149
+ return resolveLocalMediaPath(ref);
150
+ }
123
151
  async function uploadLocalFile(localPath, log, deps) {
124
152
  const client = deps.getClient ? deps.getClient() : getUploadClient(log);
125
153
  if (!client) return null;
@@ -197,11 +225,11 @@ function resolveOutboundMediaRef(ref, ctx, log, deps = {}) {
197
225
  const trimmed = ref.trim();
198
226
  if (!trimmed) return Promise.resolve(null);
199
227
  if (isRemoteUrl(trimmed)) return Promise.resolve({ url: trimmed });
200
- if (hasNonPathScheme(trimmed)) {
228
+ const localPath = toLocalUploadPath(trimmed);
229
+ if (!localPath) {
201
230
  log.warn(`Outbound media ref has unsupported scheme — skipping: ${trimmed.slice(0, 120)}`);
202
231
  return Promise.resolve(null);
203
232
  }
204
- const localPath = resolveLocalMediaPath(trimmed);
205
233
  const cached = ctx.uploads.get(localPath);
206
234
  if (cached) return cached;
207
235
  const promise = ctx.uploads.size >= 10 ? (() => {
@@ -230,8 +258,8 @@ async function resolveOutboundMedia(refs, ctx, log, deps = {}) {
230
258
  attachments.push(resolved);
231
259
  continue;
232
260
  }
233
- if (isRemoteUrl(trimmed) || hasNonPathScheme(trimmed)) continue;
234
- const localPath = resolveLocalMediaPath(trimmed);
261
+ const localPath = toLocalUploadPath(trimmed);
262
+ if (!localPath) continue;
235
263
  if (!ctx.annotatedFailures.has(localPath)) {
236
264
  ctx.annotatedFailures.add(localPath);
237
265
  newFailures.push((0, node_path.basename)(localPath));
@@ -327,11 +355,16 @@ async function rewriteLocalMarkdownImages(text, ctx, log, deps = {}) {
327
355
  const start = match.index;
328
356
  out += seg.text.slice(cursor, start);
329
357
  cursor = start + full.length;
330
- if (isRemoteUrl(src) || hasNonPathScheme(src)) {
358
+ if (isRemoteUrl(src)) {
331
359
  out += full;
332
360
  continue;
333
361
  }
334
- const ext = extensionOf(src.split("?")[0]);
362
+ const localPath = toLocalUploadPath(src);
363
+ if (!localPath) {
364
+ out += full;
365
+ continue;
366
+ }
367
+ const ext = extensionOf(localPath.split("?")[0]);
335
368
  if (!ext || !IMAGE_EXTENSIONS.has(ext)) {
336
369
  out += full;
337
370
  continue;
@@ -2255,6 +2288,48 @@ function handleChatAbort(request, log) {
2255
2288
  } else log.warn("chat.abort: OpenClaw abort primitives unavailable — cannot hard-abort the running turn");
2256
2289
  chatClient?.sendResponse(request.id, true, { aborted });
2257
2290
  }
2291
+ /**
2292
+ * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
2293
+ * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
2294
+ * the reply is read aloud by TTS, so a huge markdown paragraph reads terribly.
2295
+ * This biases STYLE only — explicitly NOT a hard cap: it must not suppress tool
2296
+ * use or make the agent omit information the user actually needs.
2297
+ */
2298
+ const VOICE_REPLY_SYSTEM_PROMPT = [
2299
+ "This message arrived over a LIVE VOICE CALL — your reply is read aloud by text-to-speech.",
2300
+ "Speak the way a helpful person would on a phone call:",
2301
+ "- Keep it brief and conversational — usually 1-3 short sentences.",
2302
+ "- Plain spoken language only: no markdown, no bullet or numbered lists, no headings, no code blocks, no URLs or link syntax.",
2303
+ "- If the full answer is long or detailed, give the short spoken version and offer to send the details to the chat.",
2304
+ "This is guidance on speaking STYLE only. Do NOT skip tool calls you would otherwise make, and never omit information the user actually needs — just deliver it the way you would say it out loud."
2305
+ ].join("\n");
2306
+ /**
2307
+ * Per-turn envelope context derived from the RPC `origin` flag. For a live
2308
+ * voice call it returns a `GroupSystemPrompt` block, which is the PROVEN
2309
+ * runtime-rendered seam for per-turn system-prompt guidance: dispatchInbound
2310
+ * spreads `extraContext` into `finalizeInboundContext` → `sessionCtx.GroupSystemPrompt`
2311
+ * → `extraSystemPrompt` → the model's system prompt (rendered under a
2312
+ * "## Group Chat Context" heading; read unconditionally, NOT gated on group
2313
+ * chats). Returns `{}` for every other origin, so text/A2A turns are unchanged.
2314
+ *
2315
+ * Why `GroupSystemPrompt` and not a bespoke key like `A2ASystemPrompt`: the
2316
+ * OpenClaw runtime does NOT read arbitrary `extraContext` keys — a custom key
2317
+ * only reaches the model if a persona/AGENTS.md template interpolates it, so it
2318
+ * silently rots into a dead wire otherwise (the `thinking` param is the
2319
+ * cautionary precedent). `GroupSystemPrompt` is verified to reach the system
2320
+ * prompt via the runtime itself.
2321
+ *
2322
+ * Transcript stays clean: this only feeds the model's system prompt for THIS
2323
+ * turn. The persisted user message (`addMessage(sessionId, 'user', message)`)
2324
+ * and everything `sessions.get` returns are the raw user text — the guidance is
2325
+ * never stored or broadcast.
2326
+ *
2327
+ * Exported pure for unit testing (mirrors the codebase's pure-helper pattern).
2328
+ */
2329
+ function buildOriginEnvelopeContext(origin) {
2330
+ if (origin === "voice") return { GroupSystemPrompt: VOICE_REPLY_SYSTEM_PROMPT };
2331
+ return {};
2332
+ }
2258
2333
  async function handleAgentRequest(request, log) {
2259
2334
  const runtime = pluginRuntime;
2260
2335
  if (!runtime) {
@@ -2265,7 +2340,7 @@ async function handleAgentRequest(request, log) {
2265
2340
  chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
2266
2341
  return;
2267
2342
  }
2268
- const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, identityId, senderPermissions, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2343
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, senderPermissions, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2269
2344
  const isA2A = !!a2a;
2270
2345
  if (!message && !rawAttachments?.length) {
2271
2346
  chatClient?.sendResponse(request.id, false, { message: "Missing message" });
@@ -2487,6 +2562,7 @@ async function handleAgentRequest(request, log) {
2487
2562
  ...userId ? { UserId: userId } : {},
2488
2563
  ...senderPermissions?.length ? { SenderPermissions: senderPermissions } : {},
2489
2564
  ChannelMode: channelMode,
2565
+ ...buildOriginEnvelopeContext(origin),
2490
2566
  ...isA2A ? {
2491
2567
  CallerType: "agent",
2492
2568
  CallerAgentId: a2a.sourceAgentId,
@@ -2836,6 +2912,12 @@ const plugin = {
2836
2912
  }
2837
2913
  };
2838
2914
  //#endregion
2915
+ Object.defineProperty(exports, "VOICE_REPLY_SYSTEM_PROMPT", {
2916
+ enumerable: true,
2917
+ get: function() {
2918
+ return VOICE_REPLY_SYSTEM_PROMPT;
2919
+ }
2920
+ });
2839
2921
  Object.defineProperty(exports, "__agentTurnQueueForTest", {
2840
2922
  enumerable: true,
2841
2923
  get: function() {
@@ -2866,6 +2948,12 @@ Object.defineProperty(exports, "buildA2ACompletePayload", {
2866
2948
  return buildA2ACompletePayload;
2867
2949
  }
2868
2950
  });
2951
+ Object.defineProperty(exports, "buildOriginEnvelopeContext", {
2952
+ enumerable: true,
2953
+ get: function() {
2954
+ return buildOriginEnvelopeContext;
2955
+ }
2956
+ });
2869
2957
  Object.defineProperty(exports, "buildToolActivity", {
2870
2958
  enumerable: true,
2871
2959
  get: function() {
@@ -1,2 +1,2 @@
1
- import { a as __setActiveRunForTest, c as buildToolActivity, d as resolveAbortTargetKeys, f as rewriteAssistantText, i as __resetAgentTurnQueueForTest, l as joinAssistantText, n as RunEventGate, o as admitAgentEvent, p as stripLeakedToolSummary, r as __agentTurnQueueForTest, s as buildA2ACompletePayload, t as ActiveRunRef, u as plugin } from "./plugin.cjs";
2
- export { ActiveRunRef, RunEventGate, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { a as __resetAgentTurnQueueForTest, c as buildA2ACompletePayload, d as joinAssistantText, f as plugin, h as stripLeakedToolSummary, i as __agentTurnQueueForTest, l as buildOriginEnvelopeContext, m as rewriteAssistantText, n as RunEventGate, o as __setActiveRunForTest, p as resolveAbortTargetKeys, r as VOICE_REPLY_SYSTEM_PROMPT, s as admitAgentEvent, t as ActiveRunRef, u as buildToolActivity } from "./plugin.cjs";
2
+ export { ActiveRunRef, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as __setActiveRunForTest, c as buildToolActivity, d as resolveAbortTargetKeys, f as rewriteAssistantText, i as __resetAgentTurnQueueForTest, l as joinAssistantText, n as RunEventGate, o as admitAgentEvent, p as stripLeakedToolSummary, r as __agentTurnQueueForTest, s as buildA2ACompletePayload, t as ActiveRunRef, u as plugin } from "./plugin.js";
2
- export { ActiveRunRef, RunEventGate, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { a as __resetAgentTurnQueueForTest, c as buildA2ACompletePayload, d as joinAssistantText, f as plugin, h as stripLeakedToolSummary, i as __agentTurnQueueForTest, l as buildOriginEnvelopeContext, m as rewriteAssistantText, n as RunEventGate, o as __setActiveRunForTest, p as resolveAbortTargetKeys, r as VOICE_REPLY_SYSTEM_PROMPT, s as admitAgentEvent, t as ActiveRunRef, u as buildToolActivity } from "./plugin.js";
2
+ export { ActiveRunRef, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.js CHANGED
@@ -106,9 +106,9 @@ function extensionOf(p) {
106
106
  function isRemoteUrl(ref) {
107
107
  return /^https?:\/\//i.test(ref);
108
108
  }
109
- /** Non-http(s) URI schemes (data:, blob:, media://inbound claim-checks…) —
110
- * not local paths, nothing we can upload. Windows drive letters (C:\) are
111
- * paths, not schemes. */
109
+ /** Non-http(s) URI schemes (data:, blob:, …) — not local paths, nothing we
110
+ * can upload. Windows drive letters (C:\) are paths, not schemes.
111
+ * `media://inbound/<id>` is handled separately (resolveOpenClawMediaPath). */
112
112
  function hasNonPathScheme(ref) {
113
113
  if (/^file:\/\//i.test(ref)) return false;
114
114
  if (/^[a-zA-Z]:[\\/]/.test(ref)) return false;
@@ -120,6 +120,34 @@ function resolveLocalMediaPath(ref) {
120
120
  if (p === "~" || p.startsWith("~/")) p = join(homedir(), p.slice(1));
121
121
  return isAbsolute(p) ? p : resolve(process.cwd(), p);
122
122
  }
123
+ const MEDIA_INBOUND_RE = /^media:\/\/inbound\/([^/\\]+)$/i;
124
+ /** Resolve a `media://inbound/<id>` URI to its media-store file, or null if
125
+ * the ref isn't one (or the id is unsafe). Traversal-hardened: the id must
126
+ * be a single path segment. */
127
+ function resolveOpenClawMediaPath(ref) {
128
+ const match = MEDIA_INBOUND_RE.exec(ref.trim());
129
+ if (!match) return null;
130
+ let id = match[1];
131
+ try {
132
+ id = decodeURIComponent(id);
133
+ } catch {}
134
+ if (!id || id === "." || id === ".." || /[/\\\0]/.test(id)) return null;
135
+ const stateDirOverride = process.env.OPENCLAW_STATE_DIR?.trim();
136
+ const home = process.env.OPENCLAW_HOME?.trim();
137
+ return join(stateDirOverride?.length ? stateDirOverride : join(home?.length ? home : homedir(), ".openclaw"), "media", "inbound", id);
138
+ }
139
+ /**
140
+ * Map an outbound ref to the local file we would upload: media-store path
141
+ * for `media://inbound/…`, filesystem path for path-shaped refs, null for
142
+ * remote URLs and unsupported schemes (data:, blob:, …).
143
+ */
144
+ function toLocalUploadPath(ref) {
145
+ if (isRemoteUrl(ref)) return null;
146
+ const mediaStorePath = resolveOpenClawMediaPath(ref);
147
+ if (mediaStorePath) return mediaStorePath;
148
+ if (hasNonPathScheme(ref)) return null;
149
+ return resolveLocalMediaPath(ref);
150
+ }
123
151
  async function uploadLocalFile(localPath, log, deps) {
124
152
  const client = deps.getClient ? deps.getClient() : getUploadClient(log);
125
153
  if (!client) return null;
@@ -197,11 +225,11 @@ function resolveOutboundMediaRef(ref, ctx, log, deps = {}) {
197
225
  const trimmed = ref.trim();
198
226
  if (!trimmed) return Promise.resolve(null);
199
227
  if (isRemoteUrl(trimmed)) return Promise.resolve({ url: trimmed });
200
- if (hasNonPathScheme(trimmed)) {
228
+ const localPath = toLocalUploadPath(trimmed);
229
+ if (!localPath) {
201
230
  log.warn(`Outbound media ref has unsupported scheme — skipping: ${trimmed.slice(0, 120)}`);
202
231
  return Promise.resolve(null);
203
232
  }
204
- const localPath = resolveLocalMediaPath(trimmed);
205
233
  const cached = ctx.uploads.get(localPath);
206
234
  if (cached) return cached;
207
235
  const promise = ctx.uploads.size >= 10 ? (() => {
@@ -230,8 +258,8 @@ async function resolveOutboundMedia(refs, ctx, log, deps = {}) {
230
258
  attachments.push(resolved);
231
259
  continue;
232
260
  }
233
- if (isRemoteUrl(trimmed) || hasNonPathScheme(trimmed)) continue;
234
- const localPath = resolveLocalMediaPath(trimmed);
261
+ const localPath = toLocalUploadPath(trimmed);
262
+ if (!localPath) continue;
235
263
  if (!ctx.annotatedFailures.has(localPath)) {
236
264
  ctx.annotatedFailures.add(localPath);
237
265
  newFailures.push(basename(localPath));
@@ -327,11 +355,16 @@ async function rewriteLocalMarkdownImages(text, ctx, log, deps = {}) {
327
355
  const start = match.index;
328
356
  out += seg.text.slice(cursor, start);
329
357
  cursor = start + full.length;
330
- if (isRemoteUrl(src) || hasNonPathScheme(src)) {
358
+ if (isRemoteUrl(src)) {
359
+ out += full;
360
+ continue;
361
+ }
362
+ const localPath = toLocalUploadPath(src);
363
+ if (!localPath) {
331
364
  out += full;
332
365
  continue;
333
366
  }
334
- const ext = extensionOf(src.split("?")[0]);
367
+ const ext = extensionOf(localPath.split("?")[0]);
335
368
  if (!ext || !IMAGE_EXTENSIONS.has(ext)) {
336
369
  out += full;
337
370
  continue;
@@ -2255,6 +2288,48 @@ function handleChatAbort(request, log) {
2255
2288
  } else log.warn("chat.abort: OpenClaw abort primitives unavailable — cannot hard-abort the running turn");
2256
2289
  chatClient?.sendResponse(request.id, true, { aborted });
2257
2290
  }
2291
+ /**
2292
+ * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
2293
+ * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
2294
+ * the reply is read aloud by TTS, so a huge markdown paragraph reads terribly.
2295
+ * This biases STYLE only — explicitly NOT a hard cap: it must not suppress tool
2296
+ * use or make the agent omit information the user actually needs.
2297
+ */
2298
+ const VOICE_REPLY_SYSTEM_PROMPT = [
2299
+ "This message arrived over a LIVE VOICE CALL — your reply is read aloud by text-to-speech.",
2300
+ "Speak the way a helpful person would on a phone call:",
2301
+ "- Keep it brief and conversational — usually 1-3 short sentences.",
2302
+ "- Plain spoken language only: no markdown, no bullet or numbered lists, no headings, no code blocks, no URLs or link syntax.",
2303
+ "- If the full answer is long or detailed, give the short spoken version and offer to send the details to the chat.",
2304
+ "This is guidance on speaking STYLE only. Do NOT skip tool calls you would otherwise make, and never omit information the user actually needs — just deliver it the way you would say it out loud."
2305
+ ].join("\n");
2306
+ /**
2307
+ * Per-turn envelope context derived from the RPC `origin` flag. For a live
2308
+ * voice call it returns a `GroupSystemPrompt` block, which is the PROVEN
2309
+ * runtime-rendered seam for per-turn system-prompt guidance: dispatchInbound
2310
+ * spreads `extraContext` into `finalizeInboundContext` → `sessionCtx.GroupSystemPrompt`
2311
+ * → `extraSystemPrompt` → the model's system prompt (rendered under a
2312
+ * "## Group Chat Context" heading; read unconditionally, NOT gated on group
2313
+ * chats). Returns `{}` for every other origin, so text/A2A turns are unchanged.
2314
+ *
2315
+ * Why `GroupSystemPrompt` and not a bespoke key like `A2ASystemPrompt`: the
2316
+ * OpenClaw runtime does NOT read arbitrary `extraContext` keys — a custom key
2317
+ * only reaches the model if a persona/AGENTS.md template interpolates it, so it
2318
+ * silently rots into a dead wire otherwise (the `thinking` param is the
2319
+ * cautionary precedent). `GroupSystemPrompt` is verified to reach the system
2320
+ * prompt via the runtime itself.
2321
+ *
2322
+ * Transcript stays clean: this only feeds the model's system prompt for THIS
2323
+ * turn. The persisted user message (`addMessage(sessionId, 'user', message)`)
2324
+ * and everything `sessions.get` returns are the raw user text — the guidance is
2325
+ * never stored or broadcast.
2326
+ *
2327
+ * Exported pure for unit testing (mirrors the codebase's pure-helper pattern).
2328
+ */
2329
+ function buildOriginEnvelopeContext(origin) {
2330
+ if (origin === "voice") return { GroupSystemPrompt: VOICE_REPLY_SYSTEM_PROMPT };
2331
+ return {};
2332
+ }
2258
2333
  async function handleAgentRequest(request, log) {
2259
2334
  const runtime = pluginRuntime;
2260
2335
  if (!runtime) {
@@ -2265,7 +2340,7 @@ async function handleAgentRequest(request, log) {
2265
2340
  chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
2266
2341
  return;
2267
2342
  }
2268
- const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, identityId, senderPermissions, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2343
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, senderPermissions, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2269
2344
  const isA2A = !!a2a;
2270
2345
  if (!message && !rawAttachments?.length) {
2271
2346
  chatClient?.sendResponse(request.id, false, { message: "Missing message" });
@@ -2487,6 +2562,7 @@ async function handleAgentRequest(request, log) {
2487
2562
  ...userId ? { UserId: userId } : {},
2488
2563
  ...senderPermissions?.length ? { SenderPermissions: senderPermissions } : {},
2489
2564
  ChannelMode: channelMode,
2565
+ ...buildOriginEnvelopeContext(origin),
2490
2566
  ...isA2A ? {
2491
2567
  CallerType: "agent",
2492
2568
  CallerAgentId: a2a.sourceAgentId,
@@ -2836,4 +2912,4 @@ const plugin = {
2836
2912
  }
2837
2913
  };
2838
2914
  //#endregion
2839
- export { buildA2ACompletePayload as a, plugin as c, stripLeakedToolSummary as d, createAlfeChannelPlugin as f, admitAgentEvent as i, resolveAbortTargetKeys as l, __resetAgentTurnQueueForTest as n, buildToolActivity as o, __setActiveRunForTest as r, joinAssistantText as s, __agentTurnQueueForTest as t, rewriteAssistantText as u };
2915
+ export { admitAgentEvent as a, buildToolActivity as c, resolveAbortTargetKeys as d, rewriteAssistantText as f, __setActiveRunForTest as i, joinAssistantText as l, createAlfeChannelPlugin as m, __agentTurnQueueForTest as n, buildA2ACompletePayload as o, stripLeakedToolSummary as p, __resetAgentTurnQueueForTest as r, buildOriginEnvelopeContext as s, VOICE_REPLY_SYSTEM_PROMPT as t, plugin as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",