clawconnect-agent 0.2.6 → 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 +1 -0
- package/dist/core/relay/file-upload.js +3 -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 +19 -2
- package/dist/openclaw/relay/assistant-media-sidecar.js +131 -15
- 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 +565 -51
- 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 +30 -3
- package/dist/openclaw/relay/outgoing-media-relay.js +407 -118
- 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 +323 -135
- 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,73 +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 || isOpenClawMediaDisplayPlaceholder(outcome.kind === "final" ? outcome.text : ""))
|
|
163
|
-
&& attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
|
|
164
|
-
scheduleChatHistoryFallback(providerRunId, context, attempt + 1);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (!outcome) {
|
|
168
|
-
chatFallbacks.delete(providerRunId);
|
|
169
|
-
chatRunContexts.delete(providerRunId);
|
|
170
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
clearChatFallback(providerRunId);
|
|
174
|
-
chatRunContexts.delete(providerRunId);
|
|
175
|
-
if (outcome.kind === "final") {
|
|
176
|
-
openClawChatRunIdentities.markTerminal(opts.gatewayId, providerRunId);
|
|
177
|
-
const basePayload = buildMobileAssistantFinalPayload({
|
|
178
|
-
run: { runId: context.canonicalRunId, sessionKey: context.sessionKey },
|
|
179
|
-
text: outcome.text,
|
|
180
|
-
includeTimelineEvents: true,
|
|
181
|
-
});
|
|
182
|
-
await publishAndSendGatewayEvent("chat", buildFinalPayloadFromHistoryOutcome(basePayload, outcome), true, context.promptText);
|
|
183
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
openClawChatRunIdentities.markTerminal(opts.gatewayId, providerRunId);
|
|
187
|
-
await publishAndSendGatewayEvent("chat", buildMobileAssistantErrorPayload({
|
|
188
|
-
run: { runId: context.canonicalRunId, sessionKey: context.sessionKey },
|
|
189
|
-
errorMessage: outcome.errorMessage,
|
|
190
|
-
includeTimelineEvents: true,
|
|
191
|
-
}), true);
|
|
192
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
193
|
-
})
|
|
194
|
-
.catch((err) => {
|
|
195
|
-
if (attempt < CHAT_HISTORY_FALLBACK_MAX_ATTEMPTS) {
|
|
196
|
-
scheduleChatHistoryFallback(providerRunId, context, attempt + 1);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
console.warn(`[relay] chat history fallback failed runId=${providerRunId}: ${String(err)}`);
|
|
200
|
-
chatFallbacks.delete(providerRunId);
|
|
201
|
-
chatRunContexts.delete(providerRunId);
|
|
202
|
-
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
203
|
-
});
|
|
204
|
-
}, attempt === 0 ? CHAT_HISTORY_FALLBACK_INITIAL_DELAY_MS : CHAT_HISTORY_FALLBACK_RETRY_DELAY_MS);
|
|
205
|
-
timer.unref?.();
|
|
206
|
-
chatFallbacks.set(providerRunId, timer);
|
|
169
|
+
return normalized || rawSessionKey.trim();
|
|
207
170
|
};
|
|
208
171
|
const ensureSessionMessagesSubscribed = async (sessionKey) => {
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
172
|
+
const key = explicitOpenClawSessionKey(sessionKey);
|
|
173
|
+
if (!key) {
|
|
211
174
|
throw new Error("cannot subscribe to an empty OpenClaw session key");
|
|
212
175
|
}
|
|
213
|
-
const key = normalizedSessionKey.trim();
|
|
214
176
|
if (subscribedSessionMessageKeys.has(key)) {
|
|
215
177
|
return;
|
|
216
178
|
}
|
|
@@ -249,6 +211,7 @@ export async function runRelayManager(opts) {
|
|
|
249
211
|
sessionDefaults = nextDefaults;
|
|
250
212
|
}
|
|
251
213
|
await ensureSessionMessagesSubscribed(sessionDefaults.mainSessionKey);
|
|
214
|
+
ensureSourceCommitWatcher(sessionDefaults.mainSessionKey);
|
|
252
215
|
const sessionsPayload = await gatewayClient.request("sessions.list", {
|
|
253
216
|
limit: 100,
|
|
254
217
|
includeGlobal: true,
|
|
@@ -264,6 +227,11 @@ export async function runRelayManager(opts) {
|
|
|
264
227
|
: undefined;
|
|
265
228
|
if (typeof sessionKey !== "string" || !sessionKey.trim())
|
|
266
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);
|
|
267
235
|
const snapshot = contextUsageSnapshotFromSessionsList(sessionsPayload, sessionKey, sessionDefaults);
|
|
268
236
|
if (!snapshot)
|
|
269
237
|
continue;
|
|
@@ -336,6 +304,7 @@ export async function runRelayManager(opts) {
|
|
|
336
304
|
cache: outgoingMediaUploadCache,
|
|
337
305
|
userMessage,
|
|
338
306
|
waitForOutgoingMediaRecord: true,
|
|
307
|
+
sessionDefaults,
|
|
339
308
|
});
|
|
340
309
|
}
|
|
341
310
|
catch (error) {
|
|
@@ -371,14 +340,14 @@ export async function runRelayManager(opts) {
|
|
|
371
340
|
console.warn(`[relay] assistant snapshot publish failed key=${key}: ${String(error)}`);
|
|
372
341
|
},
|
|
373
342
|
});
|
|
374
|
-
const assistantSnapshotKey = (runId, sessionKey) => {
|
|
343
|
+
const assistantSnapshotKey = (runId, sessionKey, messageId) => {
|
|
375
344
|
if (!runId?.trim() || !sessionKey?.trim()) {
|
|
376
345
|
return undefined;
|
|
377
346
|
}
|
|
378
347
|
return buildAssistantStreamSnapshotKey({
|
|
379
348
|
sessionKey,
|
|
380
349
|
runId,
|
|
381
|
-
messageId: `assistant-${runId}
|
|
350
|
+
...(messageId ? { messageId } : { messageId: `assistant-${runId}` }),
|
|
382
351
|
});
|
|
383
352
|
};
|
|
384
353
|
const runAfterAssistantSnapshot = (key, close, operation, releaseClosed = false) => {
|
|
@@ -419,7 +388,7 @@ export async function runRelayManager(opts) {
|
|
|
419
388
|
const sessionKey = typeof record.sessionKey === "string" && record.sessionKey.trim()
|
|
420
389
|
? record.sessionKey.trim()
|
|
421
390
|
: runContext?.sessionKey;
|
|
422
|
-
return assistantSnapshotKey(runId, sessionKey);
|
|
391
|
+
return assistantSnapshotKey(runId, sessionKey, runId ? assistantReplyMessageId(payload, runId) : undefined);
|
|
423
392
|
};
|
|
424
393
|
async function relayOutgoingMediaForResponse(method, payload) {
|
|
425
394
|
if (method !== "chat.history") {
|
|
@@ -431,12 +400,23 @@ export async function runRelayManager(opts) {
|
|
|
431
400
|
gatewayId: opts.gatewayId,
|
|
432
401
|
senderDisplayName: "OpenClaw",
|
|
433
402
|
cache: outgoingMediaUploadCache,
|
|
403
|
+
sessionDefaults,
|
|
434
404
|
});
|
|
435
405
|
}
|
|
436
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 };
|
|
437
417
|
try {
|
|
438
|
-
await chatSendDedupe.dedupePendingForSession(
|
|
439
|
-
const transcriptHistory = await readOpenClawTranscriptChatHistory(
|
|
418
|
+
await chatSendDedupe.dedupePendingForSession(projectionParams);
|
|
419
|
+
const transcriptHistory = await readOpenClawTranscriptChatHistory(projectionParams, sessionDefaults);
|
|
440
420
|
if (transcriptHistory) {
|
|
441
421
|
return transcriptHistory;
|
|
442
422
|
}
|
|
@@ -445,7 +425,7 @@ export async function runRelayManager(opts) {
|
|
|
445
425
|
console.warn(`[relay] transcript chat.history unavailable: ${String(err)}`);
|
|
446
426
|
}
|
|
447
427
|
if (hasHistoryCursor(params)) {
|
|
448
|
-
return buildEmptyHistoryPage(
|
|
428
|
+
return buildEmptyHistoryPage(projectionParams, sessionDefaults);
|
|
449
429
|
}
|
|
450
430
|
if (!gatewayClient) {
|
|
451
431
|
throw new Error("gateway not connected");
|
|
@@ -454,8 +434,56 @@ export async function runRelayManager(opts) {
|
|
|
454
434
|
const history = await gatewayClient.request("chat.history", gatewayHistoryParams);
|
|
455
435
|
return canonicalizeOpenClawGatewayHistoryResponse(history, {
|
|
456
436
|
sessionKey: String(gatewayHistoryParams.sessionKey),
|
|
437
|
+
projectionVersion: 3,
|
|
438
|
+
projectionGatewayId: opts.gatewayId,
|
|
439
|
+
sessionDefaults,
|
|
457
440
|
});
|
|
458
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
|
+
};
|
|
459
487
|
relayWs.on("open", () => {
|
|
460
488
|
console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
|
|
461
489
|
opts.onConnected?.();
|
|
@@ -497,8 +525,28 @@ export async function runRelayManager(opts) {
|
|
|
497
525
|
});
|
|
498
526
|
},
|
|
499
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
|
+
}
|
|
500
548
|
const normalizedPayload = event === "chat"
|
|
501
|
-
? canonicalizeOpenClawAssistantMediaSidecarPayload(normalizeChatEventPayload(payload))
|
|
549
|
+
? materializeOpenClawDisplayContentPayload(canonicalizeOpenClawAssistantMediaSidecarPayload(normalizeChatEventPayload(payload)))
|
|
502
550
|
: payload;
|
|
503
551
|
const shouldPublishOffice = event === "chat" || event === "agent" || event === "context_usage";
|
|
504
552
|
if (event === "chat") {
|
|
@@ -509,10 +557,16 @@ export async function runRelayManager(opts) {
|
|
|
509
557
|
const isTerminalState = state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted";
|
|
510
558
|
const sessionKey = typeof p?.sessionKey === "string" && p.sessionKey.trim().length > 0 ? p.sessionKey.trim() : undefined;
|
|
511
559
|
const explicitCanonicalRunId = resolveExplicitMobileRunIdFromChatPayload(normalizedPayload);
|
|
560
|
+
const currentText = extractChatText(normalizedPayload);
|
|
561
|
+
const role = extractChatRole(normalizedPayload);
|
|
512
562
|
let runContext = providerRunId
|
|
513
563
|
? chatRunContexts.get(providerRunId) ?? openClawChatRunIdentities.resolve(opts.gatewayId, providerRunId)
|
|
514
564
|
: undefined;
|
|
515
|
-
|
|
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") {
|
|
516
570
|
runContext = {
|
|
517
571
|
gatewayId: opts.gatewayId,
|
|
518
572
|
providerRunId,
|
|
@@ -520,19 +574,37 @@ export async function runRelayManager(opts) {
|
|
|
520
574
|
sessionKey,
|
|
521
575
|
};
|
|
522
576
|
openClawChatRunIdentities.ensure(runContext);
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
if (providerRunId && isStreamingState && openClawChatRunIdentities.isTerminal(opts.gatewayId, providerRunId)) {
|
|
526
|
-
return;
|
|
577
|
+
setChatRunContext(providerRunId, runContext);
|
|
527
578
|
}
|
|
528
579
|
if (providerRunId && isTerminalState) {
|
|
529
580
|
openClawChatRunIdentities.markTerminal(opts.gatewayId, providerRunId);
|
|
530
581
|
}
|
|
531
582
|
const canonicalRunId = runContext?.canonicalRunId ?? explicitCanonicalRunId ?? providerRunId;
|
|
532
583
|
const resolvedSessionKey = sessionKey ?? runContext?.sessionKey;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
+
}
|
|
536
608
|
if (isOpenClawHeartbeatText(currentText) || (runContext?.promptText && isOpenClawHeartbeatText(runContext.promptText))) {
|
|
537
609
|
if (providerRunId && (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted")) {
|
|
538
610
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
@@ -543,6 +615,51 @@ export async function runRelayManager(opts) {
|
|
|
543
615
|
}
|
|
544
616
|
return;
|
|
545
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
|
+
}
|
|
546
663
|
let realtimePayload = providerRunId && canonicalRunId && canonicalRunId !== providerRunId
|
|
547
664
|
? {
|
|
548
665
|
...normalizedPayload,
|
|
@@ -554,9 +671,6 @@ export async function runRelayManager(opts) {
|
|
|
554
671
|
if (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted") {
|
|
555
672
|
chatSendDedupe.scheduleForRun(providerRunId, 100);
|
|
556
673
|
}
|
|
557
|
-
if (role === "assistant" && (state === "delta" || state === "final" || state === "error" || state === "failed" || state === "fail")) {
|
|
558
|
-
clearChatFallback(providerRunId);
|
|
559
|
-
}
|
|
560
674
|
if (state === "delta" || state === "streaming" || state === "in_progress") {
|
|
561
675
|
const previousText = openClawChatRunIdentities.accumulatedText(opts.gatewayId, providerRunId);
|
|
562
676
|
const bufferedText = appendUniqueSuffix(previousText, currentText);
|
|
@@ -568,6 +682,7 @@ export async function runRelayManager(opts) {
|
|
|
568
682
|
seq: resolveChatPayloadSeq(normalizedPayload),
|
|
569
683
|
timestampMs: resolveChatPayloadTimestamp(normalizedPayload),
|
|
570
684
|
delta: bufferedText,
|
|
685
|
+
...chatLineage,
|
|
571
686
|
includeTimelineEvents: true,
|
|
572
687
|
}))
|
|
573
688
|
: withMessageText(normalizedPayload, bufferedText);
|
|
@@ -595,27 +710,79 @@ export async function runRelayManager(opts) {
|
|
|
595
710
|
});
|
|
596
711
|
return;
|
|
597
712
|
}
|
|
598
|
-
if (state === "final" && resolvedSessionKey) {
|
|
713
|
+
if (state === "final" && resolvedSessionKey && role === "assistant") {
|
|
599
714
|
scheduleContextUsageRefresh(resolvedSessionKey, 450);
|
|
600
715
|
const bufferedText = providerRunId
|
|
601
716
|
? openClawChatRunIdentities.accumulatedText(opts.gatewayId, providerRunId)
|
|
602
717
|
: "";
|
|
603
718
|
const resolvedText = appendUniqueSuffix(bufferedText, currentText);
|
|
604
719
|
if (resolvedText.trim() && !isOpenClawMediaDisplayPlaceholder(resolvedText)) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
+
}
|
|
615
783
|
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice, runContext?.promptText);
|
|
616
784
|
if (providerRunId) {
|
|
617
785
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
618
|
-
chatRunContexts.delete(providerRunId);
|
|
619
786
|
}
|
|
620
787
|
}, Boolean(providerRunId));
|
|
621
788
|
return;
|
|
@@ -623,34 +790,39 @@ export async function runRelayManager(opts) {
|
|
|
623
790
|
const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: resolvedSessionKey, limit: 10 });
|
|
624
791
|
const publishHistoryTerminal = () => withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history")
|
|
625
792
|
.then(async (history) => {
|
|
626
|
-
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
outcome = runContext ? extractHistoryOutcome(retryHistory, runContext) : null;
|
|
632
|
-
}
|
|
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;
|
|
633
798
|
if (providerRunId && outcome) {
|
|
634
|
-
chatRunContexts.delete(providerRunId);
|
|
635
799
|
}
|
|
636
800
|
if (outcome?.kind === "final") {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
801
|
+
const historyReplyMessageId = providerRunId
|
|
802
|
+
? assistantHistoryReplyMessageId(outcome.message, canonicalRunId)
|
|
803
|
+
: undefined;
|
|
641
804
|
const basePayload = providerRunId
|
|
642
805
|
? buildMobileAssistantFinalPayload({
|
|
643
806
|
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
644
807
|
text: outcome.text,
|
|
645
|
-
|
|
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
|
+
: {}),
|
|
646
817
|
includeTimelineEvents: true,
|
|
647
818
|
...mobileAssistantUsageFromPayload(normalizedPayload),
|
|
648
819
|
})
|
|
649
820
|
: normalizedPayload;
|
|
650
821
|
const outgoingPayload = buildFinalPayloadFromHistoryOutcome(basePayload, outcome);
|
|
651
822
|
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice, runContext?.promptText);
|
|
652
|
-
if (providerRunId)
|
|
823
|
+
if (providerRunId) {
|
|
653
824
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
825
|
+
}
|
|
654
826
|
return;
|
|
655
827
|
}
|
|
656
828
|
if (outcome?.kind === "error") {
|
|
@@ -670,21 +842,13 @@ export async function runRelayManager(opts) {
|
|
|
670
842
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
671
843
|
return;
|
|
672
844
|
}
|
|
673
|
-
if (providerRunId && runContext) {
|
|
674
|
-
scheduleChatHistoryFallback(providerRunId, runContext, 0);
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
845
|
await publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
678
846
|
})
|
|
679
847
|
.catch((err) => {
|
|
680
848
|
console.error(`[relay] chat.history fetch failed: ${err}`);
|
|
681
|
-
if (providerRunId && runContext) {
|
|
682
|
-
scheduleChatHistoryFallback(providerRunId, runContext, 0);
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
849
|
void publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
686
850
|
});
|
|
687
|
-
runAfterAssistantSnapshot(runSnapshotKey,
|
|
851
|
+
runAfterAssistantSnapshot(runSnapshotKey, Boolean(replyMessageId), publishHistoryTerminal, Boolean(providerRunId));
|
|
688
852
|
return;
|
|
689
853
|
}
|
|
690
854
|
if (providerRunId && (state === "error" || state === "failed" || state === "fail" || state === "aborted")) {
|
|
@@ -857,6 +1021,22 @@ export async function runRelayManager(opts) {
|
|
|
857
1021
|
// thinking/tool/text event cannot win the race with this request.
|
|
858
1022
|
await ensureSessionMessagesSubscribed(commandSessionKey);
|
|
859
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
|
+
}
|
|
860
1040
|
const registerIdentity = (result) => {
|
|
861
1041
|
const resultRecord = result && typeof result === "object" && !Array.isArray(result)
|
|
862
1042
|
? result
|
|
@@ -884,11 +1064,19 @@ export async function runRelayManager(opts) {
|
|
|
884
1064
|
: undefined,
|
|
885
1065
|
};
|
|
886
1066
|
openClawChatRunIdentities.register(identity);
|
|
887
|
-
|
|
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
|
+
});
|
|
888
1076
|
return identity;
|
|
889
1077
|
};
|
|
890
1078
|
let identity;
|
|
891
|
-
|
|
1079
|
+
let result = commandMethod === "chat.history"
|
|
892
1080
|
? await requestChatHistoryFromClawConnect(params)
|
|
893
1081
|
: await gatewayClient.request(commandMethod, params, {
|
|
894
1082
|
// response 帧内同步登记,保证紧随其后的首个 delta 已能解析 canonical 身份。
|
|
@@ -897,6 +1085,9 @@ export async function runRelayManager(opts) {
|
|
|
897
1085
|
},
|
|
898
1086
|
});
|
|
899
1087
|
identity ??= registerIdentity(result);
|
|
1088
|
+
if (commandMethod === "agents.list" && gatewayClient) {
|
|
1089
|
+
result = await enrichAgentsListWithIdentities(result, gatewayClient);
|
|
1090
|
+
}
|
|
900
1091
|
return { params, result, identity };
|
|
901
1092
|
};
|
|
902
1093
|
const commandExecution = chatSendIdempotencyRequest
|
|
@@ -926,9 +1117,7 @@ export async function runRelayManager(opts) {
|
|
|
926
1117
|
const sessionKey = typeof paramsRecord?.sessionKey === "string" && paramsRecord.sessionKey.trim().length > 0
|
|
927
1118
|
? paramsRecord.sessionKey.trim()
|
|
928
1119
|
: sessionDefaults.mainSessionKey;
|
|
929
|
-
|
|
930
|
-
scheduleChatHistoryFallback(providerRunId, identity);
|
|
931
|
-
}
|
|
1120
|
+
ensureSourceCommitWatcher(sessionKey);
|
|
932
1121
|
if (commandMethod === "chat.send" && chatSendDedupeRequest) {
|
|
933
1122
|
chatSendDedupe.register(chatSendDedupeRequest, providerRunId);
|
|
934
1123
|
}
|
|
@@ -980,10 +1169,9 @@ export async function runRelayManager(opts) {
|
|
|
980
1169
|
opts.onDisconnected?.();
|
|
981
1170
|
gatewayClient?.stop();
|
|
982
1171
|
gatewayClient = null;
|
|
983
|
-
for (const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
chatFallbacks.clear();
|
|
1172
|
+
for (const watcher of sourceCommitWatchers.values())
|
|
1173
|
+
watcher.close();
|
|
1174
|
+
sourceCommitWatchers.clear();
|
|
987
1175
|
chatSendDedupe.clearAll();
|
|
988
1176
|
for (const timer of contextUsageRefreshes.values()) {
|
|
989
1177
|
clearTimeout(timer);
|