clawconnect-agent 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +62 -30
  2. package/README.zh-CN.md +62 -30
  3. package/dist/commands/pair.d.ts +4 -0
  4. package/dist/commands/pair.js +63 -6
  5. package/dist/commands/pair.js.map +1 -1
  6. package/dist/commands/run.js +1 -1
  7. package/dist/commands/run.js.map +1 -1
  8. package/dist/commands/send-file.d.ts +2 -25
  9. package/dist/commands/send-file.js +22 -455
  10. package/dist/commands/send-file.js.map +1 -1
  11. package/dist/core/command-types.d.ts +18 -0
  12. package/dist/core/command-types.js +2 -0
  13. package/dist/core/command-types.js.map +1 -0
  14. package/dist/core/relay/attachment-staging.js.map +1 -0
  15. package/dist/{relay → core/relay}/chat-payload.js +17 -3
  16. package/dist/core/relay/chat-payload.js.map +1 -0
  17. package/dist/{commands/send-file-utils.js → core/relay/file-upload-utils.js} +1 -1
  18. package/dist/core/relay/file-upload-utils.js.map +1 -0
  19. package/dist/core/relay/file-upload.d.ts +40 -0
  20. package/dist/core/relay/file-upload.js +401 -0
  21. package/dist/core/relay/file-upload.js.map +1 -0
  22. package/dist/core/relay/mobile-chat-run-bridge.d.ts +201 -0
  23. package/dist/core/relay/mobile-chat-run-bridge.js +192 -0
  24. package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -0
  25. package/dist/core/relay/office-payload.js.map +1 -0
  26. package/dist/core/relay/reconnect.js.map +1 -0
  27. package/dist/core/relay/relay-server-connection.d.ts +6 -0
  28. package/dist/core/relay/relay-server-connection.js +32 -0
  29. package/dist/core/relay/relay-server-connection.js.map +1 -0
  30. package/dist/{relay/slash-command-catalog.d.ts → core/relay/slash-command-types.d.ts} +0 -1
  31. package/dist/core/relay/slash-command-types.js +2 -0
  32. package/dist/core/relay/slash-command-types.js.map +1 -0
  33. package/dist/core/relay/timeline-event-builder.d.ts +86 -0
  34. package/dist/core/relay/timeline-event-builder.js +199 -0
  35. package/dist/core/relay/timeline-event-builder.js.map +1 -0
  36. package/dist/core/relay/timeline-event-log.d.ts +81 -0
  37. package/dist/core/relay/timeline-event-log.js +254 -0
  38. package/dist/core/relay/timeline-event-log.js.map +1 -0
  39. package/dist/{relay → core/relay}/voice-input.js +5 -3
  40. package/dist/core/relay/voice-input.js.map +1 -0
  41. package/dist/hermes/hermes-relay-manager.d.ts +14 -4
  42. package/dist/hermes/hermes-relay-manager.js +159 -41
  43. package/dist/hermes/hermes-relay-manager.js.map +1 -1
  44. package/dist/hermes/hermes-runtime.d.ts +9 -8
  45. package/dist/hermes/hermes-runtime.js +8 -7
  46. package/dist/hermes/hermes-runtime.js.map +1 -1
  47. package/dist/hermes/hermes-session-store.js +52 -30
  48. package/dist/hermes/hermes-session-store.js.map +1 -1
  49. package/dist/hermes/hermes-voice-input.js +1 -1
  50. package/dist/hermes/hermes-voice-input.js.map +1 -1
  51. package/dist/hermes/runtime/hermes-runtime-artifacts.js.map +1 -0
  52. package/dist/hermes/{models → runtime}/hermes-runtime-backups.d.ts +1 -1
  53. package/dist/hermes/runtime/hermes-runtime-backups.js.map +1 -0
  54. package/dist/hermes/{models → runtime}/hermes-runtime-chat.d.ts +2 -1
  55. package/dist/hermes/{models → runtime}/hermes-runtime-chat.js +127 -27
  56. package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -0
  57. package/dist/hermes/{models → runtime}/hermes-runtime-command-router.d.ts +1 -1
  58. package/dist/hermes/{models → runtime}/hermes-runtime-command-router.js +3 -0
  59. package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -0
  60. package/dist/hermes/{models → runtime}/hermes-runtime-command-utils.d.ts +1 -1
  61. package/dist/hermes/runtime/hermes-runtime-command-utils.js.map +1 -0
  62. package/dist/hermes/{models → runtime}/hermes-runtime-cron.d.ts +1 -1
  63. package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -0
  64. package/dist/hermes/runtime/hermes-runtime-history.d.ts +2 -0
  65. package/dist/hermes/runtime/hermes-runtime-history.js +249 -0
  66. package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -0
  67. package/dist/hermes/{models → runtime}/hermes-runtime-lifecycle.d.ts +1 -1
  68. package/dist/hermes/runtime/hermes-runtime-lifecycle.js.map +1 -0
  69. package/dist/hermes/{models → runtime}/hermes-runtime-models.d.ts +1 -1
  70. package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -0
  71. package/dist/hermes/{models → runtime}/hermes-runtime-process.d.ts +2 -0
  72. package/dist/hermes/{models → runtime}/hermes-runtime-process.js +19 -2
  73. package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -0
  74. package/dist/hermes/{models → runtime}/hermes-runtime-sessions.d.ts +1 -1
  75. package/dist/hermes/{models → runtime}/hermes-runtime-sessions.js +26 -9
  76. package/dist/hermes/runtime/hermes-runtime-sessions.js.map +1 -0
  77. package/dist/hermes/{models → runtime}/hermes-runtime-skills.d.ts +1 -1
  78. package/dist/hermes/runtime/hermes-runtime-skills.js.map +1 -0
  79. package/dist/hermes/runtime/hermes-runtime-types.js.map +1 -0
  80. package/dist/hermes/runtime/hermes-runtime-usage.js.map +1 -0
  81. package/dist/hermes/runtime/hermes-runtime-values.js.map +1 -0
  82. package/dist/openclaw/backups/backup-manager.js.map +1 -0
  83. package/dist/{commands → openclaw/config}/provider-config.js +1 -1
  84. package/dist/openclaw/config/provider-config.js.map +1 -0
  85. package/dist/openclaw/config/provider-registry.js.map +1 -0
  86. package/dist/openclaw/gateway-client.js.map +1 -0
  87. package/dist/{commands → openclaw/handlers}/local-handlers.d.ts +1 -1
  88. package/dist/{commands → openclaw/handlers}/local-handlers.js +2 -2
  89. package/dist/openclaw/handlers/local-handlers.js.map +1 -0
  90. package/dist/{commands → openclaw/handlers}/provider-handlers.d.ts +1 -1
  91. package/dist/{commands → openclaw/handlers}/provider-handlers.js +3 -3
  92. package/dist/openclaw/handlers/provider-handlers.js.map +1 -0
  93. package/dist/openclaw/relay/chat-history.d.ts +51 -0
  94. package/dist/openclaw/relay/chat-history.js +347 -0
  95. package/dist/openclaw/relay/chat-history.js.map +1 -0
  96. package/dist/{relay → openclaw/relay}/chat-send-attachments.js +12 -1
  97. package/dist/openclaw/relay/chat-send-attachments.js.map +1 -0
  98. package/dist/{relay → openclaw/relay}/openclaw-voice-input.js +1 -1
  99. package/dist/openclaw/relay/openclaw-voice-input.js.map +1 -0
  100. package/dist/openclaw/relay/outgoing-media-relay.d.ts +11 -0
  101. package/dist/openclaw/relay/outgoing-media-relay.js +120 -0
  102. package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -0
  103. package/dist/{relay → openclaw/relay}/session-context.d.ts +6 -0
  104. package/dist/{relay → openclaw/relay}/session-context.js +42 -2
  105. package/dist/openclaw/relay/session-context.js.map +1 -0
  106. package/dist/openclaw/relay/slash-command-catalog.d.ts +2 -0
  107. package/dist/openclaw/relay/slash-command-catalog.generated.js.map +1 -0
  108. package/dist/openclaw/relay/slash-command-catalog.js.map +1 -0
  109. package/dist/openclaw/relay/transcript-dedupe.d.ts +21 -0
  110. package/dist/openclaw/relay/transcript-dedupe.js +211 -0
  111. package/dist/openclaw/relay/transcript-dedupe.js.map +1 -0
  112. package/dist/{relay → openclaw}/relay-manager.d.ts +1 -1
  113. package/dist/{relay → openclaw}/relay-manager.js +373 -99
  114. package/dist/openclaw/relay-manager.js.map +1 -0
  115. package/dist/{commands → openclaw/runtime}/local-runtime.d.ts +1 -17
  116. package/dist/openclaw/runtime/local-runtime.js.map +1 -0
  117. package/dist/openclaw/session-store.d.ts +1 -0
  118. package/dist/openclaw/session-store.js +70 -0
  119. package/dist/openclaw/session-store.js.map +1 -0
  120. package/dist/runtime-adapters.js +1 -1
  121. package/dist/runtime-adapters.js.map +1 -1
  122. package/package.json +2 -2
  123. package/dist/commands/backup-manager.js.map +0 -1
  124. package/dist/commands/local-handlers.js.map +0 -1
  125. package/dist/commands/local-runtime.js.map +0 -1
  126. package/dist/commands/provider-config.js.map +0 -1
  127. package/dist/commands/provider-handlers.js.map +0 -1
  128. package/dist/commands/provider-registry.js.map +0 -1
  129. package/dist/commands/send-file-utils.js.map +0 -1
  130. package/dist/hermes/models/hermes-runtime-artifacts.js.map +0 -1
  131. package/dist/hermes/models/hermes-runtime-backups.js.map +0 -1
  132. package/dist/hermes/models/hermes-runtime-chat.js.map +0 -1
  133. package/dist/hermes/models/hermes-runtime-command-router.js.map +0 -1
  134. package/dist/hermes/models/hermes-runtime-command-utils.js.map +0 -1
  135. package/dist/hermes/models/hermes-runtime-cron.js.map +0 -1
  136. package/dist/hermes/models/hermes-runtime-lifecycle.js.map +0 -1
  137. package/dist/hermes/models/hermes-runtime-models.js.map +0 -1
  138. package/dist/hermes/models/hermes-runtime-process.js.map +0 -1
  139. package/dist/hermes/models/hermes-runtime-sessions.js.map +0 -1
  140. package/dist/hermes/models/hermes-runtime-skills.js.map +0 -1
  141. package/dist/hermes/models/hermes-runtime-types.js.map +0 -1
  142. package/dist/hermes/models/hermes-runtime-usage.js.map +0 -1
  143. package/dist/hermes/models/hermes-runtime-values.js.map +0 -1
  144. package/dist/relay/attachment-staging.js.map +0 -1
  145. package/dist/relay/chat-history.d.ts +0 -29
  146. package/dist/relay/chat-history.js +0 -87
  147. package/dist/relay/chat-history.js.map +0 -1
  148. package/dist/relay/chat-payload.js.map +0 -1
  149. package/dist/relay/chat-send-attachments.js.map +0 -1
  150. package/dist/relay/gateway-client.js.map +0 -1
  151. package/dist/relay/mobile-chat-run-bridge.d.ts +0 -93
  152. package/dist/relay/mobile-chat-run-bridge.js +0 -68
  153. package/dist/relay/mobile-chat-run-bridge.js.map +0 -1
  154. package/dist/relay/office-payload.js.map +0 -1
  155. package/dist/relay/openclaw-voice-input.js.map +0 -1
  156. package/dist/relay/reconnect.js.map +0 -1
  157. package/dist/relay/relay-manager.js.map +0 -1
  158. package/dist/relay/session-context.js.map +0 -1
  159. package/dist/relay/slash-command-catalog.generated.js.map +0 -1
  160. package/dist/relay/slash-command-catalog.js.map +0 -1
  161. package/dist/relay/voice-input.js.map +0 -1
  162. /package/dist/{relay → core/relay}/attachment-staging.d.ts +0 -0
  163. /package/dist/{relay → core/relay}/attachment-staging.js +0 -0
  164. /package/dist/{relay → core/relay}/chat-payload.d.ts +0 -0
  165. /package/dist/{commands/send-file-utils.d.ts → core/relay/file-upload-utils.d.ts} +0 -0
  166. /package/dist/{relay → core/relay}/office-payload.d.ts +0 -0
  167. /package/dist/{relay → core/relay}/office-payload.js +0 -0
  168. /package/dist/{relay → core/relay}/reconnect.d.ts +0 -0
  169. /package/dist/{relay → core/relay}/reconnect.js +0 -0
  170. /package/dist/{relay → core/relay}/voice-input.d.ts +0 -0
  171. /package/dist/hermes/{models → runtime}/hermes-runtime-artifacts.d.ts +0 -0
  172. /package/dist/hermes/{models → runtime}/hermes-runtime-artifacts.js +0 -0
  173. /package/dist/hermes/{models → runtime}/hermes-runtime-backups.js +0 -0
  174. /package/dist/hermes/{models → runtime}/hermes-runtime-command-utils.js +0 -0
  175. /package/dist/hermes/{models → runtime}/hermes-runtime-cron.js +0 -0
  176. /package/dist/hermes/{models → runtime}/hermes-runtime-lifecycle.js +0 -0
  177. /package/dist/hermes/{models → runtime}/hermes-runtime-models.js +0 -0
  178. /package/dist/hermes/{models → runtime}/hermes-runtime-skills.js +0 -0
  179. /package/dist/hermes/{models → runtime}/hermes-runtime-types.d.ts +0 -0
  180. /package/dist/hermes/{models → runtime}/hermes-runtime-types.js +0 -0
  181. /package/dist/hermes/{models → runtime}/hermes-runtime-usage.d.ts +0 -0
  182. /package/dist/hermes/{models → runtime}/hermes-runtime-usage.js +0 -0
  183. /package/dist/hermes/{models → runtime}/hermes-runtime-values.d.ts +0 -0
  184. /package/dist/hermes/{models → runtime}/hermes-runtime-values.js +0 -0
  185. /package/dist/{commands → openclaw/backups}/backup-manager.d.ts +0 -0
  186. /package/dist/{commands → openclaw/backups}/backup-manager.js +0 -0
  187. /package/dist/{commands → openclaw/config}/provider-config.d.ts +0 -0
  188. /package/dist/{commands → openclaw/config}/provider-registry.d.ts +0 -0
  189. /package/dist/{commands → openclaw/config}/provider-registry.js +0 -0
  190. /package/dist/{relay → openclaw}/gateway-client.d.ts +0 -0
  191. /package/dist/{relay → openclaw}/gateway-client.js +0 -0
  192. /package/dist/{relay → openclaw/relay}/chat-send-attachments.d.ts +0 -0
  193. /package/dist/{relay → openclaw/relay}/openclaw-voice-input.d.ts +0 -0
  194. /package/dist/{relay → openclaw/relay}/slash-command-catalog.generated.d.ts +0 -0
  195. /package/dist/{relay → openclaw/relay}/slash-command-catalog.generated.js +0 -0
  196. /package/dist/{relay → openclaw/relay}/slash-command-catalog.js +0 -0
  197. /package/dist/{commands → openclaw/runtime}/local-runtime.js +0 -0
