clawconnect-agent 0.1.6 → 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.
- package/README.md +142 -34
- package/README.zh-CN.md +152 -44
- package/dist/commands/install.js +10 -4
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/pair.d.ts +6 -0
- package/dist/commands/pair.js +78 -10
- package/dist/commands/pair.js.map +1 -1
- package/dist/commands/run.js +14 -17
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/send-file.d.ts +3 -24
- package/dist/commands/send-file.js +22 -449
- package/dist/commands/send-file.js.map +1 -1
- package/dist/commands/status.d.ts +13 -1
- package/dist/commands/status.js +58 -15
- package/dist/commands/status.js.map +1 -1
- package/dist/config/config.d.ts +6 -11
- package/dist/config/config.js +19 -48
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +4 -5
- package/dist/config/env.js.map +1 -1
- package/dist/config/profile.d.ts +11 -0
- package/dist/config/profile.js +80 -0
- package/dist/config/profile.js.map +1 -0
- package/dist/core/command-types.d.ts +18 -0
- package/dist/core/command-types.js +2 -0
- package/dist/core/command-types.js.map +1 -0
- package/dist/core/relay/attachment-staging.js.map +1 -0
- package/dist/{relay → core/relay}/chat-payload.js +17 -3
- package/dist/core/relay/chat-payload.js.map +1 -0
- package/dist/{commands/send-file-utils.js → core/relay/file-upload-utils.js} +1 -1
- package/dist/core/relay/file-upload-utils.js.map +1 -0
- package/dist/core/relay/file-upload.d.ts +40 -0
- package/dist/core/relay/file-upload.js +401 -0
- package/dist/core/relay/file-upload.js.map +1 -0
- package/dist/core/relay/mobile-chat-run-bridge.d.ts +201 -0
- package/dist/core/relay/mobile-chat-run-bridge.js +192 -0
- package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -0
- package/dist/core/relay/office-payload.js.map +1 -0
- package/dist/core/relay/reconnect.js.map +1 -0
- package/dist/core/relay/relay-server-connection.d.ts +6 -0
- package/dist/core/relay/relay-server-connection.js +32 -0
- package/dist/core/relay/relay-server-connection.js.map +1 -0
- package/dist/core/relay/slash-command-types.d.ts +7 -0
- package/dist/core/relay/slash-command-types.js +2 -0
- package/dist/core/relay/slash-command-types.js.map +1 -0
- package/dist/core/relay/timeline-event-builder.d.ts +86 -0
- package/dist/core/relay/timeline-event-builder.js +199 -0
- package/dist/core/relay/timeline-event-builder.js.map +1 -0
- package/dist/core/relay/timeline-event-log.d.ts +81 -0
- package/dist/core/relay/timeline-event-log.js +254 -0
- package/dist/core/relay/timeline-event-log.js.map +1 -0
- package/dist/core/relay/voice-input.d.ts +11 -0
- package/dist/core/relay/voice-input.js +120 -0
- package/dist/core/relay/voice-input.js.map +1 -0
- package/dist/gateway-profiles.d.ts +8 -0
- package/dist/gateway-profiles.js +24 -0
- package/dist/gateway-profiles.js.map +1 -0
- package/dist/hermes/hermes-relay-manager.d.ts +59 -0
- package/dist/hermes/hermes-relay-manager.js +754 -0
- package/dist/hermes/hermes-relay-manager.js.map +1 -0
- package/dist/hermes/hermes-runtime.d.ts +9 -0
- package/dist/hermes/hermes-runtime.js +9 -0
- package/dist/hermes/hermes-runtime.js.map +1 -0
- package/dist/hermes/hermes-session-store.d.ts +15 -0
- package/dist/hermes/hermes-session-store.js +174 -0
- package/dist/hermes/hermes-session-store.js.map +1 -0
- package/dist/hermes/hermes-voice-input.d.ts +13 -0
- package/dist/hermes/hermes-voice-input.js +27 -0
- package/dist/hermes/hermes-voice-input.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-artifacts.d.ts +3 -0
- package/dist/hermes/runtime/hermes-runtime-artifacts.js +65 -0
- package/dist/hermes/runtime/hermes-runtime-artifacts.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-backups.d.ts +5 -0
- package/dist/hermes/runtime/hermes-runtime-backups.js +128 -0
- package/dist/hermes/runtime/hermes-runtime-backups.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +41 -0
- package/dist/hermes/runtime/hermes-runtime-chat.js +723 -0
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-command-router.d.ts +2 -0
- package/dist/hermes/runtime/hermes-runtime-command-router.js +96 -0
- package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-command-utils.d.ts +3 -0
- package/dist/hermes/runtime/hermes-runtime-command-utils.js +19 -0
- package/dist/hermes/runtime/hermes-runtime-command-utils.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-cron.d.ts +11 -0
- package/dist/hermes/runtime/hermes-runtime-cron.js +301 -0
- package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-history.d.ts +2 -0
- package/dist/hermes/runtime/hermes-runtime-history.js +249 -0
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-lifecycle.d.ts +9 -0
- package/dist/hermes/runtime/hermes-runtime-lifecycle.js +111 -0
- package/dist/hermes/runtime/hermes-runtime-lifecycle.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-models.d.ts +29 -0
- package/dist/hermes/runtime/hermes-runtime-models.js +125 -0
- package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-process.d.ts +24 -0
- package/dist/hermes/runtime/hermes-runtime-process.js +147 -0
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-sessions.d.ts +5 -0
- package/dist/hermes/runtime/hermes-runtime-sessions.js +88 -0
- package/dist/hermes/runtime/hermes-runtime-sessions.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-skills.d.ts +13 -0
- package/dist/hermes/runtime/hermes-runtime-skills.js +322 -0
- package/dist/hermes/runtime/hermes-runtime-skills.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-types.d.ts +19 -0
- package/dist/hermes/runtime/hermes-runtime-types.js +2 -0
- package/dist/hermes/runtime/hermes-runtime-types.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-usage.d.ts +22 -0
- package/dist/hermes/runtime/hermes-runtime-usage.js +211 -0
- package/dist/hermes/runtime/hermes-runtime-usage.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-values.d.ts +14 -0
- package/dist/hermes/runtime/hermes-runtime-values.js +105 -0
- package/dist/hermes/runtime/hermes-runtime-values.js.map +1 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.js +115 -9
- package/dist/index.js.map +1 -1
- package/dist/openclaw/backups/backup-manager.js.map +1 -0
- package/dist/{commands → openclaw/config}/provider-config.js +1 -1
- package/dist/openclaw/config/provider-config.js.map +1 -0
- package/dist/openclaw/config/provider-registry.js.map +1 -0
- package/dist/{relay → openclaw}/gateway-client.js +4 -3
- package/dist/openclaw/gateway-client.js.map +1 -0
- package/dist/{commands → openclaw/handlers}/local-handlers.d.ts +1 -1
- package/dist/{commands → openclaw/handlers}/local-handlers.js +2 -30
- package/dist/openclaw/handlers/local-handlers.js.map +1 -0
- package/dist/{commands → openclaw/handlers}/provider-handlers.d.ts +1 -1
- package/dist/{commands → openclaw/handlers}/provider-handlers.js +3 -3
- package/dist/openclaw/handlers/provider-handlers.js.map +1 -0
- package/dist/openclaw/relay/chat-history.d.ts +51 -0
- package/dist/openclaw/relay/chat-history.js +347 -0
- package/dist/openclaw/relay/chat-history.js.map +1 -0
- package/dist/{relay → openclaw/relay}/chat-send-attachments.js +12 -1
- package/dist/openclaw/relay/chat-send-attachments.js.map +1 -0
- package/dist/openclaw/relay/openclaw-voice-input.d.ts +13 -0
- package/dist/openclaw/relay/openclaw-voice-input.js +18 -0
- package/dist/openclaw/relay/openclaw-voice-input.js.map +1 -0
- package/dist/openclaw/relay/outgoing-media-relay.d.ts +11 -0
- package/dist/openclaw/relay/outgoing-media-relay.js +120 -0
- package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -0
- package/dist/{relay → openclaw/relay}/session-context.d.ts +6 -0
- package/dist/{relay → openclaw/relay}/session-context.js +53 -3
- package/dist/openclaw/relay/session-context.js.map +1 -0
- package/dist/openclaw/relay/slash-command-catalog.d.ts +2 -0
- package/dist/{relay → openclaw/relay}/slash-command-catalog.generated.d.ts +0 -5
- package/dist/{relay → openclaw/relay}/slash-command-catalog.generated.js +0 -6
- package/dist/openclaw/relay/slash-command-catalog.generated.js.map +1 -0
- package/dist/openclaw/relay/slash-command-catalog.js.map +1 -0
- package/dist/openclaw/relay/transcript-dedupe.d.ts +21 -0
- package/dist/openclaw/relay/transcript-dedupe.js +211 -0
- package/dist/openclaw/relay/transcript-dedupe.js.map +1 -0
- package/dist/{relay → openclaw}/relay-manager.d.ts +1 -4
- package/dist/openclaw/relay-manager.js +787 -0
- package/dist/openclaw/relay-manager.js.map +1 -0
- package/dist/{commands → openclaw/runtime}/local-runtime.d.ts +1 -17
- package/dist/openclaw/runtime/local-runtime.js.map +1 -0
- package/dist/openclaw/session-store.d.ts +1 -0
- package/dist/openclaw/session-store.js +70 -0
- package/dist/openclaw/session-store.js.map +1 -0
- package/dist/platform/service-manager-common.d.ts +5 -2
- package/dist/platform/service-manager-common.js +18 -7
- package/dist/platform/service-manager-common.js.map +1 -1
- package/dist/platform/service-manager.d.ts +16 -5
- package/dist/platform/service-manager.js +85 -50
- package/dist/platform/service-manager.js.map +1 -1
- package/dist/runtime-adapters.d.ts +19 -0
- package/dist/runtime-adapters.js +39 -0
- package/dist/runtime-adapters.js.map +1 -0
- package/package.json +3 -3
- package/dist/commands/backup-manager.js.map +0 -1
- package/dist/commands/local-handlers.js.map +0 -1
- package/dist/commands/local-runtime.js.map +0 -1
- package/dist/commands/provider-config.js.map +0 -1
- package/dist/commands/provider-handlers.js.map +0 -1
- package/dist/commands/provider-registry.js.map +0 -1
- package/dist/commands/send-file-utils.js.map +0 -1
- package/dist/commands/voice-reply.d.ts +0 -39
- package/dist/commands/voice-reply.js +0 -329
- package/dist/commands/voice-reply.js.map +0 -1
- package/dist/relay/attachment-staging.js.map +0 -1
- package/dist/relay/chat-history.d.ts +0 -29
- package/dist/relay/chat-history.js +0 -69
- package/dist/relay/chat-history.js.map +0 -1
- package/dist/relay/chat-payload.js.map +0 -1
- package/dist/relay/chat-send-attachments.js.map +0 -1
- package/dist/relay/gateway-client.js.map +0 -1
- package/dist/relay/office-payload.js.map +0 -1
- package/dist/relay/reconnect.js.map +0 -1
- package/dist/relay/relay-manager.js +0 -644
- package/dist/relay/relay-manager.js.map +0 -1
- package/dist/relay/session-context.js.map +0 -1
- package/dist/relay/slash-command-catalog.d.ts +0 -8
- package/dist/relay/slash-command-catalog.generated.js.map +0 -1
- package/dist/relay/slash-command-catalog.js.map +0 -1
- package/dist/relay/voice-reply-preference.d.ts +0 -25
- package/dist/relay/voice-reply-preference.js +0 -103
- package/dist/relay/voice-reply-preference.js.map +0 -1
- /package/dist/{relay → core/relay}/attachment-staging.d.ts +0 -0
- /package/dist/{relay → core/relay}/attachment-staging.js +0 -0
- /package/dist/{relay → core/relay}/chat-payload.d.ts +0 -0
- /package/dist/{commands/send-file-utils.d.ts → core/relay/file-upload-utils.d.ts} +0 -0
- /package/dist/{relay → core/relay}/office-payload.d.ts +0 -0
- /package/dist/{relay → core/relay}/office-payload.js +0 -0
- /package/dist/{relay → core/relay}/reconnect.d.ts +0 -0
- /package/dist/{relay → core/relay}/reconnect.js +0 -0
- /package/dist/{commands → openclaw/backups}/backup-manager.d.ts +0 -0
- /package/dist/{commands → openclaw/backups}/backup-manager.js +0 -0
- /package/dist/{commands → openclaw/config}/provider-config.d.ts +0 -0
- /package/dist/{commands → openclaw/config}/provider-registry.d.ts +0 -0
- /package/dist/{commands → openclaw/config}/provider-registry.js +0 -0
- /package/dist/{relay → openclaw}/gateway-client.d.ts +0 -0
- /package/dist/{relay → openclaw/relay}/chat-send-attachments.d.ts +0 -0
- /package/dist/{relay → openclaw/relay}/slash-command-catalog.js +0 -0
- /package/dist/{commands → openclaw/runtime}/local-runtime.js +0 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import { OpenClawGatewayClient } from "./gateway-client.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";
|
|
16
|
+
import { gatewayCapabilitiesForType } from "../gateway-profiles.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];
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Main entry point
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Connects to the cloud relay server and the local OpenClaw Gateway,
|
|
29
|
+
* then bridges messages between them indefinitely.
|
|
30
|
+
*
|
|
31
|
+
* The gateway client runs for as long as this relay connection is alive.
|
|
32
|
+
* Returns a Promise that resolves `true` (retry) when the relay server
|
|
33
|
+
* connection closes.
|
|
34
|
+
*/
|
|
35
|
+
export async function runRelayManager(opts) {
|
|
36
|
+
const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
let relayWs;
|
|
39
|
+
try {
|
|
40
|
+
relayWs = new WebSocket(wsUrl);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error("Failed to create relay WebSocket:", err);
|
|
44
|
+
resolve(true);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
bindRelayAbortSignal(relayWs, opts.signal);
|
|
48
|
+
let gatewayClient = null;
|
|
49
|
+
let sessionDefaults = { ...DEFAULT_GATEWAY_SESSION_DEFAULTS };
|
|
50
|
+
const chatBuffers = new Map();
|
|
51
|
+
const chatFallbacks = new Map();
|
|
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();
|
|
59
|
+
const contextUsageRefreshes = new Map();
|
|
60
|
+
const contextUsageFingerprints = new Map();
|
|
61
|
+
const clearChatFallback = (runId) => {
|
|
62
|
+
const timer = chatFallbacks.get(runId);
|
|
63
|
+
if (timer) {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
chatFallbacks.delete(runId);
|
|
66
|
+
}
|
|
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
|
+
}
|
|
201
|
+
const publishContextUsageSnapshot = async (sessionKey, force = false) => {
|
|
202
|
+
const normalizedSessionKey = canonicalizeSessionKey(sessionKey, sessionDefaults);
|
|
203
|
+
if (typeof normalizedSessionKey !== "string" || normalizedSessionKey.trim().length === 0) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const snapshot = await readContextUsageSnapshot(normalizedSessionKey.trim(), sessionDefaults);
|
|
207
|
+
if (!snapshot) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const fingerprint = buildContextUsageFingerprint(snapshot);
|
|
211
|
+
if (!force && contextUsageFingerprints.get(snapshot.sessionKey) === fingerprint) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
contextUsageFingerprints.set(snapshot.sessionKey, fingerprint);
|
|
215
|
+
send({
|
|
216
|
+
type: "event",
|
|
217
|
+
event: "context_usage",
|
|
218
|
+
payload: {
|
|
219
|
+
sessionKey: snapshot.sessionKey,
|
|
220
|
+
currentModel: snapshot.currentModel,
|
|
221
|
+
contextUsage: snapshot.promptTokens ?? snapshot.contextUsage,
|
|
222
|
+
contextLimit: snapshot.contextLimit,
|
|
223
|
+
promptTokens: snapshot.promptTokens,
|
|
224
|
+
maxInputTokens: snapshot.contextLimit,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
const scheduleContextUsageRefresh = (sessionKey, delayMs = 250, force = false) => {
|
|
229
|
+
if (!sessionKey) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const normalizedSessionKey = canonicalizeSessionKey(sessionKey, sessionDefaults);
|
|
233
|
+
if (typeof normalizedSessionKey !== "string" || normalizedSessionKey.trim().length === 0) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const key = normalizedSessionKey.trim();
|
|
237
|
+
const existing = contextUsageRefreshes.get(key);
|
|
238
|
+
if (existing) {
|
|
239
|
+
clearTimeout(existing);
|
|
240
|
+
}
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
contextUsageRefreshes.delete(key);
|
|
243
|
+
void publishContextUsageSnapshot(key, force).catch((error) => {
|
|
244
|
+
console.warn(`[relay] failed to publish context usage for session ${key}: ${String(error)}`);
|
|
245
|
+
});
|
|
246
|
+
}, delayMs);
|
|
247
|
+
timer.unref?.();
|
|
248
|
+
contextUsageRefreshes.set(key, timer);
|
|
249
|
+
};
|
|
250
|
+
const scheduleChatHistoryFallback = (runId, context, attempt = 0) => {
|
|
251
|
+
if (!runId || !context.sessionKey) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
clearChatFallback(runId);
|
|
255
|
+
chatRunContexts.set(runId, context);
|
|
256
|
+
const timer = setTimeout(() => {
|
|
257
|
+
if (!gatewayClient) {
|
|
258
|
+
chatFallbacks.delete(runId);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: context.sessionKey, limit: 10 });
|
|
262
|
+
withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history fallback")
|
|
263
|
+
.then(async (history) => {
|
|
264
|
+
const outcome = extractHistoryOutcome(history, context);
|
|
265
|
+
if (!outcome && attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
|
|
266
|
+
scheduleChatHistoryFallback(runId, context, attempt + 1);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (!outcome) {
|
|
270
|
+
chatFallbacks.delete(runId);
|
|
271
|
+
chatRunContexts.delete(runId);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
clearChatFallback(runId);
|
|
275
|
+
chatRunContexts.delete(runId);
|
|
276
|
+
if (outcome.kind === "final") {
|
|
277
|
+
const basePayload = buildMobileAssistantFinalPayload({
|
|
278
|
+
run: { runId, sessionKey: context.sessionKey },
|
|
279
|
+
text: outcome.text,
|
|
280
|
+
includeTimelineEvents: true,
|
|
281
|
+
});
|
|
282
|
+
await publishAndSendGatewayEvent("chat", buildFinalPayloadFromHistoryOutcome(basePayload, outcome), true);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await publishAndSendGatewayEvent("chat", buildMobileAssistantErrorPayload({
|
|
286
|
+
run: { runId, sessionKey: context.sessionKey },
|
|
287
|
+
errorMessage: outcome.errorMessage,
|
|
288
|
+
includeTimelineEvents: true,
|
|
289
|
+
}), true);
|
|
290
|
+
})
|
|
291
|
+
.catch((err) => {
|
|
292
|
+
if (attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
|
|
293
|
+
scheduleChatHistoryFallback(runId, context, attempt + 1);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
console.warn(`[relay] chat history fallback failed runId=${runId}: ${String(err)}`);
|
|
297
|
+
chatFallbacks.delete(runId);
|
|
298
|
+
chatRunContexts.delete(runId);
|
|
299
|
+
});
|
|
300
|
+
}, attempt === 0 ? CHAT_HISTORY_FALLBACK_INITIAL_DELAY_MS : CHAT_HISTORY_FALLBACK_RETRY_DELAY_MS);
|
|
301
|
+
timer.unref?.();
|
|
302
|
+
chatFallbacks.set(runId, timer);
|
|
303
|
+
};
|
|
304
|
+
const refreshSessionDefaults = async () => {
|
|
305
|
+
if (!gatewayClient) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const payload = await gatewayClient.request("config.get", {});
|
|
310
|
+
const nextDefaults = extractGatewaySessionDefaults(payload);
|
|
311
|
+
if (nextDefaults) {
|
|
312
|
+
sessionDefaults = nextDefaults;
|
|
313
|
+
}
|
|
314
|
+
scheduleContextUsageRefresh(sessionDefaults.mainSessionKey, 50, true);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
console.warn(`[relay] failed to load session defaults: ${String(err)}`);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
function send(msg) {
|
|
321
|
+
sendRelayJson(relayWs, msg);
|
|
322
|
+
}
|
|
323
|
+
function publishOfficeSnapshot(eventName, payload) {
|
|
324
|
+
const officePayload = buildOfficeEventPayload(eventName, payload, () => new Date().toISOString());
|
|
325
|
+
if (!officePayload) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
send({ type: "event", event: "office", payload: officePayload });
|
|
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
|
+
}
|
|
473
|
+
relayWs.on("open", () => {
|
|
474
|
+
console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
|
|
475
|
+
opts.onConnected?.();
|
|
476
|
+
send(buildRelayHelloMessage({
|
|
477
|
+
platform: process.platform,
|
|
478
|
+
agentVersion: "1.0.0",
|
|
479
|
+
capabilities: gatewayCapabilitiesForType("openclaw"),
|
|
480
|
+
}));
|
|
481
|
+
// Start the persistent gateway connection as soon as we're connected
|
|
482
|
+
// to the relay server. Its lifetime is tied to this relay session.
|
|
483
|
+
gatewayClient = new OpenClawGatewayClient({
|
|
484
|
+
url: opts.gatewayUrl,
|
|
485
|
+
token: opts.gatewayToken,
|
|
486
|
+
password: opts.gatewayPassword,
|
|
487
|
+
onConnected: () => {
|
|
488
|
+
console.log("Gateway connected.");
|
|
489
|
+
send({ type: "gateway_connected" });
|
|
490
|
+
publishOfficeSnapshot("gateway_connected", {
|
|
491
|
+
sessionKey: sessionDefaults.mainSessionKey,
|
|
492
|
+
});
|
|
493
|
+
void refreshSessionDefaults();
|
|
494
|
+
},
|
|
495
|
+
onDisconnected: (reason) => {
|
|
496
|
+
console.log(`Gateway disconnected: ${reason}`);
|
|
497
|
+
send({ type: "gateway_disconnected", reason });
|
|
498
|
+
publishOfficeSnapshot("gateway_disconnected", {
|
|
499
|
+
reason,
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
onEvent: (event, payload) => {
|
|
503
|
+
const normalizedPayload = event === "chat" ? normalizeChatEventPayload(payload) : payload;
|
|
504
|
+
const shouldPublishOffice = event === "chat" || event === "agent" || event === "context_usage";
|
|
505
|
+
if (event === "chat") {
|
|
506
|
+
const p = normalizedPayload;
|
|
507
|
+
const state = normalizeChatState(normalizedPayload);
|
|
508
|
+
const runId = typeof p?.runId === "string" ? p.runId : "";
|
|
509
|
+
const sessionKey = typeof p?.sessionKey === "string" && p.sessionKey.trim().length > 0 ? p.sessionKey.trim() : undefined;
|
|
510
|
+
const runContext = runId ? chatRunContexts.get(runId) : undefined;
|
|
511
|
+
const resolvedSessionKey = sessionKey ?? runContext?.sessionKey;
|
|
512
|
+
const currentText = extractChatText(normalizedPayload);
|
|
513
|
+
const role = extractChatRole(normalizedPayload);
|
|
514
|
+
let realtimePayload = normalizedPayload;
|
|
515
|
+
if (runId) {
|
|
516
|
+
if (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted") {
|
|
517
|
+
scheduleChatSendDedupeForRun(runId, 100);
|
|
518
|
+
}
|
|
519
|
+
if (role === "assistant" && (state === "delta" || state === "final" || state === "error" || state === "failed" || state === "fail")) {
|
|
520
|
+
clearChatFallback(runId);
|
|
521
|
+
}
|
|
522
|
+
if (state === "delta" || state === "streaming" || state === "in_progress") {
|
|
523
|
+
const previousText = chatBuffers.get(runId) ?? "";
|
|
524
|
+
const bufferedText = appendUniqueSuffix(previousText, currentText);
|
|
525
|
+
chatBuffers.set(runId, bufferedText);
|
|
526
|
+
if (role === "assistant" && bufferedText.trim()) {
|
|
527
|
+
realtimePayload = withMessageText(normalizedPayload, bufferedText);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (state === "error" || state === "failed" || state === "fail" || state === "aborted") {
|
|
531
|
+
chatBuffers.delete(runId);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (state === "final" && resolvedSessionKey) {
|
|
535
|
+
scheduleContextUsageRefresh(resolvedSessionKey, 450);
|
|
536
|
+
const bufferedText = runId ? chatBuffers.get(runId) ?? "" : "";
|
|
537
|
+
const resolvedText = currentText || bufferedText;
|
|
538
|
+
if (runId) {
|
|
539
|
+
chatBuffers.delete(runId);
|
|
540
|
+
}
|
|
541
|
+
if (resolvedText.trim()) {
|
|
542
|
+
if (runId) {
|
|
543
|
+
chatRunContexts.delete(runId);
|
|
544
|
+
}
|
|
545
|
+
const outgoingPayload = withMessageText(normalizedPayload, resolvedText);
|
|
546
|
+
void publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: resolvedSessionKey, limit: 10 });
|
|
550
|
+
withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history")
|
|
551
|
+
.then(async (history) => {
|
|
552
|
+
let outcome = runContext ? extractHistoryOutcome(history, runContext) : null;
|
|
553
|
+
// Retry once if OpenClaw has emitted final before the transcript is committed.
|
|
554
|
+
if (!outcome) {
|
|
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");
|
|
557
|
+
outcome = runContext ? extractHistoryOutcome(retryHistory, runContext) : null;
|
|
558
|
+
}
|
|
559
|
+
if (runId && outcome) {
|
|
560
|
+
chatRunContexts.delete(runId);
|
|
561
|
+
}
|
|
562
|
+
if (outcome?.kind === "final") {
|
|
563
|
+
const outgoingPayload = buildFinalPayloadFromHistoryOutcome(normalizedPayload, outcome);
|
|
564
|
+
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (outcome?.kind === "error") {
|
|
568
|
+
const outgoingPayload = {
|
|
569
|
+
...normalizedPayload,
|
|
570
|
+
state: "error",
|
|
571
|
+
errorMessage: outcome.errorMessage,
|
|
572
|
+
};
|
|
573
|
+
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (runId && runContext) {
|
|
577
|
+
scheduleChatHistoryFallback(runId, runContext, 0);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
await publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
581
|
+
})
|
|
582
|
+
.catch((err) => {
|
|
583
|
+
console.error(`[relay] chat.history fetch failed: ${err}`);
|
|
584
|
+
if (runId && runContext) {
|
|
585
|
+
scheduleChatHistoryFallback(runId, runContext, 0);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
void publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (runId && (state === "error" || state === "failed" || state === "fail" || state === "aborted")) {
|
|
593
|
+
chatRunContexts.delete(runId);
|
|
594
|
+
scheduleContextUsageRefresh(p?.sessionKey, 450);
|
|
595
|
+
}
|
|
596
|
+
void publishAndSendGatewayEvent(event, realtimePayload, shouldPublishOffice);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
void publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
gatewayClient.start();
|
|
603
|
+
});
|
|
604
|
+
relayWs.on("message", async (raw) => {
|
|
605
|
+
let requestId;
|
|
606
|
+
let methodForLog;
|
|
607
|
+
let voiceInputRun;
|
|
608
|
+
try {
|
|
609
|
+
const msg = parseRelayFrame(raw);
|
|
610
|
+
if (!msg) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (msg.type === "heartbeat") {
|
|
614
|
+
send({ type: "heartbeat" });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (msg.type === "hello") {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (msg.type !== "cmd" || !msg.method)
|
|
621
|
+
return;
|
|
622
|
+
requestId = msg.id;
|
|
623
|
+
methodForLog = msg.method;
|
|
624
|
+
// Handle clawpilot.provider.* commands locally (async)
|
|
625
|
+
const providerPromise = handleProviderCommand(msg.method, msg.params);
|
|
626
|
+
if (providerPromise !== null) {
|
|
627
|
+
const result = await providerPromise;
|
|
628
|
+
if (requestId) {
|
|
629
|
+
send({
|
|
630
|
+
type: "res",
|
|
631
|
+
id: requestId,
|
|
632
|
+
ok: result.ok,
|
|
633
|
+
...(result.ok
|
|
634
|
+
? { payload: result.payload }
|
|
635
|
+
: { error: { message: result.error } }),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// Handle local commands without forwarding to the gateway.
|
|
641
|
+
const localResult = await handleLocalCommand(msg.method, msg.params, {
|
|
642
|
+
requestId,
|
|
643
|
+
gatewayId: opts.gatewayId,
|
|
644
|
+
publishEvent: (event) => send(event),
|
|
645
|
+
});
|
|
646
|
+
if (localResult !== null) {
|
|
647
|
+
if (requestId) {
|
|
648
|
+
if (localResult.ok) {
|
|
649
|
+
send({ type: "res", id: requestId, ok: true, payload: localResult.payload });
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
send({ type: "res", id: requestId, ok: false, error: { message: localResult.error } });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (msg.method === "chat.voice.send") {
|
|
658
|
+
const voiceInput = await prepareOpenClawVoiceInputCommand(msg.params, {
|
|
659
|
+
requestId,
|
|
660
|
+
sessionDefaults,
|
|
661
|
+
});
|
|
662
|
+
voiceInputRun = voiceInput.run;
|
|
663
|
+
msg.params = voiceInput.params;
|
|
664
|
+
msg.method = voiceInput.method;
|
|
665
|
+
}
|
|
666
|
+
if (msg.method === "chat.send") {
|
|
667
|
+
msg.params = await prepareChatSendParams(msg.params);
|
|
668
|
+
}
|
|
669
|
+
const params = canonicalizeRelayParams(msg.method, msg.params, sessionDefaults);
|
|
670
|
+
const paramsRecord = params && typeof params === "object" && !Array.isArray(params)
|
|
671
|
+
? params
|
|
672
|
+
: undefined;
|
|
673
|
+
const chatSendDedupeRequest = msg.method === "chat.send"
|
|
674
|
+
? buildChatSendDedupeRequest(paramsRecord)
|
|
675
|
+
: undefined;
|
|
676
|
+
if (!gatewayClient && msg.method !== "chat.history") {
|
|
677
|
+
throw new Error("gateway not connected");
|
|
678
|
+
}
|
|
679
|
+
const commandPromise = msg.method === "chat.history"
|
|
680
|
+
? requestChatHistoryFromClawConnect(params)
|
|
681
|
+
: gatewayClient.request(msg.method, params);
|
|
682
|
+
commandPromise
|
|
683
|
+
.then(async (result) => {
|
|
684
|
+
let resolvedRunId;
|
|
685
|
+
if ((msg.method === "chat.send" || msg.method === "agent") && params && typeof params === "object" && !Array.isArray(params)) {
|
|
686
|
+
const paramsRecord = params;
|
|
687
|
+
const sessionKey = typeof paramsRecord.sessionKey === "string" && paramsRecord.sessionKey.trim().length > 0
|
|
688
|
+
? paramsRecord.sessionKey.trim()
|
|
689
|
+
: sessionDefaults.mainSessionKey;
|
|
690
|
+
const resultRecord = result && typeof result === "object" && !Array.isArray(result)
|
|
691
|
+
? result
|
|
692
|
+
: undefined;
|
|
693
|
+
const runId = typeof resultRecord?.runId === "string" && resultRecord.runId.trim().length > 0
|
|
694
|
+
? resultRecord.runId.trim()
|
|
695
|
+
: requestId;
|
|
696
|
+
resolvedRunId = runId;
|
|
697
|
+
if (runId) {
|
|
698
|
+
const promptText = typeof paramsRecord.message === "string" && paramsRecord.message.trim().length > 0
|
|
699
|
+
? paramsRecord.message.trim()
|
|
700
|
+
: undefined;
|
|
701
|
+
const runContext = {
|
|
702
|
+
sessionKey,
|
|
703
|
+
requestedAtMs: Date.now(),
|
|
704
|
+
promptText,
|
|
705
|
+
};
|
|
706
|
+
scheduleChatHistoryFallback(runId, runContext);
|
|
707
|
+
}
|
|
708
|
+
if (msg.method === "chat.send" && chatSendDedupeRequest) {
|
|
709
|
+
registerChatSendDedupe(chatSendDedupeRequest, runId);
|
|
710
|
+
}
|
|
711
|
+
// Let a model switch settle before publishing context usage again.
|
|
712
|
+
// Forced refreshes can replay a stale model snapshot and overwrite the new selection.
|
|
713
|
+
scheduleContextUsageRefresh(sessionKey, 1200);
|
|
714
|
+
}
|
|
715
|
+
if (requestId) {
|
|
716
|
+
const responsePayload = await relayOutgoingMediaForResponse(msg.method, result);
|
|
717
|
+
send({ type: "res", id: requestId, ok: true, payload: responsePayload });
|
|
718
|
+
}
|
|
719
|
+
})
|
|
720
|
+
.catch((err) => {
|
|
721
|
+
console.error(`[relay] cmd failed method=${msg.method} id=${requestId ?? "(no-id)"}: ${String(err)}`);
|
|
722
|
+
if (requestId) {
|
|
723
|
+
send({ type: "res", id: requestId, ok: false, error: { message: String(err) } });
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
729
|
+
console.error(`[relay] cmd failed method=${methodForLog ?? "(unknown)"} id=${requestId ?? "(no-id)"}: ${message}`);
|
|
730
|
+
const setupMessage = voiceInputSetupMessage(err);
|
|
731
|
+
if (setupMessage && voiceInputRun) {
|
|
732
|
+
send({
|
|
733
|
+
type: "event",
|
|
734
|
+
event: "chat",
|
|
735
|
+
payload: buildMobileAssistantErrorPayload({
|
|
736
|
+
run: voiceInputRun,
|
|
737
|
+
errorMessage: setupMessage,
|
|
738
|
+
includeTimelineEvents: true,
|
|
739
|
+
}),
|
|
740
|
+
});
|
|
741
|
+
if (requestId) {
|
|
742
|
+
send({ type: "res", id: requestId, ok: true, payload: voiceInputRun });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
else if (requestId) {
|
|
746
|
+
send({ type: "res", id: requestId, ok: false, error: { message } });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
relayWs.on("close", (code, reason) => {
|
|
751
|
+
console.log(`Relay connection closed: ${code} ${reason.toString()}`);
|
|
752
|
+
opts.onDisconnected?.();
|
|
753
|
+
gatewayClient?.stop();
|
|
754
|
+
gatewayClient = null;
|
|
755
|
+
for (const timer of chatFallbacks.values()) {
|
|
756
|
+
clearTimeout(timer);
|
|
757
|
+
}
|
|
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();
|
|
766
|
+
for (const timer of contextUsageRefreshes.values()) {
|
|
767
|
+
clearTimeout(timer);
|
|
768
|
+
}
|
|
769
|
+
contextUsageRefreshes.clear();
|
|
770
|
+
resolve(shouldRetryRelayClose(code, opts.signal));
|
|
771
|
+
});
|
|
772
|
+
relayWs.on("error", (err) => {
|
|
773
|
+
console.error("Relay WebSocket error:", err.message);
|
|
774
|
+
// close event will follow
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
export function buildRelayHelloMessage(opts) {
|
|
779
|
+
return {
|
|
780
|
+
type: "hello",
|
|
781
|
+
platform: opts.platform,
|
|
782
|
+
agentVersion: opts.agentVersion,
|
|
783
|
+
capabilities: opts.capabilities,
|
|
784
|
+
slashCommands: OPENCLAW_SLASH_COMMAND_CATALOG,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
//# sourceMappingURL=relay-manager.js.map
|