clawconnect-agent 0.2.5 → 0.2.7
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 +7 -3
- package/README.zh-CN.md +7 -3
- package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
- package/dist/commands/hermes-file-transfer-outcome.js +40 -0
- package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
- package/dist/commands/send-file.js +39 -33
- package/dist/commands/send-file.js.map +1 -1
- package/dist/core/command-types.d.ts +5 -0
- package/dist/core/relay/chat-payload.js +32 -22
- package/dist/core/relay/chat-payload.js.map +1 -1
- package/dist/core/relay/file-upload.d.ts +3 -0
- package/dist/core/relay/file-upload.js +4 -0
- package/dist/core/relay/file-upload.js.map +1 -1
- package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
- package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
- package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
- package/dist/core/relay/office-payload.js +54 -22
- package/dist/core/relay/office-payload.js.map +1 -1
- package/dist/core/relay/source-commit.d.ts +41 -0
- package/dist/core/relay/source-commit.js +118 -0
- package/dist/core/relay/source-commit.js.map +1 -0
- package/dist/core/relay/timeline-event-builder.d.ts +6 -0
- package/dist/core/relay/timeline-event-builder.js +3 -0
- package/dist/core/relay/timeline-event-builder.js.map +1 -1
- package/dist/core/relay/timeline-event-log.d.ts +29 -0
- package/dist/core/relay/timeline-event-log.js +37 -0
- package/dist/core/relay/timeline-event-log.js.map +1 -1
- package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
- package/dist/core/relay/timeline-projection-v3.js +87 -0
- package/dist/core/relay/timeline-projection-v3.js.map +1 -0
- package/dist/hermes/hermes-relay-manager.js +3 -0
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
- package/dist/hermes/hermes-runtime-test-support.js +12 -6
- package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
- package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
- package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
- package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
- package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
- package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/openclaw/config/provider-config.js +4 -1
- package/dist/openclaw/config/provider-config.js.map +1 -1
- package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
- package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
- package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
- package/dist/openclaw/relay/assistant-media-sidecar.d.ts +28 -1
- package/dist/openclaw/relay/assistant-media-sidecar.js +270 -10
- package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
- package/dist/openclaw/relay/chat-history.d.ts +37 -2
- package/dist/openclaw/relay/chat-history.js +570 -49
- package/dist/openclaw/relay/chat-history.js.map +1 -1
- package/dist/openclaw/relay/chat-run-identity.js +3 -1
- package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
- package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
- package/dist/openclaw/relay/gateway-history-projection.js +67 -0
- package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
- package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
- package/dist/openclaw/relay/outgoing-media-relay.d.ts +35 -1
- package/dist/openclaw/relay/outgoing-media-relay.js +541 -123
- package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
- package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
- package/dist/openclaw/relay/session-context.d.ts +15 -0
- package/dist/openclaw/relay/session-context.js +48 -4
- package/dist/openclaw/relay/session-context.js.map +1 -1
- package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
- package/dist/openclaw/relay/source-commit-projection.js +140 -0
- package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
- package/dist/openclaw/relay/transcript-dedupe.js +11 -42
- package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
- package/dist/openclaw/relay-manager.js +329 -131
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/openclaw/runtime/local-runtime.js +4 -2
- package/dist/openclaw/runtime/local-runtime.js.map +1 -1
- package/package.json +1 -1
- package/dist/openclaw/session-store.d.ts +0 -6
- package/dist/openclaw/session-store.js +0 -231
- package/dist/openclaw/session-store.js.map +0 -1
|
@@ -4,25 +4,36 @@ import { handleLocalCommand } from "./handlers/local-handlers.js";
|
|
|
4
4
|
import { handleProviderCommand } from "./handlers/provider-handlers.js";
|
|
5
5
|
import { DEFAULT_GATEWAY_SESSION_DEFAULTS, buildContextUsageFingerprint, contextUsageSnapshotFromSessionsList, readContextUsageSnapshot, canonicalizeRelayParams, canonicalizeSessionKey, extractGatewaySessionDefaults, } from "./relay/session-context.js";
|
|
6
6
|
import { appendUniqueSuffix, extractChatRole, extractChatText, normalizeChatEventPayload, normalizeChatState, withMessageText, } from "../core/relay/chat-payload.js";
|
|
7
|
-
import { canonicalizeOpenClawGatewayHistoryResponse, extractHistoryOutcome, readOpenClawTranscriptChatHistory, withTimeout, } from "./relay/chat-history.js";
|
|
7
|
+
import { canonicalizeOpenClawGatewayHistoryResponse, extractHistoryMediaContent, extractExplicitParentMediaContent, extractHistoryOutcome, readOpenClawSourceCommitCursor, readOpenClawTranscriptChatHistory, withTimeout, } from "./relay/chat-history.js";
|
|
8
|
+
import { watchOpenClawSourceCommit } from "./relay/openclaw-source-commit-watcher.js";
|
|
9
|
+
import { projectSourceCommitHistoryPages } from "./relay/source-commit-projection.js";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { resolveOpenClawStateDir } from "./runtime/openclaw-paths.js";
|
|
12
|
+
import { openClawAgentIdFromSessionKey } from "../core/relay/timeline-projection-v3.js";
|
|
8
13
|
import { OpenClawChatSendDedupeCoordinator, } from "./relay/chat-send-dedupe-coordinator.js";
|
|
9
|
-
import { buildMobileAssistantDeltaPayload, buildMobileAssistantErrorPayload, buildMobileAssistantFinalPayload, } from "../core/relay/mobile-chat-run-bridge.js";
|
|
14
|
+
import { buildMobileAssistantDeltaPayload, buildMobileAssistantCompletedPayload, buildMobileAssistantErrorPayload, buildMobileAssistantFinalPayload, } from "../core/relay/mobile-chat-run-bridge.js";
|
|
10
15
|
import { LatestSnapshotCoalescer, buildAssistantStreamSnapshotKey, } from "../core/relay/assistant-stream-coalescer.js";
|
|
11
16
|
import { reliableRelayOutboxForGateway } from "../core/relay/reliable-relay-outbox-registry.js";
|
|
12
17
|
import { isValidRelayHello, reliableDeliveryModeFromRelayHello, RELAY_HELLO_NEGOTIATION_TIMEOUT_MS, } from "../core/relay/reliable-delivery-protocol.js";
|
|
13
18
|
import { buildChatSendIdempotencyRequest, ChatSendIdempotencyGuard, } from "../core/relay/chat-send-idempotency.js";
|
|
14
19
|
import { prepareChatSendParams } from "./relay/chat-send-attachments.js";
|
|
15
|
-
import { canonicalizeOpenClawAssistantMediaSidecarPayload } from "./relay/assistant-media-sidecar.js";
|
|
20
|
+
import { canonicalizeOpenClawAssistantMediaSidecarPayload, materializeOpenClawDisplayContentPayload, } from "./relay/assistant-media-sidecar.js";
|
|
16
21
|
import { relayOutgoingMediaInHistoryResponse, relayOutgoingMediaInPayload, } from "./relay/outgoing-media-relay.js";
|
|
17
22
|
import { prepareOpenClawVoiceInputCommand } from "./relay/openclaw-voice-input.js";
|
|
18
23
|
import { voiceInputSetupMessage } from "../core/relay/voice-input.js";
|
|
19
24
|
import { buildRelayHelloMessage } from "./relay/relay-manager-hello.js";
|
|
20
25
|
import { buildOpenClawHostRuntimeMetadata } from "../runtime-metadata.js";
|
|
21
|
-
import {
|
|
26
|
+
import { CHAT_HISTORY_FETCH_TIMEOUT_MS, CHAT_HISTORY_MEDIA_ENRICHMENT_TIMEOUT_MS, } from "./relay/relay-manager-history-timing.js";
|
|
22
27
|
import { canonicalizeOpenClawChatSendResult, normalizeOpenClawMobileRunId, openClawChatRunIdentities, resolveExplicitMobileRunIdFromChatPayload, restoreOpenClawProviderRunIdForCommand, } from "./relay/chat-run-identity.js";
|
|
28
|
+
import { markOpenClawActiveRunTerminal, recordOpenClawActiveRun, updateOpenClawActiveRunProviderId, } from "./relay/openclaw-active-run-state.js";
|
|
23
29
|
import { publishOfficeSnapshot } from "./relay/relay-manager-office-events.js";
|
|
30
|
+
import { enrichAgentsListWithIdentities } from "./relay/agent-identity-helpers.js";
|
|
24
31
|
export { buildRelayHelloMessage } from "./relay/relay-manager-hello.js";
|
|
25
|
-
import { buildEmptyHistoryPage, buildFinalPayloadFromHistoryOutcome, buildOpenClawGatewayHistoryParams, extractChatErrorMessage, hasHistoryCursor, mergeCanonicalChatPayload, mobileAssistantUsageFromPayload, nonTextContentBlocks, nonTextContentBlocksFromHistory, resolveChatPayloadSeq, resolveChatPayloadTimestamp, } from "./relay/relay-manager-payload-helpers.js";
|
|
32
|
+
import { buildEmptyHistoryPage, buildFinalPayloadFromHistoryOutcome, buildOpenClawGatewayHistoryParams, assistantHistoryReplyMessageId, assistantReplyMessageId, extractChatErrorMessage, extractChatLineage, hasHistoryCursor, mergeCanonicalChatPayload, mobileAssistantUsageFromPayload, mergeNonTextContentBlocks, nonTextContentBlocks, nonTextContentBlocksFromHistory, resolveChatPayloadSeq, resolveChatPayloadTimestamp, } from "./relay/relay-manager-payload-helpers.js";
|
|
33
|
+
function hasCanonicalProjectionV3Snapshot(history) {
|
|
34
|
+
return history?.projectionVersion === 3
|
|
35
|
+
&& (history.timelineSnapshot?.messages ?? []).some((message) => message.projectionVersion === 3);
|
|
36
|
+
}
|
|
26
37
|
// 保留在连接实例之外,使 Relay WebSocket 断线重连后的同轮重投仍能复用终态结果。
|
|
27
38
|
const openClawChatSendIdempotency = new ChatSendIdempotencyGuard();
|
|
28
39
|
// ---------------------------------------------------------------------------
|
|
@@ -62,7 +73,7 @@ export async function runRelayManager(opts) {
|
|
|
62
73
|
let sessionDefaults = { ...DEFAULT_GATEWAY_SESSION_DEFAULTS };
|
|
63
74
|
const subscribedSessionMessageKeys = new Set();
|
|
64
75
|
const pendingSessionMessageSubscriptions = new Map();
|
|
65
|
-
const
|
|
76
|
+
const sourceCommitWatchers = new Map();
|
|
66
77
|
const chatRunContexts = new Map();
|
|
67
78
|
const chatSendDedupe = new OpenClawChatSendDedupeCoordinator(() => sessionDefaults);
|
|
68
79
|
const outgoingMediaUploadCache = new Map();
|
|
@@ -89,11 +100,16 @@ export async function runRelayManager(opts) {
|
|
|
89
100
|
},
|
|
90
101
|
});
|
|
91
102
|
};
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
const setChatRunContext = (providerRunId, context) => {
|
|
104
|
+
chatRunContexts.set(providerRunId, context);
|
|
105
|
+
// Delivery-mirror terminals can arrive after the provider terminal. Keep
|
|
106
|
+
// enough completed contexts for deterministic history reconciliation;
|
|
107
|
+
// bound the map explicitly instead of using a timer as correctness.
|
|
108
|
+
while (chatRunContexts.size > 2048) {
|
|
109
|
+
const oldest = chatRunContexts.keys().next().value;
|
|
110
|
+
if (!oldest)
|
|
111
|
+
break;
|
|
112
|
+
chatRunContexts.delete(oldest);
|
|
97
113
|
}
|
|
98
114
|
};
|
|
99
115
|
const publishContextUsageSnapshot = async (sessionKey, force = false) => {
|
|
@@ -144,72 +160,19 @@ export async function runRelayManager(opts) {
|
|
|
144
160
|
timer.unref?.();
|
|
145
161
|
contextUsageRefreshes.set(key, timer);
|
|
146
162
|
};
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
const explicitOpenClawSessionKey = (rawSessionKey) => {
|
|
164
|
+
const canonical = canonicalizeSessionKey(rawSessionKey, sessionDefaults);
|
|
165
|
+
const normalized = typeof canonical === "string" ? canonical.trim() : "";
|
|
166
|
+
if (normalized === "main") {
|
|
167
|
+
return `agent:${sessionDefaults.defaultAgentId ?? "main"}:${sessionDefaults.mainKey || "main"}`;
|
|
150
168
|
}
|
|
151
|
-
|
|
152
|
-
chatRunContexts.set(providerRunId, context);
|
|
153
|
-
const timer = setTimeout(() => {
|
|
154
|
-
if (!gatewayClient) {
|
|
155
|
-
chatFallbacks.delete(providerRunId);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: context.sessionKey, limit: 10 });
|
|
159
|
-
withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history fallback")
|
|
160
|
-
.then(async (history) => {
|
|
161
|
-
const outcome = extractHistoryOutcome(history, context);
|
|
162
|
-
if (!outcome && attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
|
|
163
|
-
scheduleChatHistoryFallback(providerRunId, context, attempt + 1);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (!outcome) {
|
|
167
|
-
chatFallbacks.delete(providerRunId);
|
|
168
|
-
chatRunContexts.delete(providerRunId);
|
|
169
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
clearChatFallback(providerRunId);
|
|
173
|
-
chatRunContexts.delete(providerRunId);
|
|
174
|
-
if (outcome.kind === "final") {
|
|
175
|
-
openClawChatRunIdentities.markTerminal(opts.gatewayId, providerRunId);
|
|
176
|
-
const basePayload = buildMobileAssistantFinalPayload({
|
|
177
|
-
run: { runId: context.canonicalRunId, sessionKey: context.sessionKey },
|
|
178
|
-
text: outcome.text,
|
|
179
|
-
includeTimelineEvents: true,
|
|
180
|
-
});
|
|
181
|
-
await publishAndSendGatewayEvent("chat", buildFinalPayloadFromHistoryOutcome(basePayload, outcome), true, context.promptText);
|
|
182
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
openClawChatRunIdentities.markTerminal(opts.gatewayId, providerRunId);
|
|
186
|
-
await publishAndSendGatewayEvent("chat", buildMobileAssistantErrorPayload({
|
|
187
|
-
run: { runId: context.canonicalRunId, sessionKey: context.sessionKey },
|
|
188
|
-
errorMessage: outcome.errorMessage,
|
|
189
|
-
includeTimelineEvents: true,
|
|
190
|
-
}), true);
|
|
191
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
192
|
-
})
|
|
193
|
-
.catch((err) => {
|
|
194
|
-
if (attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
|
|
195
|
-
scheduleChatHistoryFallback(providerRunId, context, attempt + 1);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
console.warn(`[relay] chat history fallback failed runId=${providerRunId}: ${String(err)}`);
|
|
199
|
-
chatFallbacks.delete(providerRunId);
|
|
200
|
-
chatRunContexts.delete(providerRunId);
|
|
201
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
202
|
-
});
|
|
203
|
-
}, attempt === 0 ? CHAT_HISTORY_FALLBACK_INITIAL_DELAY_MS : CHAT_HISTORY_FALLBACK_RETRY_DELAY_MS);
|
|
204
|
-
timer.unref?.();
|
|
205
|
-
chatFallbacks.set(providerRunId, timer);
|
|
169
|
+
return normalized || rawSessionKey.trim();
|
|
206
170
|
};
|
|
207
171
|
const ensureSessionMessagesSubscribed = async (sessionKey) => {
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
172
|
+
const key = explicitOpenClawSessionKey(sessionKey);
|
|
173
|
+
if (!key) {
|
|
210
174
|
throw new Error("cannot subscribe to an empty OpenClaw session key");
|
|
211
175
|
}
|
|
212
|
-
const key = normalizedSessionKey.trim();
|
|
213
176
|
if (subscribedSessionMessageKeys.has(key)) {
|
|
214
177
|
return;
|
|
215
178
|
}
|
|
@@ -248,6 +211,7 @@ export async function runRelayManager(opts) {
|
|
|
248
211
|
sessionDefaults = nextDefaults;
|
|
249
212
|
}
|
|
250
213
|
await ensureSessionMessagesSubscribed(sessionDefaults.mainSessionKey);
|
|
214
|
+
ensureSourceCommitWatcher(sessionDefaults.mainSessionKey);
|
|
251
215
|
const sessionsPayload = await gatewayClient.request("sessions.list", {
|
|
252
216
|
limit: 100,
|
|
253
217
|
includeGlobal: true,
|
|
@@ -263,6 +227,11 @@ export async function runRelayManager(opts) {
|
|
|
263
227
|
: undefined;
|
|
264
228
|
if (typeof sessionKey !== "string" || !sessionKey.trim())
|
|
265
229
|
continue;
|
|
230
|
+
// Cron jobs can target an agent session other than the default one.
|
|
231
|
+
// Subscribe each known session so their finished replies travel through
|
|
232
|
+
// the same ClawConnect → Relay realtime path as an interactive chat.
|
|
233
|
+
await ensureSessionMessagesSubscribed(sessionKey);
|
|
234
|
+
ensureSourceCommitWatcher(sessionKey);
|
|
266
235
|
const snapshot = contextUsageSnapshotFromSessionsList(sessionsPayload, sessionKey, sessionDefaults);
|
|
267
236
|
if (!snapshot)
|
|
268
237
|
continue;
|
|
@@ -334,6 +303,8 @@ export async function runRelayManager(opts) {
|
|
|
334
303
|
senderDisplayName: "OpenClaw",
|
|
335
304
|
cache: outgoingMediaUploadCache,
|
|
336
305
|
userMessage,
|
|
306
|
+
waitForOutgoingMediaRecord: true,
|
|
307
|
+
sessionDefaults,
|
|
337
308
|
});
|
|
338
309
|
}
|
|
339
310
|
catch (error) {
|
|
@@ -369,14 +340,14 @@ export async function runRelayManager(opts) {
|
|
|
369
340
|
console.warn(`[relay] assistant snapshot publish failed key=${key}: ${String(error)}`);
|
|
370
341
|
},
|
|
371
342
|
});
|
|
372
|
-
const assistantSnapshotKey = (runId, sessionKey) => {
|
|
343
|
+
const assistantSnapshotKey = (runId, sessionKey, messageId) => {
|
|
373
344
|
if (!runId?.trim() || !sessionKey?.trim()) {
|
|
374
345
|
return undefined;
|
|
375
346
|
}
|
|
376
347
|
return buildAssistantStreamSnapshotKey({
|
|
377
348
|
sessionKey,
|
|
378
349
|
runId,
|
|
379
|
-
messageId: `assistant-${runId}
|
|
350
|
+
...(messageId ? { messageId } : { messageId: `assistant-${runId}` }),
|
|
380
351
|
});
|
|
381
352
|
};
|
|
382
353
|
const runAfterAssistantSnapshot = (key, close, operation, releaseClosed = false) => {
|
|
@@ -417,7 +388,7 @@ export async function runRelayManager(opts) {
|
|
|
417
388
|
const sessionKey = typeof record.sessionKey === "string" && record.sessionKey.trim()
|
|
418
389
|
? record.sessionKey.trim()
|
|
419
390
|
: runContext?.sessionKey;
|
|
420
|
-
return assistantSnapshotKey(runId, sessionKey);
|
|
391
|
+
return assistantSnapshotKey(runId, sessionKey, runId ? assistantReplyMessageId(payload, runId) : undefined);
|
|
421
392
|
};
|
|
422
393
|
async function relayOutgoingMediaForResponse(method, payload) {
|
|
423
394
|
if (method !== "chat.history") {
|
|
@@ -429,12 +400,23 @@ export async function runRelayManager(opts) {
|
|
|
429
400
|
gatewayId: opts.gatewayId,
|
|
430
401
|
senderDisplayName: "OpenClaw",
|
|
431
402
|
cache: outgoingMediaUploadCache,
|
|
403
|
+
sessionDefaults,
|
|
432
404
|
});
|
|
433
405
|
}
|
|
434
406
|
async function requestChatHistoryFromClawConnect(params) {
|
|
407
|
+
// Relay-owned history reads opt into the v3 projection contract. The
|
|
408
|
+
// public legacy helper remains v2-compatible for older callers during
|
|
409
|
+
// rollout; every confirmed row consumed by this manager is v3.
|
|
410
|
+
const projectionParams = params && typeof params === "object" && !Array.isArray(params)
|
|
411
|
+
? {
|
|
412
|
+
...params,
|
|
413
|
+
projectionVersion: 3,
|
|
414
|
+
projectionGatewayId: opts.gatewayId,
|
|
415
|
+
}
|
|
416
|
+
: { projectionVersion: 3, projectionGatewayId: opts.gatewayId };
|
|
435
417
|
try {
|
|
436
|
-
await chatSendDedupe.dedupePendingForSession(
|
|
437
|
-
const transcriptHistory = await readOpenClawTranscriptChatHistory(
|
|
418
|
+
await chatSendDedupe.dedupePendingForSession(projectionParams);
|
|
419
|
+
const transcriptHistory = await readOpenClawTranscriptChatHistory(projectionParams, sessionDefaults);
|
|
438
420
|
if (transcriptHistory) {
|
|
439
421
|
return transcriptHistory;
|
|
440
422
|
}
|
|
@@ -443,7 +425,7 @@ export async function runRelayManager(opts) {
|
|
|
443
425
|
console.warn(`[relay] transcript chat.history unavailable: ${String(err)}`);
|
|
444
426
|
}
|
|
445
427
|
if (hasHistoryCursor(params)) {
|
|
446
|
-
return buildEmptyHistoryPage(
|
|
428
|
+
return buildEmptyHistoryPage(projectionParams, sessionDefaults);
|
|
447
429
|
}
|
|
448
430
|
if (!gatewayClient) {
|
|
449
431
|
throw new Error("gateway not connected");
|
|
@@ -452,8 +434,56 @@ export async function runRelayManager(opts) {
|
|
|
452
434
|
const history = await gatewayClient.request("chat.history", gatewayHistoryParams);
|
|
453
435
|
return canonicalizeOpenClawGatewayHistoryResponse(history, {
|
|
454
436
|
sessionKey: String(gatewayHistoryParams.sessionKey),
|
|
437
|
+
projectionVersion: 3,
|
|
438
|
+
projectionGatewayId: opts.gatewayId,
|
|
439
|
+
sessionDefaults,
|
|
455
440
|
});
|
|
456
441
|
}
|
|
442
|
+
const ensureSourceCommitWatcher = (sessionKey) => {
|
|
443
|
+
const normalizedSessionKey = explicitOpenClawSessionKey(sessionKey);
|
|
444
|
+
if (!normalizedSessionKey)
|
|
445
|
+
return;
|
|
446
|
+
const key = `${opts.gatewayId}\u0000${normalizedSessionKey}`;
|
|
447
|
+
if (sourceCommitWatchers.has(key))
|
|
448
|
+
return;
|
|
449
|
+
const agentId = openClawAgentIdFromSessionKey(normalizedSessionKey);
|
|
450
|
+
const databasePath = join(resolveOpenClawStateDir(), "agents", agentId, "agent", "openclaw-agent.sqlite");
|
|
451
|
+
const watcher = watchOpenClawSourceCommit({
|
|
452
|
+
databasePath,
|
|
453
|
+
readCursor: () => readOpenClawSourceCommitCursor({
|
|
454
|
+
sessionKey: normalizedSessionKey,
|
|
455
|
+
projectionVersion: 3,
|
|
456
|
+
projectionGatewayId: opts.gatewayId,
|
|
457
|
+
}, sessionDefaults),
|
|
458
|
+
onCommit: async (sourceCommit, previousCommittedThroughSeq) => {
|
|
459
|
+
await projectSourceCommitHistoryPages({
|
|
460
|
+
sourceCommit,
|
|
461
|
+
previousCommittedThroughSeq,
|
|
462
|
+
readPage: (cursorSeq) => withTimeout(requestChatHistoryFromClawConnect({
|
|
463
|
+
sessionKey: normalizedSessionKey,
|
|
464
|
+
limit: 200,
|
|
465
|
+
cursor: `seq:${cursorSeq}`,
|
|
466
|
+
direction: "newer",
|
|
467
|
+
}), CHAT_HISTORY_FETCH_TIMEOUT_MS, "source commit history reconciliation"),
|
|
468
|
+
publish: async ({ sourceCommit: pageSourceCommit, events }) => {
|
|
469
|
+
const deliveryResult = await publishAndSendGatewayEvent("chat", {
|
|
470
|
+
state: "source_commit",
|
|
471
|
+
sessionKey: normalizedSessionKey,
|
|
472
|
+
sourceCommit: pageSourceCommit,
|
|
473
|
+
timelineEvents: events,
|
|
474
|
+
}, true, undefined, true);
|
|
475
|
+
if (deliveryResult?.status === "retryable") {
|
|
476
|
+
throw deliveryResult.error;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
},
|
|
481
|
+
onError: (error) => {
|
|
482
|
+
console.warn(`[relay] source commit reconciliation failed session=${normalizedSessionKey}: ${String(error)}`);
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
sourceCommitWatchers.set(key, watcher);
|
|
486
|
+
};
|
|
457
487
|
relayWs.on("open", () => {
|
|
458
488
|
console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
|
|
459
489
|
opts.onConnected?.();
|
|
@@ -495,8 +525,28 @@ export async function runRelayManager(opts) {
|
|
|
495
525
|
});
|
|
496
526
|
},
|
|
497
527
|
onEvent: (event, payload) => {
|
|
528
|
+
if (event === "session.message") {
|
|
529
|
+
// OpenClaw emits this subscription event after the transcript
|
|
530
|
+
// append has committed. It is the primary wake-up for source
|
|
531
|
+
// reconciliation; SQLite notifications and the low-frequency
|
|
532
|
+
// anti-entropy scan remain recovery paths for dropped events.
|
|
533
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
534
|
+
? payload
|
|
535
|
+
: undefined;
|
|
536
|
+
const rawSessionKey = typeof record?.sessionKey === "string"
|
|
537
|
+
? record.sessionKey.trim()
|
|
538
|
+
: "";
|
|
539
|
+
if (rawSessionKey) {
|
|
540
|
+
const sessionKey = canonicalizeSessionKey(rawSessionKey, sessionDefaults);
|
|
541
|
+
if (typeof sessionKey === "string" && sessionKey.trim()) {
|
|
542
|
+
ensureSourceCommitWatcher(sessionKey);
|
|
543
|
+
sourceCommitWatchers.get(`${opts.gatewayId}\u0000${sessionKey.trim()}`)?.notify();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
498
548
|
const normalizedPayload = event === "chat"
|
|
499
|
-
? canonicalizeOpenClawAssistantMediaSidecarPayload(normalizeChatEventPayload(payload))
|
|
549
|
+
? materializeOpenClawDisplayContentPayload(canonicalizeOpenClawAssistantMediaSidecarPayload(normalizeChatEventPayload(payload)))
|
|
500
550
|
: payload;
|
|
501
551
|
const shouldPublishOffice = event === "chat" || event === "agent" || event === "context_usage";
|
|
502
552
|
if (event === "chat") {
|
|
@@ -507,10 +557,16 @@ export async function runRelayManager(opts) {
|
|
|
507
557
|
const isTerminalState = state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted";
|
|
508
558
|
const sessionKey = typeof p?.sessionKey === "string" && p.sessionKey.trim().length > 0 ? p.sessionKey.trim() : undefined;
|
|
509
559
|
const explicitCanonicalRunId = resolveExplicitMobileRunIdFromChatPayload(normalizedPayload);
|
|
560
|
+
const currentText = extractChatText(normalizedPayload);
|
|
561
|
+
const role = extractChatRole(normalizedPayload);
|
|
510
562
|
let runContext = providerRunId
|
|
511
563
|
? chatRunContexts.get(providerRunId) ?? openClawChatRunIdentities.resolve(opts.gatewayId, providerRunId)
|
|
512
564
|
: undefined;
|
|
513
|
-
|
|
565
|
+
// Event-only registration is safe only after the provider proves
|
|
566
|
+
// assistant provenance. A roleless terminal may contain a user
|
|
567
|
+
// echo or an unrelated provider event and must not create a run
|
|
568
|
+
// context from its text/session fields.
|
|
569
|
+
if (!runContext && providerRunId && sessionKey && role === "assistant") {
|
|
514
570
|
runContext = {
|
|
515
571
|
gatewayId: opts.gatewayId,
|
|
516
572
|
providerRunId,
|
|
@@ -518,19 +574,37 @@ export async function runRelayManager(opts) {
|
|
|
518
574
|
sessionKey,
|
|
519
575
|
};
|
|
520
576
|
openClawChatRunIdentities.ensure(runContext);
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
if (providerRunId && isStreamingState && openClawChatRunIdentities.isTerminal(opts.gatewayId, providerRunId)) {
|
|
524
|
-
return;
|
|
577
|
+
setChatRunContext(providerRunId, runContext);
|
|
525
578
|
}
|
|
526
579
|
if (providerRunId && isTerminalState) {
|
|
527
580
|
openClawChatRunIdentities.markTerminal(opts.gatewayId, providerRunId);
|
|
528
581
|
}
|
|
529
582
|
const canonicalRunId = runContext?.canonicalRunId ?? explicitCanonicalRunId ?? providerRunId;
|
|
530
583
|
const resolvedSessionKey = sessionKey ?? runContext?.sessionKey;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
584
|
+
if (isTerminalState && runContext && canonicalRunId && resolvedSessionKey) {
|
|
585
|
+
void markOpenClawActiveRunTerminal({
|
|
586
|
+
gatewayId: opts.gatewayId,
|
|
587
|
+
sessionKey: resolvedSessionKey,
|
|
588
|
+
sourceRunId: canonicalRunId,
|
|
589
|
+
}).catch((error) => {
|
|
590
|
+
console.warn(`[relay] failed to mark OpenClaw run terminal: ${String(error)}`);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const replyMessageId = role === "assistant" && canonicalRunId
|
|
594
|
+
? assistantReplyMessageId(normalizedPayload, canonicalRunId)
|
|
595
|
+
: undefined;
|
|
596
|
+
const chatLineage = extractChatLineage(normalizedPayload);
|
|
597
|
+
const runSnapshotKey = assistantSnapshotKey(canonicalRunId, resolvedSessionKey, replyMessageId);
|
|
598
|
+
// OpenClaw's message tool may emit several independent assistant
|
|
599
|
+
// replies after a provider-level terminal snapshot. Keep filtering
|
|
600
|
+
// late stream deltas for the run-level text reply, but let a reply
|
|
601
|
+
// with its own upstream message/tool/media identity through.
|
|
602
|
+
if (providerRunId
|
|
603
|
+
&& isStreamingState
|
|
604
|
+
&& openClawChatRunIdentities.isTerminal(opts.gatewayId, providerRunId)
|
|
605
|
+
&& !replyMessageId) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
534
608
|
if (isOpenClawHeartbeatText(currentText) || (runContext?.promptText && isOpenClawHeartbeatText(runContext.promptText))) {
|
|
535
609
|
if (providerRunId && (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted")) {
|
|
536
610
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
@@ -541,6 +615,51 @@ export async function runRelayManager(opts) {
|
|
|
541
615
|
}
|
|
542
616
|
return;
|
|
543
617
|
}
|
|
618
|
+
// A roleless error/abort is still a terminal lifecycle signal when
|
|
619
|
+
// it belongs to a previously registered provider run. Its identity
|
|
620
|
+
// comes from that run context; never infer it from the payload
|
|
621
|
+
// text. A roleless empty final is lifecycle-only and leaves source
|
|
622
|
+
// rows to the transcript/source-commit watcher. Text-bearing
|
|
623
|
+
// roleless deltas/finals remain rejected user/source echoes.
|
|
624
|
+
if (role !== "assistant") {
|
|
625
|
+
if (!runContext || !providerRunId || !canonicalRunId || !resolvedSessionKey) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (state === "error" || state === "failed" || state === "fail" || state === "aborted") {
|
|
629
|
+
chatSendDedupe.scheduleForRun(providerRunId, 100);
|
|
630
|
+
chatRunContexts.delete(providerRunId);
|
|
631
|
+
runAfterAssistantSnapshot(runSnapshotKey, true, async () => {
|
|
632
|
+
await publishAndSendGatewayEvent(event, mergeCanonicalChatPayload(normalizedPayload, buildMobileAssistantErrorPayload({
|
|
633
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
634
|
+
errorMessage: extractChatErrorMessage(normalizedPayload),
|
|
635
|
+
includeTimelineEvents: true,
|
|
636
|
+
})), shouldPublishOffice, runContext?.promptText);
|
|
637
|
+
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
638
|
+
}, true);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (state === "final" && !currentText) {
|
|
642
|
+
chatRunContexts.delete(providerRunId);
|
|
643
|
+
const lifecyclePayload = normalizedPayload && typeof normalizedPayload === "object" && !Array.isArray(normalizedPayload)
|
|
644
|
+
? (() => {
|
|
645
|
+
const payload = { ...normalizedPayload };
|
|
646
|
+
delete payload.message;
|
|
647
|
+
delete payload.content;
|
|
648
|
+
delete payload.text;
|
|
649
|
+
delete payload.delta;
|
|
650
|
+
return payload;
|
|
651
|
+
})()
|
|
652
|
+
: normalizedPayload;
|
|
653
|
+
runAfterAssistantSnapshot(runSnapshotKey, true, async () => {
|
|
654
|
+
await publishAndSendGatewayEvent(event, mergeCanonicalChatPayload(lifecyclePayload, buildMobileAssistantCompletedPayload({
|
|
655
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
656
|
+
includeTimelineEvents: true,
|
|
657
|
+
})), shouldPublishOffice, runContext?.promptText);
|
|
658
|
+
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
659
|
+
}, true);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
544
663
|
let realtimePayload = providerRunId && canonicalRunId && canonicalRunId !== providerRunId
|
|
545
664
|
? {
|
|
546
665
|
...normalizedPayload,
|
|
@@ -552,9 +671,6 @@ export async function runRelayManager(opts) {
|
|
|
552
671
|
if (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted") {
|
|
553
672
|
chatSendDedupe.scheduleForRun(providerRunId, 100);
|
|
554
673
|
}
|
|
555
|
-
if (role === "assistant" && (state === "delta" || state === "final" || state === "error" || state === "failed" || state === "fail")) {
|
|
556
|
-
clearChatFallback(providerRunId);
|
|
557
|
-
}
|
|
558
674
|
if (state === "delta" || state === "streaming" || state === "in_progress") {
|
|
559
675
|
const previousText = openClawChatRunIdentities.accumulatedText(opts.gatewayId, providerRunId);
|
|
560
676
|
const bufferedText = appendUniqueSuffix(previousText, currentText);
|
|
@@ -566,6 +682,7 @@ export async function runRelayManager(opts) {
|
|
|
566
682
|
seq: resolveChatPayloadSeq(normalizedPayload),
|
|
567
683
|
timestampMs: resolveChatPayloadTimestamp(normalizedPayload),
|
|
568
684
|
delta: bufferedText,
|
|
685
|
+
...chatLineage,
|
|
569
686
|
includeTimelineEvents: true,
|
|
570
687
|
}))
|
|
571
688
|
: withMessageText(normalizedPayload, bufferedText);
|
|
@@ -593,27 +710,79 @@ export async function runRelayManager(opts) {
|
|
|
593
710
|
});
|
|
594
711
|
return;
|
|
595
712
|
}
|
|
596
|
-
if (state === "final" && resolvedSessionKey) {
|
|
713
|
+
if (state === "final" && resolvedSessionKey && role === "assistant") {
|
|
597
714
|
scheduleContextUsageRefresh(resolvedSessionKey, 450);
|
|
598
715
|
const bufferedText = providerRunId
|
|
599
716
|
? openClawChatRunIdentities.accumulatedText(opts.gatewayId, providerRunId)
|
|
600
717
|
: "";
|
|
601
718
|
const resolvedText = appendUniqueSuffix(bufferedText, currentText);
|
|
602
|
-
if (resolvedText.trim()) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
719
|
+
if (resolvedText.trim() && !isOpenClawMediaDisplayPlaceholder(resolvedText)) {
|
|
720
|
+
// A provider-level terminal without a reply identity stays
|
|
721
|
+
// open for later text-only delivery terminals. Each such
|
|
722
|
+
// terminal re-reads the current turn history and reconciles
|
|
723
|
+
// the canonical media snapshot deterministically.
|
|
724
|
+
runAfterAssistantSnapshot(runSnapshotKey, Boolean(replyMessageId), async () => {
|
|
725
|
+
let outgoingPayload;
|
|
726
|
+
const directContentBlocks = nonTextContentBlocks(normalizedPayload);
|
|
727
|
+
if (providerRunId && runContext) {
|
|
728
|
+
try {
|
|
729
|
+
const history = await withTimeout(requestChatHistoryFromClawConnect({ sessionKey: resolvedSessionKey, limit: 10 }), CHAT_HISTORY_MEDIA_ENRICHMENT_TIMEOUT_MS, "chat.history media enrichment");
|
|
730
|
+
let outcome = extractHistoryOutcome(history, runContext, currentText);
|
|
731
|
+
// A message-tool delivery can reach the gateway as a
|
|
732
|
+
// text-only terminal before OpenClaw commits its
|
|
733
|
+
// display-media projection. Always inspect the whole
|
|
734
|
+
// matched turn, not just the selected outcome row, so
|
|
735
|
+
// either a direct image or a later delivery row can
|
|
736
|
+
// keep the terminal media-complete.
|
|
737
|
+
let historyContentBlocks = outcome?.kind === "final"
|
|
738
|
+
? hasCanonicalProjectionV3Snapshot(history)
|
|
739
|
+
? mergeNonTextContentBlocks(nonTextContentBlocksFromHistory(outcome.message), extractExplicitParentMediaContent(history, outcome.message))
|
|
740
|
+
: extractHistoryMediaContent(history, runContext)
|
|
741
|
+
: [];
|
|
742
|
+
if (outcome?.kind === "final" && historyContentBlocks.length > 0) {
|
|
743
|
+
// A text terminal is not guaranteed to carry the
|
|
744
|
+
// message-tool identity. Recover it from the stable
|
|
745
|
+
// transcript delivery row before building timeline
|
|
746
|
+
// events; otherwise every sidecar in the run is
|
|
747
|
+
// projected onto assistant-${runId}.
|
|
748
|
+
const historyReplyMessageId = assistantHistoryReplyMessageId(outcome.message, canonicalRunId);
|
|
749
|
+
const mergedContentBlocks = mergeNonTextContentBlocks(directContentBlocks, historyContentBlocks);
|
|
750
|
+
const basePayload = mergeCanonicalChatPayload(normalizedPayload, buildMobileAssistantFinalPayload({
|
|
751
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
752
|
+
text: resolvedText,
|
|
753
|
+
contentBlocks: mergedContentBlocks,
|
|
754
|
+
...chatLineage,
|
|
755
|
+
...((replyMessageId ?? historyReplyMessageId)
|
|
756
|
+
? { messageId: replyMessageId ?? historyReplyMessageId }
|
|
757
|
+
: {}),
|
|
758
|
+
includeTimelineEvents: true,
|
|
759
|
+
...mobileAssistantUsageFromPayload(normalizedPayload),
|
|
760
|
+
}));
|
|
761
|
+
outgoingPayload = buildFinalPayloadFromHistoryOutcome(basePayload, outcome, [{ type: "text", text: outcome.text }, ...mergedContentBlocks]);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
// Media enrichment is opportunistic. The final text event
|
|
766
|
+
// must still reach the client when history is unavailable.
|
|
767
|
+
console.warn(`[relay] terminal media history enrichment skipped: ${String(error)}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (outgoingPayload === undefined) {
|
|
771
|
+
outgoingPayload = providerRunId
|
|
772
|
+
? mergeCanonicalChatPayload(normalizedPayload, buildMobileAssistantFinalPayload({
|
|
773
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
774
|
+
text: resolvedText,
|
|
775
|
+
contentBlocks: directContentBlocks,
|
|
776
|
+
...chatLineage,
|
|
777
|
+
...(replyMessageId ? { messageId: replyMessageId } : {}),
|
|
778
|
+
includeTimelineEvents: true,
|
|
779
|
+
...mobileAssistantUsageFromPayload(normalizedPayload),
|
|
780
|
+
}))
|
|
781
|
+
: withMessageText(normalizedPayload, resolvedText);
|
|
782
|
+
}
|
|
613
783
|
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice, runContext?.promptText);
|
|
614
784
|
if (providerRunId) {
|
|
615
785
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
616
|
-
chatRunContexts.delete(providerRunId);
|
|
617
786
|
}
|
|
618
787
|
}, Boolean(providerRunId));
|
|
619
788
|
return;
|
|
@@ -621,30 +790,39 @@ export async function runRelayManager(opts) {
|
|
|
621
790
|
const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: resolvedSessionKey, limit: 10 });
|
|
622
791
|
const publishHistoryTerminal = () => withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history")
|
|
623
792
|
.then(async (history) => {
|
|
624
|
-
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
outcome = runContext ? extractHistoryOutcome(retryHistory, runContext) : null;
|
|
630
|
-
}
|
|
793
|
+
// The terminal event is the only completion signal. Do not
|
|
794
|
+
// turn transcript visibility into a wall-clock race: a
|
|
795
|
+
// later provider terminal/history page will re-enter this
|
|
796
|
+
// path and reconcile the newly committed source rows.
|
|
797
|
+
const outcome = runContext ? extractHistoryOutcome(history, runContext, currentText) : null;
|
|
631
798
|
if (providerRunId && outcome) {
|
|
632
|
-
chatRunContexts.delete(providerRunId);
|
|
633
799
|
}
|
|
634
800
|
if (outcome?.kind === "final") {
|
|
801
|
+
const historyReplyMessageId = providerRunId
|
|
802
|
+
? assistantHistoryReplyMessageId(outcome.message, canonicalRunId)
|
|
803
|
+
: undefined;
|
|
635
804
|
const basePayload = providerRunId
|
|
636
805
|
? buildMobileAssistantFinalPayload({
|
|
637
806
|
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
638
807
|
text: outcome.text,
|
|
639
|
-
|
|
808
|
+
...chatLineage,
|
|
809
|
+
contentBlocks: runContext
|
|
810
|
+
? hasCanonicalProjectionV3Snapshot(history)
|
|
811
|
+
? mergeNonTextContentBlocks(nonTextContentBlocksFromHistory(outcome.message), extractExplicitParentMediaContent(history, outcome.message))
|
|
812
|
+
: extractHistoryMediaContent(history, runContext)
|
|
813
|
+
: nonTextContentBlocksFromHistory(outcome.message),
|
|
814
|
+
...((replyMessageId ?? historyReplyMessageId)
|
|
815
|
+
? { messageId: replyMessageId ?? historyReplyMessageId }
|
|
816
|
+
: {}),
|
|
640
817
|
includeTimelineEvents: true,
|
|
641
818
|
...mobileAssistantUsageFromPayload(normalizedPayload),
|
|
642
819
|
})
|
|
643
820
|
: normalizedPayload;
|
|
644
821
|
const outgoingPayload = buildFinalPayloadFromHistoryOutcome(basePayload, outcome);
|
|
645
822
|
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice, runContext?.promptText);
|
|
646
|
-
if (providerRunId)
|
|
823
|
+
if (providerRunId) {
|
|
647
824
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
825
|
+
}
|
|
648
826
|
return;
|
|
649
827
|
}
|
|
650
828
|
if (outcome?.kind === "error") {
|
|
@@ -664,21 +842,13 @@ export async function runRelayManager(opts) {
|
|
|
664
842
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
665
843
|
return;
|
|
666
844
|
}
|
|
667
|
-
if (providerRunId && runContext) {
|
|
668
|
-
scheduleChatHistoryFallback(providerRunId, runContext, 0);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
845
|
await publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
672
846
|
})
|
|
673
847
|
.catch((err) => {
|
|
674
848
|
console.error(`[relay] chat.history fetch failed: ${err}`);
|
|
675
|
-
if (providerRunId && runContext) {
|
|
676
|
-
scheduleChatHistoryFallback(providerRunId, runContext, 0);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
849
|
void publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
680
850
|
});
|
|
681
|
-
runAfterAssistantSnapshot(runSnapshotKey,
|
|
851
|
+
runAfterAssistantSnapshot(runSnapshotKey, Boolean(replyMessageId), publishHistoryTerminal, Boolean(providerRunId));
|
|
682
852
|
return;
|
|
683
853
|
}
|
|
684
854
|
if (providerRunId && (state === "error" || state === "failed" || state === "fail" || state === "aborted")) {
|
|
@@ -851,6 +1021,22 @@ export async function runRelayManager(opts) {
|
|
|
851
1021
|
// thinking/tool/text event cannot win the race with this request.
|
|
852
1022
|
await ensureSessionMessagesSubscribed(commandSessionKey);
|
|
853
1023
|
}
|
|
1024
|
+
const sourceRunIdForState = commandMethod === "chat.send"
|
|
1025
|
+
? normalizeOpenClawMobileRunId(paramsRecord?.idempotencyKey)
|
|
1026
|
+
?? normalizeOpenClawMobileRunId(voiceInputRun?.runId)
|
|
1027
|
+
?? requestId
|
|
1028
|
+
: normalizeOpenClawMobileRunId(paramsRecord?.runId)
|
|
1029
|
+
?? normalizeOpenClawMobileRunId(paramsRecord?.idempotencyKey)
|
|
1030
|
+
?? requestId;
|
|
1031
|
+
if ((commandMethod === "chat.send" || commandMethod === "agent") && sourceRunIdForState) {
|
|
1032
|
+
await recordOpenClawActiveRun({
|
|
1033
|
+
gatewayId: opts.gatewayId,
|
|
1034
|
+
sessionKey: typeof paramsRecord?.sessionKey === "string" && paramsRecord.sessionKey.trim()
|
|
1035
|
+
? paramsRecord.sessionKey.trim()
|
|
1036
|
+
: sessionDefaults.mainSessionKey,
|
|
1037
|
+
sourceRunId: sourceRunIdForState,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
854
1040
|
const registerIdentity = (result) => {
|
|
855
1041
|
const resultRecord = result && typeof result === "object" && !Array.isArray(result)
|
|
856
1042
|
? result
|
|
@@ -878,11 +1064,19 @@ export async function runRelayManager(opts) {
|
|
|
878
1064
|
: undefined,
|
|
879
1065
|
};
|
|
880
1066
|
openClawChatRunIdentities.register(identity);
|
|
881
|
-
|
|
1067
|
+
setChatRunContext(identity.providerRunId, identity);
|
|
1068
|
+
void updateOpenClawActiveRunProviderId({
|
|
1069
|
+
gatewayId: opts.gatewayId,
|
|
1070
|
+
sessionKey: identity.sessionKey,
|
|
1071
|
+
sourceRunId: identity.canonicalRunId,
|
|
1072
|
+
providerRunId: identity.providerRunId,
|
|
1073
|
+
}).catch((error) => {
|
|
1074
|
+
console.warn(`[relay] failed to update OpenClaw active run provider id: ${String(error)}`);
|
|
1075
|
+
});
|
|
882
1076
|
return identity;
|
|
883
1077
|
};
|
|
884
1078
|
let identity;
|
|
885
|
-
|
|
1079
|
+
let result = commandMethod === "chat.history"
|
|
886
1080
|
? await requestChatHistoryFromClawConnect(params)
|
|
887
1081
|
: await gatewayClient.request(commandMethod, params, {
|
|
888
1082
|
// response 帧内同步登记,保证紧随其后的首个 delta 已能解析 canonical 身份。
|
|
@@ -891,6 +1085,9 @@ export async function runRelayManager(opts) {
|
|
|
891
1085
|
},
|
|
892
1086
|
});
|
|
893
1087
|
identity ??= registerIdentity(result);
|
|
1088
|
+
if (commandMethod === "agents.list" && gatewayClient) {
|
|
1089
|
+
result = await enrichAgentsListWithIdentities(result, gatewayClient);
|
|
1090
|
+
}
|
|
894
1091
|
return { params, result, identity };
|
|
895
1092
|
};
|
|
896
1093
|
const commandExecution = chatSendIdempotencyRequest
|
|
@@ -920,9 +1117,7 @@ export async function runRelayManager(opts) {
|
|
|
920
1117
|
const sessionKey = typeof paramsRecord?.sessionKey === "string" && paramsRecord.sessionKey.trim().length > 0
|
|
921
1118
|
? paramsRecord.sessionKey.trim()
|
|
922
1119
|
: sessionDefaults.mainSessionKey;
|
|
923
|
-
|
|
924
|
-
scheduleChatHistoryFallback(providerRunId, identity);
|
|
925
|
-
}
|
|
1120
|
+
ensureSourceCommitWatcher(sessionKey);
|
|
926
1121
|
if (commandMethod === "chat.send" && chatSendDedupeRequest) {
|
|
927
1122
|
chatSendDedupe.register(chatSendDedupeRequest, providerRunId);
|
|
928
1123
|
}
|
|
@@ -974,10 +1169,9 @@ export async function runRelayManager(opts) {
|
|
|
974
1169
|
opts.onDisconnected?.();
|
|
975
1170
|
gatewayClient?.stop();
|
|
976
1171
|
gatewayClient = null;
|
|
977
|
-
for (const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
chatFallbacks.clear();
|
|
1172
|
+
for (const watcher of sourceCommitWatchers.values())
|
|
1173
|
+
watcher.close();
|
|
1174
|
+
sourceCommitWatchers.clear();
|
|
981
1175
|
chatSendDedupe.clearAll();
|
|
982
1176
|
for (const timer of contextUsageRefreshes.values()) {
|
|
983
1177
|
clearTimeout(timer);
|
|
@@ -1006,6 +1200,10 @@ export async function runRelayManager(opts) {
|
|
|
1006
1200
|
upper.startsWith("HEARTBEAT_OK") ||
|
|
1007
1201
|
upper.startsWith("HEARTBEAT OK"));
|
|
1008
1202
|
}
|
|
1203
|
+
function isOpenClawMediaDisplayPlaceholder(text) {
|
|
1204
|
+
const normalized = text?.replace(/\r/g, "").trim() ?? "";
|
|
1205
|
+
return /^(?:Media reply could not be displayed\.\s*)+$/i.test(normalized);
|
|
1206
|
+
}
|
|
1009
1207
|
function hasCanonicalTerminalTimeline(payload) {
|
|
1010
1208
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
1011
1209
|
return false;
|