@@ -1,17 +1,26 @@
1
1
  import { WebSocket } from "ws";
2
2
  import { OpenClawGatewayClient } from "./gateway-client.js";
3
- import { handleLocalCommand } from "../commands/local-handlers.js";
4
- import { handleProviderCommand } from "../commands/provider-handlers.js";
5
- import { DEFAULT_GATEWAY_SESSION_DEFAULTS, buildContextUsageFingerprint, readContextUsageSnapshot, canonicalizeRelayParams, canonicalizeSessionKey, extractGatewaySessionDefaults, } from "./session-context.js";
6
- import { appendUniqueSuffix, extractChatRole, extractChatText, normalizeChatEventPayload, normalizeChatState, withMessageText, } from "./chat-payload.js";
7
- import { extractHistoryOutcome, withTimeout } from "./chat-history.js";
8
- import { buildMobileAssistantErrorPayload, buildMobileAssistantFinalPayload, } from "./mobile-chat-run-bridge.js";
9
- import { buildOfficeEventPayload } from "./office-payload.js";
10
- import { prepareChatSendParams } from "./chat-send-attachments.js";
11
- import { prepareOpenClawVoiceInputCommand } from "./openclaw-voice-input.js";
12
- import { voiceInputSetupMessage } from "./voice-input.js";
3
+ import { bindRelayAbortSignal, buildRelayUrl, parseRelayFrame, sendRelayJson, shouldRetryRelayClose, } from "../core/relay/relay-server-connection.js";
4
+ import { handleLocalCommand } from "./handlers/local-handlers.js";
5
+ import { handleProviderCommand } from "./handlers/provider-handlers.js";
6
+ import { DEFAULT_GATEWAY_SESSION_DEFAULTS, buildContextUsageFingerprint, readContextUsageSnapshot, canonicalizeRelayParams, canonicalizeSessionKey, extractGatewaySessionDefaults, } from "./relay/session-context.js";
7
+ import { appendUniqueSuffix, extractChatRole, extractChatText, normalizeChatEventPayload, normalizeChatState, withMessageText, } from "../core/relay/chat-payload.js";
8
+ import { extractHistoryOutcome, readOpenClawTranscriptChatHistory, withTimeout, } from "./relay/chat-history.js";
9
+ import { dedupeOpenClawChatSendUserMirrorTranscript, } from "./relay/transcript-dedupe.js";
10
+ import { buildMobileAssistantErrorPayload, buildMobileAssistantFinalPayload, } from "../core/relay/mobile-chat-run-bridge.js";
11
+ import { buildOfficeEventPayload } from "../core/relay/office-payload.js";
12
+ import { prepareChatSendParams } from "./relay/chat-send-attachments.js";
13
+ import { relayOutgoingMediaInHistoryResponse, relayOutgoingMediaInPayload } from "./relay/outgoing-media-relay.js";
14
+ import { prepareOpenClawVoiceInputCommand } from "./relay/openclaw-voice-input.js";
15
+ import { voiceInputSetupMessage } from "../core/relay/voice-input.js";
13
16
  import { gatewayCapabilitiesForType } from "../gateway-profiles.js";
