@xfxstudio/claworld 2026.5.27-testing.1 → 2026.5.28-testing.2

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.
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { pathToFileURL } from 'node:url';
2
3
  import path from 'node:path';
3
4
 
4
5
  import {
@@ -99,6 +100,125 @@ function normalizePluginOptionalText(value) {
99
100
  return normalized || null;
100
101
  }
101
102
 
103
+ function normalizeAssistantOutputTexts(value) {
104
+ const rawTexts = Array.isArray(value) ? value : [value];
105
+ const texts = [];
106
+ for (const rawText of rawTexts) {
107
+ const normalized = normalizePluginOptionalText(rawText);
108
+ if (normalized && !texts.includes(normalized)) texts.push(normalized);
109
+ }
110
+ return texts;
111
+ }
112
+
113
+ function pruneRecentAssistantOutputs(now = Date.now()) {
114
+ const pruneMap = (records) => {
115
+ for (const [key, record] of records.entries()) {
116
+ if (!record || now - Number(record.recordedAt || 0) > CLAWORLD_ASSISTANT_OUTPUT_TTL_MS) {
117
+ records.delete(key);
118
+ }
119
+ }
120
+ if (records.size <= CLAWORLD_ASSISTANT_OUTPUT_MAX_RECORDS) return;
121
+ const sorted = [...records.entries()].sort((a, b) =>
122
+ Number(a[1]?.recordedAt || 0) - Number(b[1]?.recordedAt || 0),
123
+ );
124
+ for (const [key] of sorted.slice(0, Math.max(0, records.size - CLAWORLD_ASSISTANT_OUTPUT_MAX_RECORDS))) {
125
+ records.delete(key);
126
+ }
127
+ };
128
+ pruneMap(recentAssistantOutputBySessionKey);
129
+ pruneMap(recentAssistantOutputBySessionId);
130
+ }
131
+
132
+ export function recordClaworldRuntimeAssistantOutput({
133
+ sessionKey = null,
134
+ sessionId = null,
135
+ runId = null,
136
+ assistantTexts = [],
137
+ timestamp = null,
138
+ recordedAt = Date.now(),
139
+ } = {}) {
140
+ const texts = normalizeAssistantOutputTexts(assistantTexts);
141
+ if (texts.length === 0) return false;
142
+ const normalizedSessionKey = normalizePluginOptionalText(sessionKey);
143
+ const normalizedSessionId = normalizePluginOptionalText(sessionId);
144
+ if (!normalizedSessionKey && !normalizedSessionId) return false;
145
+ const record = {
146
+ sessionKey: normalizedSessionKey,
147
+ sessionId: normalizedSessionId,
148
+ runId: normalizePluginOptionalText(runId),
149
+ assistantTexts: texts,
150
+ timestamp: normalizePluginOptionalText(timestamp),
151
+ recordedAt: Number.isFinite(Number(recordedAt)) ? Number(recordedAt) : Date.now(),
152
+ };
153
+ if (normalizedSessionKey) recentAssistantOutputBySessionKey.set(normalizedSessionKey, record);
154
+ if (normalizedSessionId) recentAssistantOutputBySessionId.set(normalizedSessionId, record);
155
+ pruneRecentAssistantOutputs(record.recordedAt);
156
+ return true;
157
+ }
158
+
159
+ function readRecentAssistantOutputRecord({
160
+ sessionKeys = [],
161
+ sessionId = null,
162
+ afterMs = 0,
163
+ } = {}) {
164
+ pruneRecentAssistantOutputs();
165
+ const candidates = [];
166
+ for (const sessionKey of sessionKeys) {
167
+ const normalizedSessionKey = normalizePluginOptionalText(sessionKey);
168
+ if (!normalizedSessionKey) continue;
169
+ const record = recentAssistantOutputBySessionKey.get(normalizedSessionKey);
170
+ if (record) candidates.push(record);
171
+ }
172
+ const normalizedSessionId = normalizePluginOptionalText(sessionId);
173
+ if (normalizedSessionId) {
174
+ const record = recentAssistantOutputBySessionId.get(normalizedSessionId);
175
+ if (record) candidates.push(record);
176
+ }
177
+ return candidates
178
+ .filter((record) =>
179
+ Number(record?.recordedAt || 0) >= afterMs
180
+ && normalizeAssistantOutputTexts(record?.assistantTexts).length > 0,
181
+ )
182
+ .sort((a, b) => Number(b.recordedAt || 0) - Number(a.recordedAt || 0))[0] || null;
183
+ }
184
+
185
+ async function waitForRecentAssistantOutputRecord({
186
+ sessionKeys = [],
187
+ sessionId = null,
188
+ afterMs = 0,
189
+ timeoutMs = CLAWORLD_ASSISTANT_OUTPUT_WAIT_MS,
190
+ } = {}) {
191
+ const startedAt = Date.now();
192
+ while (Date.now() - startedAt <= timeoutMs) {
193
+ const record = readRecentAssistantOutputRecord({ sessionKeys, sessionId, afterMs });
194
+ if (record) return record;
195
+ await new Promise((resolve) => {
196
+ setTimeout(resolve, CLAWORLD_ASSISTANT_OUTPUT_POLL_MS);
197
+ });
198
+ }
199
+ return null;
200
+ }
201
+
202
+ async function loadOpenClawReplyRuntime() {
203
+ if (!openClawReplyRuntimePromise) {
204
+ openClawReplyRuntimePromise = import('openclaw/plugin-sdk/reply-runtime')
205
+ .catch(async () => {
206
+ const argvPath = normalizePluginOptionalText(process.argv?.[1]);
207
+ if (!argvPath || !argvPath.endsWith('openclaw.mjs')) return null;
208
+ const runtimePath = path.join(path.dirname(argvPath), 'dist', 'plugin-sdk', 'reply-runtime.js');
209
+ return import(pathToFileURL(runtimePath).href).catch(() => null);
210
+ });
211
+ }
212
+ return openClawReplyRuntimePromise;
213
+ }
214
+
215
+ async function resolveOpenClawReplyResolver(runtime = null) {
216
+ const directResolver = runtime?.channel?.reply?.getReplyFromConfig;
217
+ if (typeof directResolver === 'function') return directResolver;
218
+ const replyRuntime = await loadOpenClawReplyRuntime();
219
+ return typeof replyRuntime?.getReplyFromConfig === 'function' ? replyRuntime.getReplyFromConfig : null;
220
+ }
221
+
102
222
  function requireClientMessageId(value = null) {
103
223
  const normalized = normalizePluginOptionalText(value);
104
224
  if (!normalized) {
@@ -112,6 +232,14 @@ function buildGeneratedClientMessageId() {
112
232
  }
113
233
 
114
234
  const DEFAULT_RELAY_HTTP_TIMEOUT_MS = 15_000;
235
+ const CLAWORLD_ASSISTANT_OUTPUT_TTL_MS = 60_000;
236
+ const CLAWORLD_ASSISTANT_OUTPUT_WAIT_MS = 750;
237
+ const CLAWORLD_ASSISTANT_OUTPUT_POLL_MS = 25;
238
+ const CLAWORLD_ASSISTANT_OUTPUT_MAX_RECORDS = 200;
239
+
240
+ const recentAssistantOutputBySessionKey = new Map();
241
+ const recentAssistantOutputBySessionId = new Map();
242
+ let openClawReplyRuntimePromise = null;
115
243
 
116
244
  function buildRelayAgentSummary(item = {}) {
117
245
  const normalizedAgentId = normalizeClaworldText(item?.agentId, null);
@@ -326,6 +454,7 @@ const CLAWORLD_RELAY_OPERATIONAL_NOTICE_PATTERNS = [
326
454
  /^↪️\s*Model Fallback:/i,
327
455
  /^↪️\s*Model Fallback cleared:/i,
328
456
  /^⚠️\s*Agent failed before reply:/i,
457
+ /^Sent the (?:reply|opener|Claworld reply)\.?$/i,
329
458
  ];
330
459
 
331
460
  // Older/runtime-variant OpenClaw hosts may surface provider/runtime failures as
@@ -445,6 +574,87 @@ function appendRuntimeOutputPreview(previews, text) {
445
574
  previews.push(preview);
446
575
  }
447
576
 
577
+ function buildActiveDeliveryReplyKey({ accountId = null, targetAgentId = null } = {}) {
578
+ return [
579
+ normalizePluginOptionalText(accountId) || 'default',
580
+ normalizePluginOptionalText(targetAgentId) || '',
581
+ ].join('\0');
582
+ }
583
+
584
+ function resolveClaworldTargetAliases(value) {
585
+ const normalized = normalizeClaworldTarget(value) || normalizePluginOptionalText(value);
586
+ if (!normalized) return [];
587
+ const aliases = new Set([normalized]);
588
+ const atIndex = normalized.indexOf('@');
589
+ if (atIndex > 0) aliases.add(normalized.slice(0, atIndex));
590
+ return [...aliases].filter(Boolean);
591
+ }
592
+
593
+ function createActiveDeliveryReplyRegistry() {
594
+ const entriesByAccountTarget = new Map();
595
+ const entriesByAccount = new Map();
596
+
597
+ const addAccountEntry = (accountId, entry) => {
598
+ const accountKey = normalizePluginOptionalText(accountId) || 'default';
599
+ let set = entriesByAccount.get(accountKey);
600
+ if (!set) {
601
+ set = new Set();
602
+ entriesByAccount.set(accountKey, set);
603
+ }
604
+ set.add(entry);
605
+ return accountKey;
606
+ };
607
+
608
+ const removeAccountEntry = (accountKey, entry) => {
609
+ const set = entriesByAccount.get(accountKey);
610
+ if (!set) return;
611
+ set.delete(entry);
612
+ if (set.size === 0) entriesByAccount.delete(accountKey);
613
+ };
614
+
615
+ return {
616
+ register(rawEntry = {}) {
617
+ const accountId = normalizePluginOptionalText(rawEntry.accountId) || 'default';
618
+ const targetAliases = resolveClaworldTargetAliases(rawEntry.targetAgentId);
619
+ const entry = {
620
+ ...rawEntry,
621
+ accountId,
622
+ targetAgentId: targetAliases[0] || normalizePluginOptionalText(rawEntry.targetAgentId),
623
+ registeredAt: Date.now(),
624
+ };
625
+ const accountKey = addAccountEntry(accountId, entry);
626
+ const exactKeys = [];
627
+ for (const alias of targetAliases) {
628
+ const key = buildActiveDeliveryReplyKey({ accountId, targetAgentId: alias });
629
+ entriesByAccountTarget.set(key, entry);
630
+ exactKeys.push(key);
631
+ }
632
+ return () => {
633
+ for (const key of exactKeys) {
634
+ if (entriesByAccountTarget.get(key) === entry) entriesByAccountTarget.delete(key);
635
+ }
636
+ removeAccountEntry(accountKey, entry);
637
+ };
638
+ },
639
+
640
+ resolve({ accountId = null, to = null } = {}) {
641
+ const normalizedAccountId = normalizePluginOptionalText(accountId) || 'default';
642
+ const targetAliases = resolveClaworldTargetAliases(to);
643
+ for (const targetAlias of targetAliases) {
644
+ const exact = entriesByAccountTarget.get(buildActiveDeliveryReplyKey({
645
+ accountId: normalizedAccountId,
646
+ targetAgentId: targetAlias,
647
+ }));
648
+ if (exact) return exact;
649
+ }
650
+ if (targetAliases.length > 0) return null;
651
+ const accountEntries = entriesByAccount.get(normalizedAccountId);
652
+ if (!accountEntries || accountEntries.size !== 1) return null;
653
+ return [...accountEntries][0] || null;
654
+ },
655
+ };
656
+ }
657
+
448
658
  function appendPartialContinuationChunk(currentText, chunk) {
449
659
  const nextChunk = typeof chunk === 'string' ? chunk : '';
450
660
  if (!nextChunk) return currentText;
@@ -456,6 +666,14 @@ function appendPartialContinuationChunk(currentText, chunk) {
456
666
  return `${existing}${nextChunk}`;
457
667
  }
458
668
 
669
+ function normalizeReplyResolverPayloads(result) {
670
+ if (!result) return [];
671
+ if (Array.isArray(result)) return result.filter((entry) => entry && typeof entry === 'object');
672
+ if (Array.isArray(result.payloads)) return result.payloads.filter((entry) => entry && typeof entry === 'object');
673
+ if (result && typeof result === 'object') return [result];
674
+ return [];
675
+ }
676
+
459
677
  function buildRelayContinuationText({
460
678
  finalTexts = [],
461
679
  blockTexts = [],
@@ -2160,6 +2378,8 @@ function createDeliveryReplyDispatcher({
2160
2378
  relayClient,
2161
2379
  deliveryId,
2162
2380
  sessionKey,
2381
+ localSessionKey = null,
2382
+ sessionId = null,
2163
2383
  localAgentId = null,
2164
2384
  allowReply = true,
2165
2385
  logger,
@@ -2181,7 +2401,9 @@ function createDeliveryReplyDispatcher({
2181
2401
  let keptSilentFallbackUsed = false;
2182
2402
  const finalTexts = [];
2183
2403
  const blockTexts = [];
2404
+ const replyResolverTexts = [];
2184
2405
  let partialContinuationText = '';
2406
+ const dispatchStartedAt = Date.now();
2185
2407
  const runtimeOutputSummary = {
2186
2408
  counts: {
2187
2409
  final: 0,
@@ -2197,6 +2419,10 @@ function createDeliveryReplyDispatcher({
2197
2419
  nonRenderableFinal: 0,
2198
2420
  operationalNotice: 0,
2199
2421
  runtimeErrorFinal: 0,
2422
+ replyResolverPayload: 0,
2423
+ replyResolverNonRenderable: 0,
2424
+ assistantTextFallback: 0,
2425
+ messageToolReply: 0,
2200
2426
  },
2201
2427
  previews: {
2202
2428
  final: [],
@@ -2206,6 +2432,9 @@ function createDeliveryReplyDispatcher({
2206
2432
  reasoning: [],
2207
2433
  operationalNotice: [],
2208
2434
  runtimeErrorFinal: [],
2435
+ replyResolver: [],
2436
+ assistantTextFallback: [],
2437
+ messageToolReply: [],
2209
2438
  },
2210
2439
  relayContinuationSource: 'none',
2211
2440
  relayContinuationPreview: null,
@@ -2246,6 +2475,60 @@ function createDeliveryReplyDispatcher({
2246
2475
  }
2247
2476
  };
2248
2477
 
2478
+ const recordReplyResolverPayloads = (result) => {
2479
+ for (const payload of normalizeReplyResolverPayloads(result)) {
2480
+ runtimeOutputSummary.counts.replyResolverPayload += 1;
2481
+ const classified = classifyRelayContinuationPayload(payload);
2482
+ const text = String(payload?.text ?? payload?.body ?? '').trim();
2483
+ if (classified.text) {
2484
+ if (!replyResolverTexts.includes(classified.text)) {
2485
+ replyResolverTexts.push(classified.text);
2486
+ }
2487
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.replyResolver, classified.text);
2488
+ }
2489
+ if (classified.nonRenderable) {
2490
+ runtimeOutputSummary.counts.replyResolverNonRenderable += 1;
2491
+ }
2492
+ if (classified.operationalNotice) {
2493
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.operationalNotice, classified.previewText || text);
2494
+ }
2495
+ if (classified.runtimeError) {
2496
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.runtimeErrorFinal, classified.previewText || text);
2497
+ }
2498
+ }
2499
+ };
2500
+
2501
+ const resolveAssistantTextFallback = async () => {
2502
+ if (replyResolverTexts.length > 0) {
2503
+ return {
2504
+ source: 'reply_result',
2505
+ texts: [...replyResolverTexts],
2506
+ };
2507
+ }
2508
+
2509
+ const outputRecord = await waitForRecentAssistantOutputRecord({
2510
+ sessionKeys: [localSessionKey, sessionKey],
2511
+ sessionId,
2512
+ afterMs: dispatchStartedAt,
2513
+ });
2514
+ const assistantTexts = normalizeAssistantOutputTexts(outputRecord?.assistantTexts);
2515
+ if (assistantTexts.length === 0) {
2516
+ return {
2517
+ source: 'none',
2518
+ texts: [],
2519
+ };
2520
+ }
2521
+
2522
+ runtimeOutputSummary.counts.assistantTextFallback += assistantTexts.length;
2523
+ for (const text of assistantTexts) {
2524
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.assistantTextFallback, text);
2525
+ }
2526
+ return {
2527
+ source: 'assistant_text',
2528
+ texts: assistantTexts,
2529
+ };
2530
+ };
2531
+
2249
2532
  const recordRuntimeTextEvent = (kind, text) => {
2250
2533
  if (!Object.prototype.hasOwnProperty.call(runtimeOutputSummary.counts, kind)) return;
2251
2534
  runtimeOutputSummary.counts[kind] += 1;
@@ -2301,6 +2584,21 @@ function createDeliveryReplyDispatcher({
2301
2584
  return true;
2302
2585
  };
2303
2586
 
2587
+ const submitMessageToolReply = async ({ text } = {}) => {
2588
+ const normalized = sanitizeRelayContinuationText(text);
2589
+ if (!normalized) return false;
2590
+ runtimeOutputSummary.counts.messageToolReply += 1;
2591
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.messageToolReply, normalized);
2592
+ runtimeOutputSummary.relayContinuationSource = 'message_tool';
2593
+ runtimeOutputSummary.relayContinuationPreview = previewRuntimeOutputText(normalized);
2594
+ if (isExactNoReplyToken(normalized)) {
2595
+ runtimeOutputSummary.relayContinuationSource = 'message_tool_no_reply_token';
2596
+ runtimeOutputSummary.relayContinuationPreview = 'NO_REPLY';
2597
+ return await flushKeptSilent('no_reply_token');
2598
+ }
2599
+ return await flushReply(normalized);
2600
+ };
2601
+
2304
2602
  const flushKeptSilent = async (reason = null) => {
2305
2603
  if (replied || keptSilent || suppressed) return false;
2306
2604
  if (allowReply === false) {
@@ -2346,18 +2644,39 @@ function createDeliveryReplyDispatcher({
2346
2644
  const markDispatchIdle = async () => {
2347
2645
  await dispatchApi.dispatcher.waitForIdle?.();
2348
2646
  if (!replied && !suppressed) {
2647
+ let assistantTextFallback = {
2648
+ source: 'none',
2649
+ texts: [],
2650
+ };
2651
+ const shouldResolveAssistantTextFallback = (
2652
+ replyResolverTexts.length > 0
2653
+ || runtimeOutputSummary.counts.assistantMessageStart > 0
2654
+ );
2655
+ if (finalTexts.length === 0 && blockTexts.length === 0 && shouldResolveAssistantTextFallback) {
2656
+ assistantTextFallback = await resolveAssistantTextFallback();
2657
+ }
2658
+ const continuationFinalTexts = finalTexts.length > 0
2659
+ ? finalTexts
2660
+ : assistantTextFallback.texts;
2349
2661
  const allowPartialFallback = (
2350
2662
  runtimeOutputSummary.counts.final > 0
2351
- && finalTexts.length === 0
2663
+ && continuationFinalTexts.length === 0
2352
2664
  && blockTexts.length === 0
2353
2665
  && runtimeOutputSummary.counts.nonRenderableFinal === 0
2354
2666
  );
2355
2667
  const safeContinuation = buildRelayContinuationText({
2356
- finalTexts,
2668
+ finalTexts: continuationFinalTexts,
2357
2669
  blockTexts,
2358
2670
  partialText: partialContinuationText,
2359
2671
  allowPartialFallback,
2360
2672
  });
2673
+ if (
2674
+ safeContinuation.source === 'final'
2675
+ && finalTexts.length === 0
2676
+ && assistantTextFallback.source !== 'none'
2677
+ ) {
2678
+ safeContinuation.source = assistantTextFallback.source;
2679
+ }
2361
2680
  runtimeOutputSummary.relayContinuationSource = safeContinuation.source;
2362
2681
  runtimeOutputSummary.relayContinuationPreview = safeContinuation.text
2363
2682
  ? previewRuntimeOutputText(safeContinuation.text)
@@ -2414,23 +2733,28 @@ function createDeliveryReplyDispatcher({
2414
2733
  recordRuntimeLifecycle('compactionEnd');
2415
2734
  },
2416
2735
  },
2736
+ recordReplyResolverPayloads,
2417
2737
  markDispatchIdle,
2418
2738
  didReply: () => replied,
2419
2739
  didKeepSilent: () => keptSilent,
2740
+ submitMessageToolReply,
2420
2741
  getRuntimeOutputSummary: () => ({
2421
2742
  counts: { ...runtimeOutputSummary.counts },
2422
2743
  previews: {
2423
2744
  final: [...runtimeOutputSummary.previews.final],
2424
2745
  block: [...runtimeOutputSummary.previews.block],
2425
2746
  tool: [...runtimeOutputSummary.previews.tool],
2426
- partial: [...runtimeOutputSummary.previews.partial],
2427
- reasoning: [...runtimeOutputSummary.previews.reasoning],
2428
- operationalNotice: [...runtimeOutputSummary.previews.operationalNotice],
2429
- runtimeErrorFinal: [...runtimeOutputSummary.previews.runtimeErrorFinal],
2430
- },
2431
- relayContinuationSource: runtimeOutputSummary.relayContinuationSource,
2432
- relayContinuationPreview: runtimeOutputSummary.relayContinuationPreview,
2433
- replyTransport,
2747
+ partial: [...runtimeOutputSummary.previews.partial],
2748
+ reasoning: [...runtimeOutputSummary.previews.reasoning],
2749
+ operationalNotice: [...runtimeOutputSummary.previews.operationalNotice],
2750
+ runtimeErrorFinal: [...runtimeOutputSummary.previews.runtimeErrorFinal],
2751
+ replyResolver: [...runtimeOutputSummary.previews.replyResolver],
2752
+ assistantTextFallback: [...runtimeOutputSummary.previews.assistantTextFallback],
2753
+ messageToolReply: [...runtimeOutputSummary.previews.messageToolReply],
2754
+ },
2755
+ relayContinuationSource: runtimeOutputSummary.relayContinuationSource,
2756
+ relayContinuationPreview: runtimeOutputSummary.relayContinuationPreview,
2757
+ replyTransport,
2434
2758
  replyFallbackUsed,
2435
2759
  keptSilentTransport,
2436
2760
  keptSilentFallbackUsed,
@@ -2444,18 +2768,23 @@ async function runDeliveryReplyDispatch({
2444
2768
  relayClient,
2445
2769
  deliveryId,
2446
2770
  sessionKey,
2771
+ localSessionKey,
2772
+ sessionId,
2447
2773
  localAgentId,
2448
2774
  allowReply,
2449
2775
  logger,
2450
2776
  runtimeAccountId,
2451
2777
  inboundCtx,
2778
+ activeDeliveryReplies = null,
2452
2779
  } = {}) {
2453
2780
  const {
2454
2781
  dispatcher,
2455
2782
  replyOptions,
2783
+ recordReplyResolverPayloads,
2456
2784
  markDispatchIdle,
2457
2785
  didReply,
2458
2786
  didKeepSilent,
2787
+ submitMessageToolReply,
2459
2788
  getRuntimeOutputSummary,
2460
2789
  } = createDeliveryReplyDispatcher({
2461
2790
  runtime,
@@ -2463,19 +2792,59 @@ async function runDeliveryReplyDispatch({
2463
2792
  relayClient,
2464
2793
  deliveryId,
2465
2794
  sessionKey,
2795
+ localSessionKey,
2796
+ sessionId,
2466
2797
  localAgentId,
2467
2798
  allowReply,
2468
2799
  logger,
2469
2800
  runtimeAccountId,
2470
2801
  });
2471
2802
 
2472
- const dispatchResult = await runtime.channel.reply.dispatchReplyFromConfig({
2803
+ const baseReplyResolver = await resolveOpenClawReplyResolver(runtime);
2804
+ const replyResolver = baseReplyResolver
2805
+ ? async (...args) => {
2806
+ const result = await baseReplyResolver(...args);
2807
+ recordReplyResolverPayloads(result);
2808
+ return result;
2809
+ }
2810
+ : undefined;
2811
+
2812
+ const dispatchParams = {
2473
2813
  ctx: inboundCtx,
2474
2814
  cfg: currentCfg,
2475
2815
  dispatcher,
2476
2816
  replyOptions,
2477
- });
2478
- await markDispatchIdle();
2817
+ };
2818
+ if (replyResolver) {
2819
+ dispatchParams.replyResolver = replyResolver;
2820
+ }
2821
+
2822
+ const shouldRegisterMessageToolCompat = (
2823
+ inboundCtx?.sessionKind === 'conversation'
2824
+ && activeDeliveryReplies
2825
+ && typeof activeDeliveryReplies.register === 'function'
2826
+ );
2827
+ const unregisterMessageToolCompat = shouldRegisterMessageToolCompat
2828
+ ? activeDeliveryReplies.register({
2829
+ accountId: runtimeAccountId,
2830
+ localAgentId,
2831
+ targetAgentId: inboundCtx?.RelayFromAgentId || inboundCtx?.SenderId || inboundCtx?.OriginatingFrom || null,
2832
+ deliveryId,
2833
+ sessionKey,
2834
+ localSessionKey,
2835
+ sessionId,
2836
+ conversationKey: inboundCtx?.RelayConversationKey || inboundCtx?.conversationKey || null,
2837
+ submitMessageToolReply,
2838
+ })
2839
+ : null;
2840
+
2841
+ let dispatchResult;
2842
+ try {
2843
+ dispatchResult = await runtime.channel.reply.dispatchReplyFromConfig(dispatchParams);
2844
+ await markDispatchIdle();
2845
+ } finally {
2846
+ unregisterMessageToolCompat?.();
2847
+ }
2479
2848
 
2480
2849
  return {
2481
2850
  dispatchResult,
@@ -2529,6 +2898,7 @@ async function maybeBridgeRuntimeInboundEvent({
2529
2898
  runtime,
2530
2899
  cfg,
2531
2900
  inbound,
2901
+ activeDeliveryReplies = null,
2532
2902
  }) {
2533
2903
  const delivery = event?.delivery && typeof event.delivery === 'object' && !Array.isArray(event.delivery)
2534
2904
  ? event.delivery
@@ -2661,6 +3031,7 @@ async function maybeBridgeRuntimeInboundEvent({
2661
3031
  CommandAuthorized: commandAuthorized,
2662
3032
  RelayDeliveryId: isRelayDelivery ? deliveryId : null,
2663
3033
  RelayFromAgentId: fromAgentId,
3034
+ RelayConversationKey: metadata.conversationKey || null,
2664
3035
  UntrustedContext,
2665
3036
  });
2666
3037
 
@@ -2726,11 +3097,14 @@ async function maybeBridgeRuntimeInboundEvent({
2726
3097
  relayClient,
2727
3098
  deliveryId,
2728
3099
  sessionKey,
3100
+ localSessionKey,
3101
+ sessionId: sessionArtifacts.sessionId || null,
2729
3102
  localAgentId,
2730
3103
  allowReply,
2731
3104
  logger,
2732
3105
  runtimeAccountId,
2733
3106
  inboundCtx,
3107
+ activeDeliveryReplies,
2734
3108
  });
2735
3109
 
2736
3110
  const shouldRetryKickoffDispatch = (
@@ -2772,11 +3146,14 @@ async function maybeBridgeRuntimeInboundEvent({
2772
3146
  relayClient,
2773
3147
  deliveryId,
2774
3148
  sessionKey,
3149
+ localSessionKey,
3150
+ sessionId: sessionArtifacts.sessionId || null,
2775
3151
  localAgentId,
2776
3152
  allowReply,
2777
3153
  logger,
2778
3154
  runtimeAccountId,
2779
3155
  inboundCtx,
3156
+ activeDeliveryReplies,
2780
3157
  }));
2781
3158
  }
2782
3159
 
@@ -2856,6 +3233,7 @@ export function createClaworldChannelPlugin({
2856
3233
  const lifecycles = new Map();
2857
3234
  const accountRuntimeContexts = new Map();
2858
3235
  const accountBindingStates = new Map();
3236
+ const activeDeliveryReplies = createActiveDeliveryReplyRegistry();
2859
3237
 
2860
3238
  function resolveAccountBindingKey(runtimeConfig = {}, fallbackAccountId = 'default') {
2861
3239
  return String(runtimeConfig?.accountId || fallbackAccountId || 'default');
@@ -3290,6 +3668,7 @@ export function createClaworldChannelPlugin({
3290
3668
  runtime: runtimeContext.runtime,
3291
3669
  cfg: runtimeContext.cfg,
3292
3670
  inbound,
3671
+ activeDeliveryReplies,
3293
3672
  }).catch((error) => {
3294
3673
  logger.error?.(`[claworld:${runtimeAccountId}] inbound bridge exception`, {
3295
3674
  error: error?.message || String(error),
@@ -3692,8 +4071,44 @@ async function generateRuntimeProfileCard(context = {}) {
3692
4071
  deliveryMode: 'direct',
3693
4072
  createReplyEnvelope: (params = {}) => outbound.createReplyEnvelope(params),
3694
4073
  sendText: async (ctx = {}) => {
3695
- if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for claworld outbound');
3696
4074
  const resolvedContext = await resolveBoundRuntimeContext(ctx);
4075
+ const activeReply = activeDeliveryReplies.resolve({
4076
+ accountId: resolvedContext.accountId || ctx.accountId || null,
4077
+ to: ctx.to,
4078
+ });
4079
+ const activeReplyText = normalizeClaworldText(ctx.text, null);
4080
+ if (activeReply && activeReplyText) {
4081
+ const submitted = await activeReply.submitMessageToolReply?.({
4082
+ text: activeReplyText,
4083
+ to: ctx.to,
4084
+ });
4085
+ const clientMessageId = normalizePluginOptionalText(
4086
+ ctx.clientMessageId || ctx.metadata?.clientMessageId || null,
4087
+ ) || buildGeneratedClientMessageId();
4088
+ logger.info?.(`[claworld:${resolvedContext.accountId || ctx.accountId || 'default'}] routed message tool send through active delivery reply`, {
4089
+ deliveryId: activeReply.deliveryId || null,
4090
+ sessionKey: activeReply.sessionKey || null,
4091
+ localSessionKey: activeReply.localSessionKey || null,
4092
+ targetAgentId: activeReply.targetAgentId || normalizeClaworldTarget(ctx.to) || null,
4093
+ submitted: submitted === true,
4094
+ });
4095
+ return {
4096
+ channel: 'claworld',
4097
+ messageId: activeReply.deliveryId || `delivery_${Date.now()}`,
4098
+ chatId: activeReply.targetAgentId || normalizeClaworldTarget(ctx.to) || ctx.to || null,
4099
+ timestamp: Date.now(),
4100
+ meta: {
4101
+ clientMessageId,
4102
+ sessionKey: activeReply.sessionKey || null,
4103
+ turnId: activeReply.deliveryId || null,
4104
+ conversationKey: activeReply.conversationKey || null,
4105
+ targetAgentId: activeReply.targetAgentId || normalizeClaworldTarget(ctx.to) || null,
4106
+ deliveryId: activeReply.deliveryId || null,
4107
+ routedVia: 'delivery_reply',
4108
+ },
4109
+ };
4110
+ }
4111
+ if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable for claworld outbound');
3697
4112
  return deliverRelayMessage({
3698
4113
  runtimeConfig: resolvedContext.runtimeConfig,
3699
4114
  to: ctx.to,