clawconnect-agent 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/relay-server-connection.d.ts +1 -0
- package/dist/core/relay/relay-server-connection.js +17 -0
- package/dist/core/relay/relay-server-connection.js.map +1 -1
- package/dist/core/relay/source-commit.d.ts +42 -0
- package/dist/core/relay/source-commit.js +127 -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 +23 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js +65 -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 +328 -136
- 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
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
import { OpenClawGatewayClient } from "./gateway-client.js";
|
|
2
|
-
import { bindRelayAbortSignal, buildRelayUrl, createRelayWebSocket, disconnectRelaySocketForRecovery, parseRelayFrame, sendRelayJson, sendRelayJsonWithWriteConfirmation, shouldRetryRelayClose, } from "../core/relay/relay-server-connection.js";
|
|
2
|
+
import { bindRelayAbortSignal, buildRelayUrl, createRelayWebSocket, disconnectRelaySocketForRecovery, parseRelayFrame, sendRelayJson, sendRelayJsonWithWriteConfirmation, shouldRetryRelayClose, waitForRelaySocketDrain, } from "../core/relay/relay-server-connection.js";
|
|
3
3
|
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, "latest");
|
|
252
215
|
const sessionsPayload = await gatewayClient.request("sessions.list", {
|
|
253
216
|
limit: 100,
|
|
254
217
|
includeGlobal: true,
|
|
@@ -264,6 +227,13 @@ export async function runRelayManager(opts) {
|
|
|
264
227
|
: undefined;
|
|
265
228
|
if (typeof sessionKey !== "string" || !sessionKey.trim())
|
|
266
229
|
continue;
|
|
230
|
+
if (/(?:^|:)dashboard:/i.test(sessionKey))
|
|
231
|
+
continue;
|
|
232
|
+
// Cron jobs can target an agent session other than the default one.
|
|
233
|
+
// Subscribe each known session so their finished replies travel through
|
|
234
|
+
// the same ClawConnect → Relay realtime path as an interactive chat.
|
|
235
|
+
await ensureSessionMessagesSubscribed(sessionKey);
|
|
236
|
+
ensureSourceCommitWatcher(sessionKey, "latest");
|
|
267
237
|
const snapshot = contextUsageSnapshotFromSessionsList(sessionsPayload, sessionKey, sessionDefaults);
|
|
268
238
|
if (!snapshot)
|
|
269
239
|
continue;
|
|
@@ -336,6 +306,7 @@ export async function runRelayManager(opts) {
|
|
|
336
306
|
cache: outgoingMediaUploadCache,
|
|
337
307
|
userMessage,
|
|
338
308
|
waitForOutgoingMediaRecord: true,
|
|
309
|
+
sessionDefaults,
|
|
339
310
|
});
|
|
340
311
|
}
|
|
341
312
|
catch (error) {
|
|
@@ -371,14 +342,14 @@ export async function runRelayManager(opts) {
|
|
|
371
342
|
console.warn(`[relay] assistant snapshot publish failed key=${key}: ${String(error)}`);
|
|
372
343
|
},
|
|
373
344
|
});
|
|
374
|
-
const assistantSnapshotKey = (runId, sessionKey) => {
|
|
345
|
+
const assistantSnapshotKey = (runId, sessionKey, messageId) => {
|
|
375
346
|
if (!runId?.trim() || !sessionKey?.trim()) {
|
|
376
347
|
return undefined;
|
|
377
348
|
}
|
|
378
349
|
return buildAssistantStreamSnapshotKey({
|
|
379
350
|
sessionKey,
|
|
380
351
|
runId,
|
|
381
|
-
messageId: `assistant-${runId}
|
|
352
|
+
...(messageId ? { messageId } : { messageId: `assistant-${runId}` }),
|
|
382
353
|
});
|
|
383
354
|
};
|
|
384
355
|
const runAfterAssistantSnapshot = (key, close, operation, releaseClosed = false) => {
|
|
@@ -419,7 +390,7 @@ export async function runRelayManager(opts) {
|
|
|
419
390
|
const sessionKey = typeof record.sessionKey === "string" && record.sessionKey.trim()
|
|
420
391
|
? record.sessionKey.trim()
|
|
421
392
|
: runContext?.sessionKey;
|
|
422
|
-
return assistantSnapshotKey(runId, sessionKey);
|
|
393
|
+
return assistantSnapshotKey(runId, sessionKey, runId ? assistantReplyMessageId(payload, runId) : undefined);
|
|
423
394
|
};
|
|
424
395
|
async function relayOutgoingMediaForResponse(method, payload) {
|
|
425
396
|
if (method !== "chat.history") {
|
|
@@ -431,12 +402,23 @@ export async function runRelayManager(opts) {
|
|
|
431
402
|
gatewayId: opts.gatewayId,
|
|
432
403
|
senderDisplayName: "OpenClaw",
|
|
433
404
|
cache: outgoingMediaUploadCache,
|
|
405
|
+
sessionDefaults,
|
|
434
406
|
});
|
|
435
407
|
}
|
|
436
408
|
async function requestChatHistoryFromClawConnect(params) {
|
|
409
|
+
// Relay-owned history reads opt into the v3 projection contract. The
|
|
410
|
+
// public legacy helper remains v2-compatible for older callers during
|
|
411
|
+
// rollout; every confirmed row consumed by this manager is v3.
|
|
412
|
+
const projectionParams = params && typeof params === "object" && !Array.isArray(params)
|
|
413
|
+
? {
|
|
414
|
+
...params,
|
|
415
|
+
projectionVersion: 3,
|
|
416
|
+
projectionGatewayId: opts.gatewayId,
|
|
417
|
+
}
|
|
418
|
+
: { projectionVersion: 3, projectionGatewayId: opts.gatewayId };
|
|
437
419
|
try {
|
|
438
|
-
await chatSendDedupe.dedupePendingForSession(
|
|
439
|
-
const transcriptHistory = await readOpenClawTranscriptChatHistory(
|
|
420
|
+
await chatSendDedupe.dedupePendingForSession(projectionParams);
|
|
421
|
+
const transcriptHistory = await readOpenClawTranscriptChatHistory(projectionParams, sessionDefaults);
|
|
440
422
|
if (transcriptHistory) {
|
|
441
423
|
return transcriptHistory;
|
|
442
424
|
}
|
|
@@ -445,7 +427,7 @@ export async function runRelayManager(opts) {
|
|
|
445
427
|
console.warn(`[relay] transcript chat.history unavailable: ${String(err)}`);
|
|
446
428
|
}
|
|
447
429
|
if (hasHistoryCursor(params)) {
|
|
448
|
-
return buildEmptyHistoryPage(
|
|
430
|
+
return buildEmptyHistoryPage(projectionParams, sessionDefaults);
|
|
449
431
|
}
|
|
450
432
|
if (!gatewayClient) {
|
|
451
433
|
throw new Error("gateway not connected");
|
|
@@ -454,8 +436,58 @@ export async function runRelayManager(opts) {
|
|
|
454
436
|
const history = await gatewayClient.request("chat.history", gatewayHistoryParams);
|
|
455
437
|
return canonicalizeOpenClawGatewayHistoryResponse(history, {
|
|
456
438
|
sessionKey: String(gatewayHistoryParams.sessionKey),
|
|
439
|
+
projectionVersion: 3,
|
|
440
|
+
projectionGatewayId: opts.gatewayId,
|
|
441
|
+
sessionDefaults,
|
|
457
442
|
});
|
|
458
443
|
}
|
|
444
|
+
const ensureSourceCommitWatcher = (sessionKey, initialWatermarkMode = "from_zero") => {
|
|
445
|
+
const normalizedSessionKey = explicitOpenClawSessionKey(sessionKey);
|
|
446
|
+
if (!normalizedSessionKey)
|
|
447
|
+
return;
|
|
448
|
+
const key = `${opts.gatewayId}\u0000${normalizedSessionKey}`;
|
|
449
|
+
if (sourceCommitWatchers.has(key))
|
|
450
|
+
return;
|
|
451
|
+
const agentId = openClawAgentIdFromSessionKey(normalizedSessionKey);
|
|
452
|
+
const databasePath = join(resolveOpenClawStateDir(), "agents", agentId, "agent", "openclaw-agent.sqlite");
|
|
453
|
+
const watcher = watchOpenClawSourceCommit({
|
|
454
|
+
databasePath,
|
|
455
|
+
initialWatermarkMode,
|
|
456
|
+
readCursor: () => readOpenClawSourceCommitCursor({
|
|
457
|
+
sessionKey: normalizedSessionKey,
|
|
458
|
+
projectionVersion: 3,
|
|
459
|
+
projectionGatewayId: opts.gatewayId,
|
|
460
|
+
}, sessionDefaults),
|
|
461
|
+
onCommit: async (sourceCommit, previousCommittedThroughSeq) => {
|
|
462
|
+
await projectSourceCommitHistoryPages({
|
|
463
|
+
sourceCommit,
|
|
464
|
+
previousCommittedThroughSeq,
|
|
465
|
+
readPage: (cursorSeq) => withTimeout(requestChatHistoryFromClawConnect({
|
|
466
|
+
sessionKey: normalizedSessionKey,
|
|
467
|
+
limit: 200,
|
|
468
|
+
cursor: `seq:${cursorSeq}`,
|
|
469
|
+
direction: "newer",
|
|
470
|
+
}), CHAT_HISTORY_FETCH_TIMEOUT_MS, "source commit history reconciliation"),
|
|
471
|
+
publish: async ({ sourceCommit: pageSourceCommit, events }) => {
|
|
472
|
+
await waitForRelaySocketDrain(relayWs, 256 * 1024);
|
|
473
|
+
const deliveryResult = await publishAndSendGatewayEvent("chat", {
|
|
474
|
+
state: "source_commit",
|
|
475
|
+
sessionKey: normalizedSessionKey,
|
|
476
|
+
sourceCommit: pageSourceCommit,
|
|
477
|
+
timelineEvents: events,
|
|
478
|
+
}, true, undefined, true);
|
|
479
|
+
if (deliveryResult?.status === "retryable") {
|
|
480
|
+
throw deliveryResult.error;
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
onError: (error) => {
|
|
486
|
+
console.warn(`[relay] source commit reconciliation failed session=${normalizedSessionKey}: ${String(error)}`);
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
sourceCommitWatchers.set(key, watcher);
|
|
490
|
+
};
|
|
459
491
|
relayWs.on("open", () => {
|
|
460
492
|
console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
|
|
461
493
|
opts.onConnected?.();
|
|
@@ -497,8 +529,28 @@ export async function runRelayManager(opts) {
|
|
|
497
529
|
});
|
|
498
530
|
},
|
|
499
531
|
onEvent: (event, payload) => {
|
|
532
|
+
if (event === "session.message") {
|
|
533
|
+
// OpenClaw emits this subscription event after the transcript
|
|
534
|
+
// append has committed. It is the primary wake-up for source
|
|
535
|
+
// reconciliation; SQLite notifications and the low-frequency
|
|
536
|
+
// anti-entropy scan remain recovery paths for dropped events.
|
|
537
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
538
|
+
? payload
|
|
539
|
+
: undefined;
|
|
540
|
+
const rawSessionKey = typeof record?.sessionKey === "string"
|
|
541
|
+
? record.sessionKey.trim()
|
|
542
|
+
: "";
|
|
543
|
+
if (rawSessionKey) {
|
|
544
|
+
const sessionKey = canonicalizeSessionKey(rawSessionKey, sessionDefaults);
|
|
545
|
+
if (typeof sessionKey === "string" && sessionKey.trim()) {
|
|
546
|
+
ensureSourceCommitWatcher(sessionKey, "from_zero");
|
|
547
|
+
sourceCommitWatchers.get(`${opts.gatewayId}\u0000${sessionKey.trim()}`)?.notify();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
500
552
|
const normalizedPayload = event === "chat"
|
|
501
|
-
? canonicalizeOpenClawAssistantMediaSidecarPayload(normalizeChatEventPayload(payload))
|
|
553
|
+
? materializeOpenClawDisplayContentPayload(canonicalizeOpenClawAssistantMediaSidecarPayload(normalizeChatEventPayload(payload)))
|
|
502
554
|
: payload;
|
|
503
555
|
const shouldPublishOffice = event === "chat" || event === "agent" || event === "context_usage";
|
|
504
556
|
if (event === "chat") {
|
|
@@ -509,10 +561,16 @@ export async function runRelayManager(opts) {
|
|
|
509
561
|
const isTerminalState = state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted";
|
|
510
562
|
const sessionKey = typeof p?.sessionKey === "string" && p.sessionKey.trim().length > 0 ? p.sessionKey.trim() : undefined;
|
|
511
563
|
const explicitCanonicalRunId = resolveExplicitMobileRunIdFromChatPayload(normalizedPayload);
|
|
564
|
+
const currentText = extractChatText(normalizedPayload);
|
|
565
|
+
const role = extractChatRole(normalizedPayload);
|
|
512
566
|
let runContext = providerRunId
|
|
513
567
|
? chatRunContexts.get(providerRunId) ?? openClawChatRunIdentities.resolve(opts.gatewayId, providerRunId)
|
|
514
568
|
: undefined;
|
|
515
|
-
|
|
569
|
+
// Event-only registration is safe only after the provider proves
|
|
570
|
+
// assistant provenance. A roleless terminal may contain a user
|
|
571
|
+
// echo or an unrelated provider event and must not create a run
|
|
572
|
+
// context from its text/session fields.
|
|
573
|
+
if (!runContext && providerRunId && sessionKey && role === "assistant") {
|
|
516
574
|
runContext = {
|
|
517
575
|
gatewayId: opts.gatewayId,
|
|
518
576
|
providerRunId,
|
|
@@ -520,19 +578,37 @@ export async function runRelayManager(opts) {
|
|
|
520
578
|
sessionKey,
|
|
521
579
|
};
|
|
522
580
|
openClawChatRunIdentities.ensure(runContext);
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
if (providerRunId && isStreamingState && openClawChatRunIdentities.isTerminal(opts.gatewayId, providerRunId)) {
|
|
526
|
-
return;
|
|
581
|
+
setChatRunContext(providerRunId, runContext);
|
|
527
582
|
}
|
|
528
583
|
if (providerRunId && isTerminalState) {
|
|
529
584
|
openClawChatRunIdentities.markTerminal(opts.gatewayId, providerRunId);
|
|
530
585
|
}
|
|
531
586
|
const canonicalRunId = runContext?.canonicalRunId ?? explicitCanonicalRunId ?? providerRunId;
|
|
532
587
|
const resolvedSessionKey = sessionKey ?? runContext?.sessionKey;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
588
|
+
if (isTerminalState && runContext && canonicalRunId && resolvedSessionKey) {
|
|
589
|
+
void markOpenClawActiveRunTerminal({
|
|
590
|
+
gatewayId: opts.gatewayId,
|
|
591
|
+
sessionKey: resolvedSessionKey,
|
|
592
|
+
sourceRunId: canonicalRunId,
|
|
593
|
+
}).catch((error) => {
|
|
594
|
+
console.warn(`[relay] failed to mark OpenClaw run terminal: ${String(error)}`);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
const replyMessageId = role === "assistant" && canonicalRunId
|
|
598
|
+
? assistantReplyMessageId(normalizedPayload, canonicalRunId)
|
|
599
|
+
: undefined;
|
|
600
|
+
const chatLineage = extractChatLineage(normalizedPayload);
|
|
601
|
+
const runSnapshotKey = assistantSnapshotKey(canonicalRunId, resolvedSessionKey, replyMessageId);
|
|
602
|
+
// OpenClaw's message tool may emit several independent assistant
|
|
603
|
+
// replies after a provider-level terminal snapshot. Keep filtering
|
|
604
|
+
// late stream deltas for the run-level text reply, but let a reply
|
|
605
|
+
// with its own upstream message/tool/media identity through.
|
|
606
|
+
if (providerRunId
|
|
607
|
+
&& isStreamingState
|
|
608
|
+
&& openClawChatRunIdentities.isTerminal(opts.gatewayId, providerRunId)
|
|
609
|
+
&& !replyMessageId) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
536
612
|
if (isOpenClawHeartbeatText(currentText) || (runContext?.promptText && isOpenClawHeartbeatText(runContext.promptText))) {
|
|
537
613
|
if (providerRunId && (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted")) {
|
|
538
614
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
@@ -543,6 +619,51 @@ export async function runRelayManager(opts) {
|
|
|
543
619
|
}
|
|
544
620
|
return;
|
|
545
621
|
}
|
|
622
|
+
// A roleless error/abort is still a terminal lifecycle signal when
|
|
623
|
+
// it belongs to a previously registered provider run. Its identity
|
|
624
|
+
// comes from that run context; never infer it from the payload
|
|
625
|
+
// text. A roleless empty final is lifecycle-only and leaves source
|
|
626
|
+
// rows to the transcript/source-commit watcher. Text-bearing
|
|
627
|
+
// roleless deltas/finals remain rejected user/source echoes.
|
|
628
|
+
if (role !== "assistant") {
|
|
629
|
+
if (!runContext || !providerRunId || !canonicalRunId || !resolvedSessionKey) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (state === "error" || state === "failed" || state === "fail" || state === "aborted") {
|
|
633
|
+
chatSendDedupe.scheduleForRun(providerRunId, 100);
|
|
634
|
+
chatRunContexts.delete(providerRunId);
|
|
635
|
+
runAfterAssistantSnapshot(runSnapshotKey, true, async () => {
|
|
636
|
+
await publishAndSendGatewayEvent(event, mergeCanonicalChatPayload(normalizedPayload, buildMobileAssistantErrorPayload({
|
|
637
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
638
|
+
errorMessage: extractChatErrorMessage(normalizedPayload),
|
|
639
|
+
includeTimelineEvents: true,
|
|
640
|
+
})), shouldPublishOffice, runContext?.promptText);
|
|
641
|
+
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
642
|
+
}, true);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (state === "final" && !currentText) {
|
|
646
|
+
chatRunContexts.delete(providerRunId);
|
|
647
|
+
const lifecyclePayload = normalizedPayload && typeof normalizedPayload === "object" && !Array.isArray(normalizedPayload)
|
|
648
|
+
? (() => {
|
|
649
|
+
const payload = { ...normalizedPayload };
|
|
650
|
+
delete payload.message;
|
|
651
|
+
delete payload.content;
|
|
652
|
+
delete payload.text;
|
|
653
|
+
delete payload.delta;
|
|
654
|
+
return payload;
|
|
655
|
+
})()
|
|
656
|
+
: normalizedPayload;
|
|
657
|
+
runAfterAssistantSnapshot(runSnapshotKey, true, async () => {
|
|
658
|
+
await publishAndSendGatewayEvent(event, mergeCanonicalChatPayload(lifecyclePayload, buildMobileAssistantCompletedPayload({
|
|
659
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
660
|
+
includeTimelineEvents: true,
|
|
661
|
+
})), shouldPublishOffice, runContext?.promptText);
|
|
662
|
+
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
663
|
+
}, true);
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
546
667
|
let realtimePayload = providerRunId && canonicalRunId && canonicalRunId !== providerRunId
|
|
547
668
|
? {
|
|
548
669
|
...normalizedPayload,
|
|
@@ -554,9 +675,6 @@ export async function runRelayManager(opts) {
|
|
|
554
675
|
if (state === "final" || state === "error" || state === "failed" || state === "fail" || state === "aborted") {
|
|
555
676
|
chatSendDedupe.scheduleForRun(providerRunId, 100);
|
|
556
677
|
}
|
|
557
|
-
if (role === "assistant" && (state === "delta" || state === "final" || state === "error" || state === "failed" || state === "fail")) {
|
|
558
|
-
clearChatFallback(providerRunId);
|
|
559
|
-
}
|
|
560
678
|
if (state === "delta" || state === "streaming" || state === "in_progress") {
|
|
561
679
|
const previousText = openClawChatRunIdentities.accumulatedText(opts.gatewayId, providerRunId);
|
|
562
680
|
const bufferedText = appendUniqueSuffix(previousText, currentText);
|
|
@@ -568,6 +686,7 @@ export async function runRelayManager(opts) {
|
|
|
568
686
|
seq: resolveChatPayloadSeq(normalizedPayload),
|
|
569
687
|
timestampMs: resolveChatPayloadTimestamp(normalizedPayload),
|
|
570
688
|
delta: bufferedText,
|
|
689
|
+
...chatLineage,
|
|
571
690
|
includeTimelineEvents: true,
|
|
572
691
|
}))
|
|
573
692
|
: withMessageText(normalizedPayload, bufferedText);
|
|
@@ -595,27 +714,79 @@ export async function runRelayManager(opts) {
|
|
|
595
714
|
});
|
|
596
715
|
return;
|
|
597
716
|
}
|
|
598
|
-
if (state === "final" && resolvedSessionKey) {
|
|
717
|
+
if (state === "final" && resolvedSessionKey && role === "assistant") {
|
|
599
718
|
scheduleContextUsageRefresh(resolvedSessionKey, 450);
|
|
600
719
|
const bufferedText = providerRunId
|
|
601
720
|
? openClawChatRunIdentities.accumulatedText(opts.gatewayId, providerRunId)
|
|
602
721
|
: "";
|
|
603
722
|
const resolvedText = appendUniqueSuffix(bufferedText, currentText);
|
|
604
723
|
if (resolvedText.trim() && !isOpenClawMediaDisplayPlaceholder(resolvedText)) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
724
|
+
// A provider-level terminal without a reply identity stays
|
|
725
|
+
// open for later text-only delivery terminals. Each such
|
|
726
|
+
// terminal re-reads the current turn history and reconciles
|
|
727
|
+
// the canonical media snapshot deterministically.
|
|
728
|
+
runAfterAssistantSnapshot(runSnapshotKey, Boolean(replyMessageId), async () => {
|
|
729
|
+
let outgoingPayload;
|
|
730
|
+
const directContentBlocks = nonTextContentBlocks(normalizedPayload);
|
|
731
|
+
if (providerRunId && runContext) {
|
|
732
|
+
try {
|
|
733
|
+
const history = await withTimeout(requestChatHistoryFromClawConnect({ sessionKey: resolvedSessionKey, limit: 10 }), CHAT_HISTORY_MEDIA_ENRICHMENT_TIMEOUT_MS, "chat.history media enrichment");
|
|
734
|
+
let outcome = extractHistoryOutcome(history, runContext, currentText);
|
|
735
|
+
// A message-tool delivery can reach the gateway as a
|
|
736
|
+
// text-only terminal before OpenClaw commits its
|
|
737
|
+
// display-media projection. Always inspect the whole
|
|
738
|
+
// matched turn, not just the selected outcome row, so
|
|
739
|
+
// either a direct image or a later delivery row can
|
|
740
|
+
// keep the terminal media-complete.
|
|
741
|
+
let historyContentBlocks = outcome?.kind === "final"
|
|
742
|
+
? hasCanonicalProjectionV3Snapshot(history)
|
|
743
|
+
? mergeNonTextContentBlocks(nonTextContentBlocksFromHistory(outcome.message), extractExplicitParentMediaContent(history, outcome.message))
|
|
744
|
+
: extractHistoryMediaContent(history, runContext)
|
|
745
|
+
: [];
|
|
746
|
+
if (outcome?.kind === "final" && historyContentBlocks.length > 0) {
|
|
747
|
+
// A text terminal is not guaranteed to carry the
|
|
748
|
+
// message-tool identity. Recover it from the stable
|
|
749
|
+
// transcript delivery row before building timeline
|
|
750
|
+
// events; otherwise every sidecar in the run is
|
|
751
|
+
// projected onto assistant-${runId}.
|
|
752
|
+
const historyReplyMessageId = assistantHistoryReplyMessageId(outcome.message, canonicalRunId);
|
|
753
|
+
const mergedContentBlocks = mergeNonTextContentBlocks(directContentBlocks, historyContentBlocks);
|
|
754
|
+
const basePayload = mergeCanonicalChatPayload(normalizedPayload, buildMobileAssistantFinalPayload({
|
|
755
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
756
|
+
text: resolvedText,
|
|
757
|
+
contentBlocks: mergedContentBlocks,
|
|
758
|
+
...chatLineage,
|
|
759
|
+
...((replyMessageId ?? historyReplyMessageId)
|
|
760
|
+
? { messageId: replyMessageId ?? historyReplyMessageId }
|
|
761
|
+
: {}),
|
|
762
|
+
includeTimelineEvents: true,
|
|
763
|
+
...mobileAssistantUsageFromPayload(normalizedPayload),
|
|
764
|
+
}));
|
|
765
|
+
outgoingPayload = buildFinalPayloadFromHistoryOutcome(basePayload, outcome, [{ type: "text", text: outcome.text }, ...mergedContentBlocks]);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
// Media enrichment is opportunistic. The final text event
|
|
770
|
+
// must still reach the client when history is unavailable.
|
|
771
|
+
console.warn(`[relay] terminal media history enrichment skipped: ${String(error)}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (outgoingPayload === undefined) {
|
|
775
|
+
outgoingPayload = providerRunId
|
|
776
|
+
? mergeCanonicalChatPayload(normalizedPayload, buildMobileAssistantFinalPayload({
|
|
777
|
+
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
778
|
+
text: resolvedText,
|
|
779
|
+
contentBlocks: directContentBlocks,
|
|
780
|
+
...chatLineage,
|
|
781
|
+
...(replyMessageId ? { messageId: replyMessageId } : {}),
|
|
782
|
+
includeTimelineEvents: true,
|
|
783
|
+
...mobileAssistantUsageFromPayload(normalizedPayload),
|
|
784
|
+
}))
|
|
785
|
+
: withMessageText(normalizedPayload, resolvedText);
|
|
786
|
+
}
|
|
615
787
|
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice, runContext?.promptText);
|
|
616
788
|
if (providerRunId) {
|
|
617
789
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
618
|
-
chatRunContexts.delete(providerRunId);
|
|
619
790
|
}
|
|
620
791
|
}, Boolean(providerRunId));
|
|
621
792
|
return;
|
|
@@ -623,34 +794,39 @@ export async function runRelayManager(opts) {
|
|
|
623
794
|
const fetchHistory = () => requestChatHistoryFromClawConnect({ sessionKey: resolvedSessionKey, limit: 10 });
|
|
624
795
|
const publishHistoryTerminal = () => withTimeout(fetchHistory(), CHAT_HISTORY_FETCH_TIMEOUT_MS, "chat.history")
|
|
625
796
|
.then(async (history) => {
|
|
626
|
-
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
outcome = runContext ? extractHistoryOutcome(retryHistory, runContext) : null;
|
|
632
|
-
}
|
|
797
|
+
// The terminal event is the only completion signal. Do not
|
|
798
|
+
// turn transcript visibility into a wall-clock race: a
|
|
799
|
+
// later provider terminal/history page will re-enter this
|
|
800
|
+
// path and reconcile the newly committed source rows.
|
|
801
|
+
const outcome = runContext ? extractHistoryOutcome(history, runContext, currentText) : null;
|
|
633
802
|
if (providerRunId && outcome) {
|
|
634
|
-
chatRunContexts.delete(providerRunId);
|
|
635
803
|
}
|
|
636
804
|
if (outcome?.kind === "final") {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
805
|
+
const historyReplyMessageId = providerRunId
|
|
806
|
+
? assistantHistoryReplyMessageId(outcome.message, canonicalRunId)
|
|
807
|
+
: undefined;
|
|
641
808
|
const basePayload = providerRunId
|
|
642
809
|
? buildMobileAssistantFinalPayload({
|
|
643
810
|
run: { runId: canonicalRunId, sessionKey: resolvedSessionKey },
|
|
644
811
|
text: outcome.text,
|
|
645
|
-
|
|
812
|
+
...chatLineage,
|
|
813
|
+
contentBlocks: runContext
|
|
814
|
+
? hasCanonicalProjectionV3Snapshot(history)
|
|
815
|
+
? mergeNonTextContentBlocks(nonTextContentBlocksFromHistory(outcome.message), extractExplicitParentMediaContent(history, outcome.message))
|
|
816
|
+
: extractHistoryMediaContent(history, runContext)
|
|
817
|
+
: nonTextContentBlocksFromHistory(outcome.message),
|
|
818
|
+
...((replyMessageId ?? historyReplyMessageId)
|
|
819
|
+
? { messageId: replyMessageId ?? historyReplyMessageId }
|
|
820
|
+
: {}),
|
|
646
821
|
includeTimelineEvents: true,
|
|
647
822
|
...mobileAssistantUsageFromPayload(normalizedPayload),
|
|
648
823
|
})
|
|
649
824
|
: normalizedPayload;
|
|
650
825
|
const outgoingPayload = buildFinalPayloadFromHistoryOutcome(basePayload, outcome);
|
|
651
826
|
await publishAndSendGatewayEvent(event, outgoingPayload, shouldPublishOffice, runContext?.promptText);
|
|
652
|
-
if (providerRunId)
|
|
827
|
+
if (providerRunId) {
|
|
653
828
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
829
|
+
}
|
|
654
830
|
return;
|
|
655
831
|
}
|
|
656
832
|
if (outcome?.kind === "error") {
|
|
@@ -670,21 +846,13 @@ export async function runRelayManager(opts) {
|
|
|
670
846
|
openClawChatRunIdentities.clearTransient(opts.gatewayId, providerRunId);
|
|
671
847
|
return;
|
|
672
848
|
}
|
|
673
|
-
if (providerRunId && runContext) {
|
|
674
|
-
scheduleChatHistoryFallback(providerRunId, runContext, 0);
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
849
|
await publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
678
850
|
})
|
|
679
851
|
.catch((err) => {
|
|
680
852
|
console.error(`[relay] chat.history fetch failed: ${err}`);
|
|
681
|
-
if (providerRunId && runContext) {
|
|
682
|
-
scheduleChatHistoryFallback(providerRunId, runContext, 0);
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
853
|
void publishAndSendGatewayEvent(event, normalizedPayload, shouldPublishOffice);
|
|
686
854
|
});
|
|
687
|
-
runAfterAssistantSnapshot(runSnapshotKey,
|
|
855
|
+
runAfterAssistantSnapshot(runSnapshotKey, Boolean(replyMessageId), publishHistoryTerminal, Boolean(providerRunId));
|
|
688
856
|
return;
|
|
689
857
|
}
|
|
690
858
|
if (providerRunId && (state === "error" || state === "failed" || state === "fail" || state === "aborted")) {
|
|
@@ -857,6 +1025,22 @@ export async function runRelayManager(opts) {
|
|
|
857
1025
|
// thinking/tool/text event cannot win the race with this request.
|
|
858
1026
|
await ensureSessionMessagesSubscribed(commandSessionKey);
|
|
859
1027
|
}
|
|
1028
|
+
const sourceRunIdForState = commandMethod === "chat.send"
|
|
1029
|
+
? normalizeOpenClawMobileRunId(paramsRecord?.idempotencyKey)
|
|
1030
|
+
?? normalizeOpenClawMobileRunId(voiceInputRun?.runId)
|
|
1031
|
+
?? requestId
|
|
1032
|
+
: normalizeOpenClawMobileRunId(paramsRecord?.runId)
|
|
1033
|
+
?? normalizeOpenClawMobileRunId(paramsRecord?.idempotencyKey)
|
|
1034
|
+
?? requestId;
|
|
1035
|
+
if ((commandMethod === "chat.send" || commandMethod === "agent") && sourceRunIdForState) {
|
|
1036
|
+
await recordOpenClawActiveRun({
|
|
1037
|
+
gatewayId: opts.gatewayId,
|
|
1038
|
+
sessionKey: typeof paramsRecord?.sessionKey === "string" && paramsRecord.sessionKey.trim()
|
|
1039
|
+
? paramsRecord.sessionKey.trim()
|
|
1040
|
+
: sessionDefaults.mainSessionKey,
|
|
1041
|
+
sourceRunId: sourceRunIdForState,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
860
1044
|
const registerIdentity = (result) => {
|
|
861
1045
|
const resultRecord = result && typeof result === "object" && !Array.isArray(result)
|
|
862
1046
|
? result
|
|
@@ -884,11 +1068,19 @@ export async function runRelayManager(opts) {
|
|
|
884
1068
|
: undefined,
|
|
885
1069
|
};
|
|
886
1070
|
openClawChatRunIdentities.register(identity);
|
|
887
|
-
|
|
1071
|
+
setChatRunContext(identity.providerRunId, identity);
|
|
1072
|
+
void updateOpenClawActiveRunProviderId({
|
|
1073
|
+
gatewayId: opts.gatewayId,
|
|
1074
|
+
sessionKey: identity.sessionKey,
|
|
1075
|
+
sourceRunId: identity.canonicalRunId,
|
|
1076
|
+
providerRunId: identity.providerRunId,
|
|
1077
|
+
}).catch((error) => {
|
|
1078
|
+
console.warn(`[relay] failed to update OpenClaw active run provider id: ${String(error)}`);
|
|
1079
|
+
});
|
|
888
1080
|
return identity;
|
|
889
1081
|
};
|
|
890
1082
|
let identity;
|
|
891
|
-
|
|
1083
|
+
let result = commandMethod === "chat.history"
|
|
892
1084
|
? await requestChatHistoryFromClawConnect(params)
|
|
893
1085
|
: await gatewayClient.request(commandMethod, params, {
|
|
894
1086
|
// response 帧内同步登记,保证紧随其后的首个 delta 已能解析 canonical 身份。
|
|
@@ -897,6 +1089,9 @@ export async function runRelayManager(opts) {
|
|
|
897
1089
|
},
|
|
898
1090
|
});
|
|
899
1091
|
identity ??= registerIdentity(result);
|
|
1092
|
+
if (commandMethod === "agents.list" && gatewayClient) {
|
|
1093
|
+
result = await enrichAgentsListWithIdentities(result, gatewayClient);
|
|
1094
|
+
}
|
|
900
1095
|
return { params, result, identity };
|
|
901
1096
|
};
|
|
902
1097
|
const commandExecution = chatSendIdempotencyRequest
|
|
@@ -926,9 +1121,7 @@ export async function runRelayManager(opts) {
|
|
|
926
1121
|
const sessionKey = typeof paramsRecord?.sessionKey === "string" && paramsRecord.sessionKey.trim().length > 0
|
|
927
1122
|
? paramsRecord.sessionKey.trim()
|
|
928
1123
|
: sessionDefaults.mainSessionKey;
|
|
929
|
-
|
|
930
|
-
scheduleChatHistoryFallback(providerRunId, identity);
|
|
931
|
-
}
|
|
1124
|
+
ensureSourceCommitWatcher(sessionKey, "from_zero");
|
|
932
1125
|
if (commandMethod === "chat.send" && chatSendDedupeRequest) {
|
|
933
1126
|
chatSendDedupe.register(chatSendDedupeRequest, providerRunId);
|
|
934
1127
|
}
|
|
@@ -980,10 +1173,9 @@ export async function runRelayManager(opts) {
|
|
|
980
1173
|
opts.onDisconnected?.();
|
|
981
1174
|
gatewayClient?.stop();
|
|
982
1175
|
gatewayClient = null;
|
|
983
|
-
for (const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
chatFallbacks.clear();
|
|
1176
|
+
for (const watcher of sourceCommitWatchers.values())
|
|
1177
|
+
watcher.close();
|
|
1178
|
+
sourceCommitWatchers.clear();
|
|
987
1179
|
chatSendDedupe.clearAll();
|
|
988
1180
|
for (const timer of contextUsageRefreshes.values()) {
|
|
989
1181
|
clearTimeout(timer);
|