14
- import { OPENCLAW_SLASH_COMMAND_CATALOG, } from "./slash-command-catalog.js";
17
+ import { OPENCLAW_SLASH_COMMAND_CATALOG } from "./relay/slash-command-catalog.js";
18
+ const CHAT_HISTORY_FETCH_TIMEOUT_MS = 3000;
19
+ const CHAT_HISTORY_FALLBACK_INITIAL_DELAY_MS = 1200;
20
+ const CHAT_HISTORY_FALLBACK_RETRY_DELAY_MS = 1800;
21
+ const CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS = 120;
22
+ const CHAT_HISTORY_FINAL_RETRY_DELAY_MS = 750;
23
+ const CHAT_SEND_MIRROR_DEDUPE_RETRY_DELAYS_MS = [250, 1000, 2500, 5000, 10000, 20000, 30000];
15
24
  // ---------------------------------------------------------------------------
16
25
  // Main entry point
17
26
  // ---------------------------------------------------------------------------
@@ -35,20 +44,18 @@ export async function runRelayManager(opts) {
35
44
  resolve(true);
36
45
  return;
37
46
  }
38
- // If a shutdown signal was provided, close the relay cleanly when fired.
39
- if (opts.signal) {
40
- if (opts.signal.aborted) {
41
- relayWs.close(1001, "shutdown");
42
- }
43
- else {
44
- opts.signal.addEventListener("abort", () => relayWs.close(1001, "shutdown"), { once: true });
45
- }
46
- }
47
+ bindRelayAbortSignal(relayWs, opts.signal);
47
48
  let gatewayClient = null;
48
49
  let sessionDefaults = { ...DEFAULT_GATEWAY_SESSION_DEFAULTS };
49
50
  const chatBuffers = new Map();
50
51
  const chatFallbacks = new Map();
51
52
  const chatRunContexts = new Map();
53
+ const chatSendDedupeRequests = new Map();
54
+ const chatSendDedupeRunKeys = new Map();
55
+ const chatSendDedupeTimers = new Map();
56
+ const chatSendDedupeAttempts = new Map();
57
+ const chatSendDedupeRunning = new Map();
58
+ const outgoingMediaUploadCache = new Map();
52
59
  const contextUsageRefreshes = new Map();
53
60
  const contextUsageFingerprints = new Map();
54
61
  const clearChatFallback = (runId) => {
@@ -58,6 +65,139 @@ export async function runRelayManager(opts) {
58
65
  chatFallbacks.delete(runId);
59
66
  }
60
67
  };
68
+ const clearChatSendDedupe = (clientRunId) => {
69
+ const timer = chatSendDedupeTimers.get(clientRunId);
70
+ if (timer) {
71
+ clearTimeout(timer);
72
+ chatSendDedupeTimers.delete(clientRunId);
73
+ }
74
+ chatSendDedupeRequests.delete(clientRunId);
75
+ chatSendDedupeAttempts.delete(clientRunId);
76
+ for (const [runId, mappedClientRunId] of chatSendDedupeRunKeys.entries()) {
77
+ if (mappedClientRunId === clientRunId) {
78
+ chatSendDedupeRunKeys.delete(runId);
79
+ }
80
+ }
81
+ };
82
+ const registerChatSendDedupe = (request, runId) => {
83
+ chatSendDedupeRequests.set(request.clientRunId, request);
84
+ if (runId) {
85
+ chatSendDedupeRunKeys.set(runId, request.clientRunId);
86
+ }
87
+ scheduleChatSendDedupe(request.clientRunId);
88
+ };
89
+ function scheduleChatSendDedupe(clientRunId, delayOverrideMs) {
90
+ if (!chatSendDedupeRequests.has(clientRunId)) {
91
+ return;
92
+ }
93
+ const existing = chatSendDedupeTimers.get(clientRunId);
94
+ if (existing) {
95
+ clearTimeout(existing);
96
+ }
97
+ const attempt = chatSendDedupeAttempts.get(clientRunId) ?? 0;
98
+ const delayMs = delayOverrideMs ?? CHAT_SEND_MIRROR_DEDUPE_RETRY_DELAYS_MS[Math.min(attempt, CHAT_SEND_MIRROR_DEDUPE_RETRY_DELAYS_MS.length - 1)];
99
+ const timer = setTimeout(() => {
100
+ chatSendDedupeTimers.delete(clientRunId);
101
+ void runChatSendDedupeAttempt(clientRunId).then((changed) => {
102
+ if (changed || !chatSendDedupeRequests.has(clientRunId)) {
103
+ return;
104
+ }
105
+ const nextAttempt = attempt + 1;
106
+ if (nextAttempt >= CHAT_SEND_MIRROR_DEDUPE_RETRY_DELAYS_MS.length) {
107
+ clearChatSendDedupe(clientRunId);
108
+ return;
109
+ }
110
+ chatSendDedupeAttempts.set(clientRunId, nextAttempt);
111
+ scheduleChatSendDedupe(clientRunId);
112
+ });
113
+ }, delayMs);
114
+ timer.unref?.();
115
+ chatSendDedupeTimers.set(clientRunId, timer);
116
+ }
117
+ function scheduleChatSendDedupeForRun(runId, delayMs = 100) {
118
+ const clientRunId = chatSendDedupeRunKeys.get(runId);
119
+ if (!clientRunId) {
120
+ return;
121
+ }
122
+ scheduleChatSendDedupe(clientRunId, delayMs);
123
+ }
124
+ async function runChatSendDedupeAttempt(clientRunId) {
125
+ const running = chatSendDedupeRunning.get(clientRunId);
126
+ if (running) {
127
+ return running;
128
+ }
129
+ const promise = (async () => {
130
+ const request = chatSendDedupeRequests.get(clientRunId);
131
+ if (!request) {
132
+ return false;
133
+ }
134
+ try {
135
+ const result = await dedupeOpenClawChatSendUserMirrorTranscript(request, sessionDefaults, { maxRetries: 2 });
136
+ if (result.changed) {
137
+ console.log(`[relay] removed ${result.removedCount} duplicate OpenClaw prompt mirror(s) from ${result.transcriptPath ?? "transcript"}`);
138
+ clearChatSendDedupe(clientRunId);
139
+ return true;
140
+ }
141
+ }
142
+ catch (error) {
143
+ console.warn(`[relay] chat.send transcript dedupe failed runId=${clientRunId}: ${String(error)}`);
144
+ }
145
+ return false;
146
+ })().finally(() => {
147
+ chatSendDedupeRunning.delete(clientRunId);
148
+ });
149
+ chatSendDedupeRunning.set(clientRunId, promise);
150
+ return promise;
151
+ }
152
+ async function dedupePendingChatSendMirrorsForSession(rawParams) {
153
+ const sessionKey = resolveRawParamsSessionKey(rawParams);
154
+ const pending = Array.from(chatSendDedupeRequests.entries()).filter(([, request]) => {
155
+ const requestSessionKey = canonicalizeSessionKey(request.sessionKey ?? sessionDefaults.mainSessionKey, sessionDefaults);
156
+ return requestSessionKey === sessionKey;
157
+ });
158
+ for (const [clientRunId] of pending) {
159
+ await runChatSendDedupeAttempt(clientRunId);
160
+ }
161
+ }
162
+ function buildChatSendDedupeRequest(paramsRecord) {
163
+ if (!paramsRecord) {
164
+ return undefined;
165
+ }
166
+ const message = typeof paramsRecord.message === "string" && paramsRecord.message.trim().length > 0
167
+ ? paramsRecord.message.trim()
168
+ : "";
169
+ const idempotencyKey = typeof paramsRecord.idempotencyKey === "string" && paramsRecord.idempotencyKey.trim().length > 0
170
+ ? paramsRecord.idempotencyKey.trim()
171
+ : "";
172
+ if (!message || !idempotencyKey) {
173
+ return undefined;
174
+ }
175
+ const clientRunId = idempotencyKey.endsWith(":user")
176
+ ? idempotencyKey.slice(0, -":user".length)
177
+ : idempotencyKey;
178
+ if (!clientRunId) {
179
+ return undefined;
180
+ }
181
+ return {
182
+ clientRunId,
183
+ message,
184
+ sessionKey: resolveRawParamsSessionKey(paramsRecord),
185
+ senderId: "openclaw-macos",
186
+ senderName: "ClawConnect Agent",
187
+ };
188
+ }
189
+ function resolveRawParamsSessionKey(rawParams) {
190
+ const record = rawParams && typeof rawParams === "object" && !Array.isArray(rawParams)
191
+ ? rawParams
192
+ : {};
193
+ const rawSessionKey = typeof record.sessionKey === "string" && record.sessionKey.trim().length > 0
194
+ ? record.sessionKey.trim()
195
+ : sessionDefaults.mainSessionKey;
196
+ const normalized = canonicalizeSessionKey(rawSessionKey, sessionDefaults);
197
+ return typeof normalized === "string" && normalized.trim().length > 0
198
+ ? normalized.trim()
199
+ : sessionDefaults.mainSessionKey;
200
+ }
61
201
  const publishContextUsageSnapshot = async (sessionKey, force = false) => {
62
202
  const normalizedSessionKey = canonicalizeSessionKey(sessionKey, sessionDefaults);
63
203
  if (typeof normalizedSessionKey !== "string" || normalizedSessionKey.trim().length === 0) {
@@ -118,11 +258,11 @@ export async function runRelayManager(opts) {
118
258
  chatFallbacks.delete(runId);
119
259
  return;
120
260
  }
121
- const fetchHistory = () => gatewayClient.request("chat.history", { sessionKey: context.sessionKey, limit: 10 });
122
- withTimeout(fetchHistory(), 800, "chat.history fallback")
261
+ const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: context.sessionKey, limit: 10 });
262
+ withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history fallback")
123
263
  .then(async (history) => {
124
264
  const outcome = extractHistoryOutcome(history, context);
125
- if (!outcome && attempt < 4) {
265
+ if (!outcome && attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
126
266
  scheduleChatHistoryFallback(runId, context, attempt + 1);
127
267
  return;
128
268
  }
@@ -134,27 +274,22 @@ export async function runRelayManager(opts) {
134
274
  clearChatFallback(runId);
135
275
  chatRunContexts.delete(runId);
136
276
  if (outcome.kind === "final") {
137
- send({
138
- type: "event",
139
- event: "chat",
140
- payload: buildMobileAssistantFinalPayload({
141
- run: { runId, sessionKey: context.sessionKey },
142
- text: outcome.text,
143
- }),
277
+ const basePayload = buildMobileAssistantFinalPayload({
278
+ run: { runId, sessionKey: context.sessionKey },
279
+ text: outcome.text,
280
+ includeTimelineEvents: true,
144
281
  });
282
+ await publishAndSendGatewayEvent("chat", buildFinalPayloadFromHistoryOutcome(basePayload, outcome), true);
145
283
  return;
146
284
  }
147
- send({
148
- type: "event",
149
- event: "chat",
150
- payload: buildMobileAssistantErrorPayload({
151
- run: { runId, sessionKey: context.sessionKey },
152
- errorMessage: outcome.errorMessage,
153
- }),
154
- });
285
+ await publishAndSendGatewayEvent("chat", buildMobileAssistantErrorPayload({
286
+ run: { runId, sessionKey: context.sessionKey },
287
+ errorMessage: outcome.errorMessage,
288
+ includeTimelineEvents: true,
289
+ }), true);
155
290
  })
156
291
  .catch((err) => {
157
- if (attempt < 4) {
292
+ if (attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
158
293
  scheduleChatHistoryFallback(runId, context, attempt + 1);
159
294
  return;
160
295
  }
@@ -162,7 +297,7 @@ export async function runRelayManager(opts) {
162
297
  chatFallbacks.delete(runId);
163
298
  chatRunContexts.delete(runId);
164
299
  });
165
- }, attempt === 0 ? 1500 : 2000);
300
+ }, attempt === 0 ? CHAT_HISTORY_FALLBACK_INITIAL_DELAY_MS : CHAT_HISTORY_FALLBACK_RETRY_DELAY_MS);
166
301
  timer.unref?.();
167
302
  chatFallbacks.set(runId, timer);
168
303
  };
@@ -183,9 +318,7 @@ export async function runRelayManager(opts) {
183
318
  }
184
319
  };
185
320
  function send(msg) {
186
- if (relayWs.readyState === WebSocket.OPEN) {
187
- relayWs.send(JSON.stringify(msg));
188
- }
321
+ sendRelayJson(relayWs, msg);
189
322
  }
190
323
  function publishOfficeSnapshot(eventName, payload) {
191
324
  const officePayload = buildOfficeEventPayload(eventName, payload, () => new Date().toISOString());
@@ -194,6 +327,149 @@ export async function runRelayManager(opts) {
194
327
  }
195
328
  send({ type: "event", event: "office", payload: officePayload });
196
329
  }
330
+ async function publishAndSendGatewayEvent(eventName, payload, publishOffice) {
331
+ const outgoingPayload = eventName === "chat"
332
+ ? await relayOutgoingMediaInPayload(payload, {
333
+ relayServerUrl: opts.relayServerUrl,
334
+ relaySecret: opts.relaySecret,
335
+ gatewayId: opts.gatewayId,
336
+ senderDisplayName: "OpenClaw",
337
+ cache: outgoingMediaUploadCache,
338
+ })
339
+ : payload;
340
+ if (publishOffice) {
341
+ publishOfficeSnapshot(eventName, outgoingPayload);
342
+ }
343
+ send({ type: "event", event: eventName, payload: outgoingPayload });
344
+ }
345
+ async function relayOutgoingMediaForResponse(method, payload) {
346
+ if (method !== "chat.history") {
347
+ return payload;
348
+ }
349
+ return relayOutgoingMediaInHistoryResponse(payload, {
350
+ relayServerUrl: opts.relayServerUrl,
351
+ relaySecret: opts.relaySecret,
352
+ gatewayId: opts.gatewayId,
353
+ senderDisplayName: "OpenClaw",
354
+ cache: outgoingMediaUploadCache,
355
+ });
356
+ }
357
+ async function requestChatHistoryFromClawConnect(params) {
358
+ try {
359
+ await dedupePendingChatSendMirrorsForSession(params);
360
+ const transcriptHistory = await readOpenClawTranscriptChatHistory(params, sessionDefaults);
361
+ if (transcriptHistory) {
362
+ return transcriptHistory;
363
+ }
364
+ }
365
+ catch (err) {
366
+ console.warn(`[relay] transcript chat.history unavailable: ${String(err)}`);
367
+ }
368
+ if (hasHistoryCursor(params)) {
369
+ return buildEmptyHistoryPage(params);
370
+ }
371
+ if (!shouldUseLegacyOpenClawHistoryFallback(params)) {
372
+ return buildEmptyHistoryPage(params);
373
+ }
374
+ if (!gatewayClient) {
375
+ throw new Error("gateway not connected");
376
+ }
377
+ return gatewayClient.request("chat.history", buildLegacyOpenClawHistoryParams(params));
378
+ }
379
+ function shouldUseLegacyOpenClawHistoryFallback(params) {
380
+ const record = params && typeof params === "object" && !Array.isArray(params)
381
+ ? params
382
+ : {};
383
+ const rawSessionKey = typeof record.sessionKey === "string" && record.sessionKey.trim().length > 0
384
+ ? record.sessionKey.trim()
385
+ : sessionDefaults.mainSessionKey;
386
+ const normalized = canonicalizeSessionKey(rawSessionKey, sessionDefaults);
387
+ return typeof normalized === "string" && normalized === sessionDefaults.mainSessionKey;
388
+ }
389
+ function buildLegacyOpenClawHistoryParams(params) {
390
+ const record = params && typeof params === "object" && !Array.isArray(params)
391
+ ? params
392
+ : {};
393
+ const rawSessionKey = typeof record.sessionKey === "string" && record.sessionKey.trim().length > 0
394
+ ? record.sessionKey.trim()
395
+ : sessionDefaults.mainSessionKey;
396
+ const sessionKey = canonicalizeSessionKey(rawSessionKey, sessionDefaults);
397
+ const legacyParams = {
398
+ sessionKey: typeof sessionKey === "string" && sessionKey.trim().length > 0
399
+ ? sessionKey.trim()
400
+ : sessionDefaults.mainSessionKey,
401
+ };
402
+ const limit = normalizePositiveInteger(record.limit);
403
+ if (limit !== undefined) {
404
+ legacyParams.limit = limit;
405
+ }
406
+ const maxChars = normalizePositiveInteger(record.maxChars);
407
+ if (maxChars !== undefined) {
408
+ legacyParams.maxChars = maxChars;
409
+ }
410
+ return legacyParams;
411
+ }
412
+ function buildEmptyHistoryPage(params) {
413
+ const record = params && typeof params === "object" && !Array.isArray(params)
414
+ ? params
415
+ : {};
416
+ const rawSessionKey = typeof record.sessionKey === "string" && record.sessionKey.trim().length > 0
417
+ ? record.sessionKey.trim()
418
+ : sessionDefaults.mainSessionKey;
419
+ const sessionKey = canonicalizeSessionKey(rawSessionKey, sessionDefaults);
420
+ return {
421
+ sessionKey: typeof sessionKey === "string" && sessionKey.trim().length > 0
422
+ ? sessionKey.trim()
423
+ : sessionDefaults.mainSessionKey,
424
+ messages: [],
425
+ hasMore: false,
426
+ };
427
+ }
428
+ function hasHistoryCursor(params) {
429
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
430
+ return false;
431
+ }
432
+ const cursor = params.cursor;
433
+ return typeof cursor === "string" && cursor.trim().length > 0;
434
+ }
435
+ function normalizePositiveInteger(value) {
436
+ const parsed = typeof value === "number" && Number.isFinite(value)
437
+ ? Math.round(value)
438
+ : typeof value === "string" && value.trim().length > 0
439
+ ? Number.parseInt(value.trim(), 10)
440
+ : Number.NaN;
441
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
442
+ }
443
+ function buildFinalPayloadFromHistoryOutcome(basePayload, outcome) {
444
+ const payload = outcome.text.trim()
445
+ ? withMessageText(basePayload, outcome.text)
446
+ : basePayload && typeof basePayload === "object" && !Array.isArray(basePayload)
447
+ ? { ...basePayload }
448
+ : {};
449
+ const historyMessage = outcome.message;
450
+ const content = Array.isArray(historyMessage.content) ? historyMessage.content : [];
451
+ if (content.length === 0) {
452
+ return payload;
453
+ }
454
+ const payloadRecord = payload && typeof payload === "object" && !Array.isArray(payload)
455
+ ? payload
456
+ : {};
457
+ const existingMessage = payloadRecord.message && typeof payloadRecord.message === "object" && !Array.isArray(payloadRecord.message)
458
+ ? payloadRecord.message
459
+ : {};
460
+ return {
461
+ ...payloadRecord,
462
+ message: {
463
+ ...existingMessage,
464
+ ...stripUndefinedHistoryMessageFields(historyMessage),
465
+ role: "assistant",
466
+ content,
467
+ },
468
+ };
469
+ }
470
+ function stripUndefinedHistoryMessageFields(message) {
471
+ return Object.fromEntries(Object.entries(message).filter(([, value]) => value !== undefined));
472
+ }
197
473
  relayWs.on("open", () => {
198
474
  console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
199
475
  opts.onConnected?.();
@@ -235,13 +511,21 @@ export async function runRelayManager(opts) {
235
511
  const resolvedSessionKey = sessionKey ?? runContext?.sessionKey;
236
512
  const currentText = extractChatText(normalizedPayload);
237
513
  const role = extractChatRole(normalizedPayload);
514
+ let realtimePayload = normalizedPayload;
238
515
  if (runId) {
516
+ if (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted") {
517
+ scheduleChatSendDedupeForRun(runId, 100);
518
+ }
239
519
  if (role === "assistant" && (state === "delta" || state === "final" || state === "error" || state === "failed" || state === "fail")) {
240
520
  clearChatFallback(runId);
241
521
  }
242
522
  if (state === "delta" || state === "streaming" || state === "in_progress") {
243
523
  const previousText = chatBuffers.get(runId) ?? "";
244
- chatBuffers.set(runId, appendUniqueSuffix(previousText, currentText));
524
+ const bufferedText = appendUniqueSuffix(previousText, currentText);
525
+ chatBuffers.set(runId, bufferedText);
526
+ if (role === "assistant" && bufferedText.trim()) {
527
+ realtimePayload = withMessageText(normalizedPayload, bufferedText);
528
+ }
245
529
  }
246
530
  else if (state === "error" || state === "failed" || state === "fail" || state === "aborted") {
247
531
  chatBuffers.delete(runId);
@@ -259,31 +543,25 @@ export async function runRelayManager(opts) {
259
543
  chatRunContexts.delete(runId);
260
544
  }
261
545
  const outgoingPayload = withMessageText(normalizedPayload, resolvedText);
262
- if (shouldPublishOffice) {
263
- publishOfficeSnapshot(event, outgoingPayload);
264
- }
265
- send({ type: "event", event, payload: outgoingPayload });
546
+ void publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice);
266
547
  return;
267
548
  }
268
- const fetchHistory = () => gatewayClient.request("chat.history", { sessionKey: resolvedSessionKey, limit: 10 });
269
- withTimeout(fetchHistory(), 500, "chat.history")
549
+ const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: resolvedSessionKey, limit: 10 });
550
+ withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history")
270
551
  .then(async (history) => {
271
552
  let outcome = runContext ? extractHistoryOutcome(history, runContext) : null;
272
- // Retry once after a short delay if OpenClaw hasn't committed the message yet.
553
+ // Retry once if OpenClaw has emitted final before the transcript is committed.
273
554
  if (!outcome) {
274
- await new Promise((resolve) => setTimeout(resolve, 150));
275
- const retryHistory = await withTimeout(fetchHistory(), 500, "chat.history retry");
555
+ await new Promise((resolve) => setTimeout(resolve, CHAT_HISTORY_FINAL_RETRY_DELAY_MS));
556
+ const retryHistory = await withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history retry");
276
557
  outcome = runContext ? extractHistoryOutcome(retryHistory, runContext) : null;
277
558
  }
278
- if (runId) {
559
+ if (runId && outcome) {
279
560
  chatRunContexts.delete(runId);
280
561
  }
281
562
  if (outcome?.kind === "final") {
282
- const outgoingPayload = withMessageText(normalizedPayload, outcome.text);
283
- if (shouldPublishOffice) {
284
- publishOfficeSnapshot(event, outgoingPayload);
285
- }
286
- send({ type: "event", event, payload: outgoingPayload });
563
+ const outgoingPayload = buildFinalPayloadFromHistoryOutcome(normalizedPayload, outcome);
564
+ await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice);
287
565
  return;
288
566
  }
289
567
  if (outcome?.kind === "error") {
@@ -292,30 +570,22 @@ export async function runRelayManager(opts) {
292
570
  state: "error",
293
571
  errorMessage: outcome.errorMessage,
294
572
  };
295
- if (shouldPublishOffice) {
296
- publishOfficeSnapshot(event, outgoingPayload);
297
- }
298
- send({
299
- type: "event",
300
- event,
301
- payload: outgoingPayload,
302
- });
573
+ await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice);
303
574
  return;
304
575
  }
305
- if (shouldPublishOffice) {
306
- publishOfficeSnapshot(event, normalizedPayload);
576
+ if (runId && runContext) {
577
+ scheduleChatHistoryFallback(runId, runContext, 0);
578
+ return;
307
579
  }
308
- send({ type: "event", event, payload: normalizedPayload });
580
+ await publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
309
581
  })
310
582
  .catch((err) => {
311
583
  console.error(`[relay] chat.history fetch failed: ${err}`);
312
- if (runId) {
313
- chatRunContexts.delete(runId);
314
- }
315
- if (shouldPublishOffice) {
316
- publishOfficeSnapshot(event, normalizedPayload);
584
+ if (runId && runContext) {
585
+ scheduleChatHistoryFallback(runId, runContext, 0);
586
+ return;
317
587
  }
318
- send({ type: "event", event, payload: normalizedPayload });
588
+ void publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
319
589
  });
320
590
  return;
321
591
  }
@@ -323,12 +593,10 @@ export async function runRelayManager(opts) {
323
593
  chatRunContexts.delete(runId);
324
594
  scheduleContextUsageRefresh(p?.sessionKey, 450);
325
595
  }
326
- if (shouldPublishOffice) {
327
- publishOfficeSnapshot(event, normalizedPayload);
328
- }
329
- send({ type: "event", event, payload: normalizedPayload });
596
+ void publishAndSendGatewayEvent(event, realtimePayload, shouldPublishOffice);
330
597
  return;
331
598
  }
599
+ void publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
332
600
  },
333
601
  });
334
602
  gatewayClient.start();
@@ -338,11 +606,8 @@ export async function runRelayManager(opts) {
338
606
  let methodForLog;
339
607
  let voiceInputRun;
340
608
  try {
341
- let msg;
342
- try {
343
- msg = JSON.parse(raw.toString());
344
- }
345
- catch {
609
+ const msg = parseRelayFrame(raw);
610
+ if (!msg) {
346
611
  return;
347
612
  }
348
613
  if (msg.type === "heartbeat") {
@@ -405,12 +670,17 @@ export async function runRelayManager(opts) {
405
670
  const paramsRecord = params && typeof params === "object" && !Array.isArray(params)
406
671
  ? params
407
672
  : undefined;
408
- if (!gatewayClient) {
673
+ const chatSendDedupeRequest = msg.method === "chat.send"
674
+ ? buildChatSendDedupeRequest(paramsRecord)
675
+ : undefined;
676
+ if (!gatewayClient && msg.method !== "chat.history") {
409
677
  throw new Error("gateway not connected");
410
678
  }
411
- gatewayClient
412
- .request(msg.method, params)
413
- .then((result) => {
679
+ const commandPromise = msg.method === "chat.history"
680
+ ? requestChatHistoryFromClawConnect(params)
681
+ : gatewayClient.request(msg.method, params);
682
+ commandPromise
683
+ .then(async (result) => {
414
684
  let resolvedRunId;
415
685
  if ((msg.method === "chat.send" || msg.method === "agent") && params && typeof params === "object" && !Array.isArray(params)) {
416
686
  const paramsRecord = params;
@@ -435,12 +705,16 @@ export async function runRelayManager(opts) {
435
705
  };
436
706
  scheduleChatHistoryFallback(runId, runContext);
437
707
  }
708
+ if (msg.method === "chat.send" && chatSendDedupeRequest) {
709
+ registerChatSendDedupe(chatSendDedupeRequest, runId);
710
+ }
438
711
  // Let a model switch settle before publishing context usage again.
439
712
  // Forced refreshes can replay a stale model snapshot and overwrite the new selection.
440
713
  scheduleContextUsageRefresh(sessionKey, 1200);
441
714
  }
442
715
  if (requestId) {
443
- send({ type: "res", id: requestId, ok: true, payload: result });
716
+ const responsePayload = await relayOutgoingMediaForResponse(msg.method, result);
717
+ send({ type: "res", id: requestId, ok: true, payload: responsePayload });
444
718
  }
445
719
  })
446
720
  .catch((err) => {
@@ -461,6 +735,7 @@ export async function runRelayManager(opts) {
461
735
  payload: buildMobileAssistantErrorPayload({
462
736
  run: voiceInputRun,
463
737
  errorMessage: setupMessage,
738
+ includeTimelineEvents: true,
464
739
  }),
465
740
  });
466
741
  if (requestId) {
@@ -481,15 +756,18 @@ export async function runRelayManager(opts) {
481
756
  clearTimeout(timer);
482
757
  }
483
758
  chatFallbacks.clear();
759
+ for (const timer of chatSendDedupeTimers.values()) {
760
+ clearTimeout(timer);
761
+ }
762
+ chatSendDedupeTimers.clear();
763
+ chatSendDedupeRequests.clear();
764
+ chatSendDedupeRunKeys.clear();
765
+ chatSendDedupeAttempts.clear();
484
766
  for (const timer of contextUsageRefreshes.values()) {
485
767
  clearTimeout(timer);
486
768
  }
487
769
  contextUsageRefreshes.clear();
488
- // Code 4000 = server kicked us because another relay client took over.
489
- // Stop retrying so the two instances don't bounce each other forever.
490
- // Also stop retrying when the shutdown signal was received.
491
- const intentional = opts.signal?.aborted || code === 4000;
492
- resolve(!intentional);
770
+ resolve(shouldRetryRelayClose(code, opts.signal));
493
771
  });
494
772
  relayWs.on("error", (err) => {
495
773
  console.error("Relay WebSocket error:", err.message);
@@ -506,8 +784,4 @@ export function buildRelayHelloMessage(opts) {
506
784
  slashCommands: OPENCLAW_SLASH_COMMAND_CATALOG,
507
785
  };
508
786
  }
509
- function buildRelayUrl(serverUrl, gatewayId, relaySecret) {
510
- const base = serverUrl.replace(/\/+$/, "").replace(/^http/, "ws");
511
- return `${base}/relay/${gatewayId}?secret=${encodeURIComponent(relaySecret)}`;
512
- }
513
787
  //# sourceMappingURL=relay-manager.js.map