clawconnect-agent 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/README.zh-CN.md +7 -3
- package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
- package/dist/commands/hermes-file-transfer-outcome.js +40 -0
- package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
- package/dist/commands/send-file.js +39 -33
- package/dist/commands/send-file.js.map +1 -1
- package/dist/core/command-types.d.ts +5 -0
- package/dist/core/relay/chat-payload.js +32 -22
- package/dist/core/relay/chat-payload.js.map +1 -1
- package/dist/core/relay/file-upload.d.ts +3 -0
- package/dist/core/relay/file-upload.js +4 -0
- package/dist/core/relay/file-upload.js.map +1 -1
- package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
- package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
- package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
- package/dist/core/relay/office-payload.js +54 -22
- package/dist/core/relay/office-payload.js.map +1 -1
- package/dist/core/relay/source-commit.d.ts +41 -0
- package/dist/core/relay/source-commit.js +118 -0
- package/dist/core/relay/source-commit.js.map +1 -0
- package/dist/core/relay/timeline-event-builder.d.ts +6 -0
- package/dist/core/relay/timeline-event-builder.js +3 -0
- package/dist/core/relay/timeline-event-builder.js.map +1 -1
- package/dist/core/relay/timeline-event-log.d.ts +29 -0
- package/dist/core/relay/timeline-event-log.js +37 -0
- package/dist/core/relay/timeline-event-log.js.map +1 -1
- package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
- package/dist/core/relay/timeline-projection-v3.js +87 -0
- package/dist/core/relay/timeline-projection-v3.js.map +1 -0
- package/dist/hermes/hermes-relay-manager.js +3 -0
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
- package/dist/hermes/hermes-runtime-test-support.js +12 -6
- package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
- package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
- package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
- package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
- package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
- package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/openclaw/config/provider-config.js +4 -1
- package/dist/openclaw/config/provider-config.js.map +1 -1
- package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
- package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
- package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
- package/dist/openclaw/relay/assistant-media-sidecar.d.ts +28 -1
- package/dist/openclaw/relay/assistant-media-sidecar.js +270 -10
- package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
- package/dist/openclaw/relay/chat-history.d.ts +37 -2
- package/dist/openclaw/relay/chat-history.js +570 -49
- package/dist/openclaw/relay/chat-history.js.map +1 -1
- package/dist/openclaw/relay/chat-run-identity.js +3 -1
- package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
- package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
- package/dist/openclaw/relay/gateway-history-projection.js +67 -0
- package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
- package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
- package/dist/openclaw/relay/outgoing-media-relay.d.ts +35 -1
- package/dist/openclaw/relay/outgoing-media-relay.js +541 -123
- package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
- package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
- package/dist/openclaw/relay/session-context.d.ts +15 -0
- package/dist/openclaw/relay/session-context.js +48 -4
- package/dist/openclaw/relay/session-context.js.map +1 -1
- package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
- package/dist/openclaw/relay/source-commit-projection.js +140 -0
- package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
- package/dist/openclaw/relay/transcript-dedupe.js +11 -42
- package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
- package/dist/openclaw/relay-manager.js +329 -131
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/openclaw/runtime/local-runtime.js +4 -2
- package/dist/openclaw/runtime/local-runtime.js.map +1 -1
- package/package.json +1 -1
- package/dist/openclaw/session-store.d.ts +0 -6
- package/dist/openclaw/session-store.js +0 -231
- package/dist/openclaw/session-store.js.map +0 -1
|
@@ -1,8 +1,60 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { restoreGatewayHistoryMessages } from "./gateway-history-projection.js";
|
|
2
5
|
import { readFile, stat } from "fs/promises";
|
|
3
6
|
import { buildHistorySnapshotPage } from "../../core/relay/timeline-event-builder.js";
|
|
4
|
-
import { resolveOpenClawSessionTranscript, } from "./session-context.js";
|
|
5
|
-
import {
|
|
7
|
+
import { canonicalizeOpenClawSessionScope, resolveOpenClawSessionTranscript, sessionKeyCandidates, } from "./session-context.js";
|
|
8
|
+
import { resolveOpenClawStateDir } from "../runtime/openclaw-paths.js";
|
|
9
|
+
import { normalizeOpenClawAssistantMediaSidecars, normalizeOpenClawAutomaticMediaReplies, } from "./assistant-media-sidecar.js";
|
|
10
|
+
import { adaptOpenClawMessageToolDelivery } from "./outgoing-media-relay.js";
|
|
11
|
+
import { createProjectionMetadata, openClawAgentIdFromSessionKey, openClawSourceOrderScope, } from "../../core/relay/timeline-projection-v3.js";
|
|
12
|
+
import { createSourceCommit } from "../../core/relay/source-commit.js";
|
|
13
|
+
/**
|
|
14
|
+
* Read the durable OpenClaw SQLite watermark for one session. This is a
|
|
15
|
+
* source-commit signal only; callers must not use it to derive message ids or
|
|
16
|
+
* reorder rows. It intentionally returns null when the authoritative SQLite
|
|
17
|
+
* source is not available so the caller can use the native gateway stream.
|
|
18
|
+
*/
|
|
19
|
+
export function readOpenClawSourceCommitCursor(rawParams, defaults) {
|
|
20
|
+
const params = normalizeTranscriptHistoryParams(rawParams, defaults.mainSessionKey);
|
|
21
|
+
const agentId = params.sessionKey.match(/^agent:([^:]+):/)?.[1] ?? defaults.defaultAgentId ?? "main";
|
|
22
|
+
const databasePath = join(resolveOpenClawStateDir(), "agents", agentId, "agent", "openclaw-agent.sqlite");
|
|
23
|
+
let database;
|
|
24
|
+
try {
|
|
25
|
+
database = new DatabaseSync(databasePath, { readOnly: true });
|
|
26
|
+
const session = findSqliteSessionNode(database, params.sessionKey, defaults);
|
|
27
|
+
const sourceSessionId = cleanHistoryString(session?.current_session_id);
|
|
28
|
+
if (!sourceSessionId)
|
|
29
|
+
return null;
|
|
30
|
+
const row = database.prepare(`
|
|
31
|
+
SELECT MAX(seq) AS committed_through_seq
|
|
32
|
+
FROM transcript_events
|
|
33
|
+
WHERE session_id = ?
|
|
34
|
+
`).get(sourceSessionId);
|
|
35
|
+
const committedThroughSeq = Number(row?.committed_through_seq ?? 0);
|
|
36
|
+
if (!Number.isSafeInteger(committedThroughSeq) || committedThroughSeq < 0)
|
|
37
|
+
return null;
|
|
38
|
+
const gatewayId = requireProjectionIdentity(params.projectionGatewayId, "gatewayId");
|
|
39
|
+
const sourceOrderScope = openClawSourceOrderScope({ agentId, sessionId: sourceSessionId });
|
|
40
|
+
return createSourceCommit({
|
|
41
|
+
gatewayType: "openclaw",
|
|
42
|
+
gatewayId,
|
|
43
|
+
producerId: agentId,
|
|
44
|
+
sourceSessionId,
|
|
45
|
+
sourceOrderScope,
|
|
46
|
+
sourceGeneration: sourceSessionId,
|
|
47
|
+
committedThroughSeq,
|
|
48
|
+
sourceRevision: `seq:${committedThroughSeq}`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
database?.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
6
58
|
const DEFAULT_TRANSCRIPT_HISTORY_LIMIT = 100;
|
|
7
59
|
const MAX_TRANSCRIPT_HISTORY_LIMIT = 200;
|
|
8
60
|
const CURSOR_PREFIX = "seq:";
|
|
@@ -14,6 +66,14 @@ export function clearTranscriptHistoryCache() {
|
|
|
14
66
|
}
|
|
15
67
|
export async function readOpenClawTranscriptChatHistory(rawParams, defaults) {
|
|
16
68
|
const params = normalizeTranscriptHistoryParams(rawParams, defaults.mainSessionKey);
|
|
69
|
+
// OpenClaw 2026.8 writes the live conversation into the agent SQLite event
|
|
70
|
+
// log. Its seq is the same order used by Control UI; falling through to the
|
|
71
|
+
// gateway projection loses that order for records appended asynchronously
|
|
72
|
+
// (notably scheduled jobs with images).
|
|
73
|
+
const sqliteHistory = readOpenClawSqliteChatHistory(params, defaults);
|
|
74
|
+
if (sqliteHistory) {
|
|
75
|
+
return sqliteHistory;
|
|
76
|
+
}
|
|
17
77
|
const transcript = await resolveOpenClawSessionTranscript(params.sessionKey, defaults);
|
|
18
78
|
if (!transcript) {
|
|
19
79
|
return null;
|
|
@@ -22,39 +82,178 @@ export async function readOpenClawTranscriptChatHistory(rawParams, defaults) {
|
|
|
22
82
|
sessionKey: transcript.sessionKey,
|
|
23
83
|
sessionId: transcript.sessionId,
|
|
24
84
|
transcriptPath: transcript.logPath,
|
|
85
|
+
projectionGatewayId: params.projectionGatewayId,
|
|
25
86
|
limit: params.limit,
|
|
26
87
|
cursor: params.cursor,
|
|
27
88
|
direction: params.direction,
|
|
89
|
+
projectionVersion: params.projectionVersion,
|
|
90
|
+
sessionDefaults: defaults,
|
|
28
91
|
});
|
|
29
92
|
}
|
|
93
|
+
function readOpenClawSqliteChatHistory(params, defaults) {
|
|
94
|
+
const agentId = params.sessionKey.match(/^agent:([^:]+):/)?.[1] ?? defaults.defaultAgentId ?? "main";
|
|
95
|
+
const databasePath = join(resolveOpenClawStateDir(), "agents", agentId, "agent", "openclaw-agent.sqlite");
|
|
96
|
+
let database;
|
|
97
|
+
try {
|
|
98
|
+
database = new DatabaseSync(databasePath, { readOnly: true });
|
|
99
|
+
const session = findSqliteSessionNode(database, params.sessionKey, defaults);
|
|
100
|
+
const sessionId = cleanHistoryString(session?.current_session_id);
|
|
101
|
+
if (!sessionId)
|
|
102
|
+
return null;
|
|
103
|
+
const rows = database.prepare(`
|
|
104
|
+
SELECT seq, event_json
|
|
105
|
+
FROM transcript_events
|
|
106
|
+
WHERE session_id = ?
|
|
107
|
+
ORDER BY seq ASC
|
|
108
|
+
`).all(sessionId);
|
|
109
|
+
const visibleParentIds = resolveSqliteVisibleParentIds(rows);
|
|
110
|
+
const messages = rows
|
|
111
|
+
.map((row) => sqliteTranscriptHistoryMessage(row, sessionId, params.projectionVersion === 3, visibleParentIds))
|
|
112
|
+
.filter((message) => Boolean(message));
|
|
113
|
+
if (messages.length === 0 && rows.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
const cursorSeq = parseHistoryCursorSeq(params.cursor);
|
|
116
|
+
const sourceRange = params.direction === "newer"
|
|
117
|
+
? (() => {
|
|
118
|
+
const startIndex = rows.findIndex((row) => {
|
|
119
|
+
const seq = typeof row.seq === "number" && Number.isSafeInteger(row.seq) ? row.seq : undefined;
|
|
120
|
+
return seq !== undefined && (cursorSeq === undefined || seq > cursorSeq);
|
|
121
|
+
});
|
|
122
|
+
const resolvedStart = startIndex < 0 ? rows.length : startIndex;
|
|
123
|
+
const rawPage = rows.slice(resolvedStart, resolvedStart + params.limit);
|
|
124
|
+
const rawLast = rawPage.at(-1)?.seq;
|
|
125
|
+
const rawFirst = rawPage[0]?.seq;
|
|
126
|
+
const rawSeqs = rawPage.map((row) => row.seq);
|
|
127
|
+
const sourceRangeHasGap = rawSeqs.some((seq, index) => {
|
|
128
|
+
if (index === 0)
|
|
129
|
+
return false;
|
|
130
|
+
const previous = rawSeqs[index - 1];
|
|
131
|
+
return typeof previous !== "number"
|
|
132
|
+
|| !Number.isSafeInteger(previous)
|
|
133
|
+
|| typeof seq !== "number"
|
|
134
|
+
|| !Number.isSafeInteger(seq)
|
|
135
|
+
|| seq !== previous + 1;
|
|
136
|
+
});
|
|
137
|
+
const sourceReadThroughSeq = typeof rawLast === "number" && Number.isSafeInteger(rawLast)
|
|
138
|
+
? rawLast
|
|
139
|
+
: undefined;
|
|
140
|
+
const sourceRangeStartSeq = typeof rawFirst === "number" && Number.isSafeInteger(rawFirst)
|
|
141
|
+
? rawFirst
|
|
142
|
+
: undefined;
|
|
143
|
+
return sourceReadThroughSeq === undefined || sourceRangeStartSeq === undefined
|
|
144
|
+
? undefined
|
|
145
|
+
: {
|
|
146
|
+
sourceReadThroughSeq,
|
|
147
|
+
sourceRangeStartSeq,
|
|
148
|
+
sourceRangeHasGap,
|
|
149
|
+
sourceHasMore: resolvedStart + rawPage.length < rows.length,
|
|
150
|
+
};
|
|
151
|
+
})()
|
|
152
|
+
: undefined;
|
|
153
|
+
return buildTranscriptHistoryResponse({
|
|
154
|
+
sessionKey: params.sessionKey,
|
|
155
|
+
sessionId,
|
|
156
|
+
projectionGatewayId: params.projectionGatewayId,
|
|
157
|
+
messages: prepareTranscriptHistoryMessages(messages, params.sessionKey, params.projectionVersion, defaults),
|
|
158
|
+
limit: params.limit,
|
|
159
|
+
cursor: params.cursor,
|
|
160
|
+
direction: params.direction,
|
|
161
|
+
projectionVersion: params.projectionVersion,
|
|
162
|
+
...(sourceRange ?? {}),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (params.projectionVersion === 3 && error instanceof Error && error.message.startsWith("OpenClaw projection v3")) {
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
database?.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function findSqliteSessionNode(database, sessionKey, defaults) {
|
|
176
|
+
const statement = database.prepare(`
|
|
177
|
+
SELECT current_session_id
|
|
178
|
+
FROM session_nodes
|
|
179
|
+
WHERE session_key = ?
|
|
180
|
+
LIMIT 1
|
|
181
|
+
`);
|
|
182
|
+
for (const candidate of sessionKeyCandidates(sessionKey, defaults)) {
|
|
183
|
+
const session = statement.get(candidate);
|
|
184
|
+
if (session)
|
|
185
|
+
return session;
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
30
189
|
export async function readChatHistoryFromTranscriptFile(request) {
|
|
31
|
-
const messages = await readIndexedTranscriptMessages(request.transcriptPath, request.sessionKey);
|
|
32
|
-
|
|
33
|
-
|
|
190
|
+
const messages = await readIndexedTranscriptMessages(request.transcriptPath, request.sessionKey, request.projectionVersion, request.sessionDefaults);
|
|
191
|
+
return buildTranscriptHistoryResponse({
|
|
192
|
+
sessionKey: request.sessionKey,
|
|
193
|
+
...(request.sessionId ? { sessionId: request.sessionId } : {}),
|
|
194
|
+
projectionGatewayId: request.projectionGatewayId,
|
|
195
|
+
messages,
|
|
196
|
+
limit: normalizeHistoryLimit(request.limit),
|
|
197
|
+
cursor: normalizeCursor(request.cursor),
|
|
198
|
+
direction: normalizeHistoryDirection(request.direction),
|
|
199
|
+
projectionVersion: request.projectionVersion,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function buildTranscriptHistoryResponse(request) {
|
|
203
|
+
const projectionGatewayId = request.projectionVersion === 3
|
|
204
|
+
? requireProjectionIdentity(request.projectionGatewayId, "gatewayId")
|
|
205
|
+
: request.projectionGatewayId ?? "clawconnect";
|
|
206
|
+
const projectionSessionId = request.projectionVersion === 3
|
|
207
|
+
? requireProjectionIdentity(request.sessionId, "sourceSessionId")
|
|
208
|
+
: request.sessionId;
|
|
34
209
|
const cursorSeq = parseHistoryCursorSeq(request.cursor);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
210
|
+
const boundedMessages = request.direction === "newer" && request.sourceReadThroughSeq !== undefined
|
|
211
|
+
? request.messages.filter((message) => {
|
|
212
|
+
const seq = messageSeq(message);
|
|
213
|
+
return seq !== undefined
|
|
214
|
+
&& (cursorSeq === undefined || seq > cursorSeq)
|
|
215
|
+
&& seq <= request.sourceReadThroughSeq;
|
|
216
|
+
})
|
|
217
|
+
: request.messages;
|
|
218
|
+
const page = paginateHistoryMessages(boundedMessages, {
|
|
219
|
+
limit: request.limit,
|
|
220
|
+
direction: request.direction,
|
|
38
221
|
cursorSeq,
|
|
39
222
|
});
|
|
40
223
|
return {
|
|
41
224
|
sessionKey: request.sessionKey,
|
|
225
|
+
...(request.projectionVersion === 3 ? { projectionVersion: 3 } : {}),
|
|
42
226
|
...(request.sessionId ? { sessionId: request.sessionId } : {}),
|
|
43
227
|
messages: page.messages,
|
|
44
228
|
hasMore: page.hasMore,
|
|
45
229
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
46
230
|
...(page.newestCursor ? { newestCursor: page.newestCursor } : {}),
|
|
231
|
+
...(request.sourceReadThroughSeq !== undefined ? { sourceReadThroughSeq: request.sourceReadThroughSeq } : {}),
|
|
232
|
+
...(request.sourceRangeStartSeq !== undefined ? { sourceRangeStartSeq: request.sourceRangeStartSeq } : {}),
|
|
233
|
+
...(request.sourceRangeHasGap !== undefined ? { sourceRangeHasGap: request.sourceRangeHasGap } : {}),
|
|
234
|
+
...(request.sourceHasMore !== undefined ? { sourceHasMore: request.sourceHasMore } : {}),
|
|
47
235
|
timelineSnapshot: buildHistorySnapshotPage({
|
|
48
|
-
gatewayId:
|
|
236
|
+
gatewayId: projectionGatewayId,
|
|
49
237
|
sessionKey: request.sessionKey,
|
|
50
238
|
cursor: normalizeCursor(request.cursor) ?? null,
|
|
51
239
|
hasMore: page.hasMore,
|
|
52
240
|
nextCursor: page.nextCursor ?? null,
|
|
53
241
|
newestCursor: page.newestCursor ?? null,
|
|
54
242
|
orderPolicy: "transcript",
|
|
55
|
-
...(
|
|
243
|
+
...(projectionSessionId
|
|
244
|
+
? {
|
|
245
|
+
sourceOrderScope: request.projectionVersion === 3
|
|
246
|
+
? openClawSourceOrderScope({
|
|
247
|
+
agentId: openClawAgentIdFromSessionKey(request.sessionKey),
|
|
248
|
+
sessionId: projectionSessionId,
|
|
249
|
+
})
|
|
250
|
+
: projectionSessionId,
|
|
251
|
+
}
|
|
252
|
+
: {}),
|
|
56
253
|
messages: page.messages.map((message, index) => {
|
|
57
|
-
const seq =
|
|
254
|
+
const seq = request.projectionVersion === 3
|
|
255
|
+
? requireProjectionSequence(messageSeq(message), index)
|
|
256
|
+
: messageSeq(message) ?? index + 1;
|
|
58
257
|
const role = normalizeTimelineRole(message.role);
|
|
59
258
|
const clientMessageId = historyString(message, "clientMessageId", "client_message_id");
|
|
60
259
|
const idempotencyKey = historyString(message, "idempotencyKey", "idempotency_key");
|
|
@@ -73,10 +272,34 @@ export async function readChatHistoryFromTranscriptFile(request) {
|
|
|
73
272
|
? block
|
|
74
273
|
: { ...block, toolCallId })
|
|
75
274
|
: content;
|
|
275
|
+
const sourceSessionId = request.projectionVersion === 3 ? projectionSessionId : undefined;
|
|
276
|
+
const sourceMessageId = request.projectionVersion === 3
|
|
277
|
+
? requireProjectionIdentity(messageId, "sourceMessageId")
|
|
278
|
+
: messageId;
|
|
279
|
+
const projection = sourceSessionId && sourceMessageId
|
|
280
|
+
? createProjectionMetadata({
|
|
281
|
+
gatewayType: "openclaw",
|
|
282
|
+
gatewayId: projectionGatewayId,
|
|
283
|
+
producerId: openClawAgentIdFromSessionKey(request.sessionKey),
|
|
284
|
+
sourceSessionId,
|
|
285
|
+
sourceMessageId,
|
|
286
|
+
sourceOrderScope: openClawSourceOrderScope({
|
|
287
|
+
agentId: openClawAgentIdFromSessionKey(request.sessionKey),
|
|
288
|
+
sessionId: sourceSessionId,
|
|
289
|
+
}),
|
|
290
|
+
sourceOrderSeq: seq,
|
|
291
|
+
sourceRole: role,
|
|
292
|
+
...(historyString(message, "parentId", "parent_id")
|
|
293
|
+
? { parentSourceMessageId: historyString(message, "parentId", "parent_id") }
|
|
294
|
+
: {}),
|
|
295
|
+
timelineDelivery: "independent",
|
|
296
|
+
})
|
|
297
|
+
: undefined;
|
|
298
|
+
const timelineMessageId = projection?.canonicalMessageId ?? messageId ?? `${role}-${turnId}`;
|
|
76
299
|
return {
|
|
77
300
|
turnId,
|
|
78
301
|
runId: historyString(message, "runId", "run_id") ?? turnId,
|
|
79
|
-
messageId:
|
|
302
|
+
messageId: timelineMessageId,
|
|
80
303
|
role,
|
|
81
304
|
messageState: normalizeTimelineMessageState(message),
|
|
82
305
|
createdAt: normalizeTimelineCreatedAt(message) ?? fallbackTimelineCreatedAt(page.messages, index),
|
|
@@ -89,6 +312,7 @@ export async function readChatHistoryFromTranscriptFile(request) {
|
|
|
89
312
|
...(extractAttachmentIds(canonicalContent).length > 0
|
|
90
313
|
? { attachmentIds: extractAttachmentIds(canonicalContent) }
|
|
91
314
|
: {}),
|
|
315
|
+
...(projection ?? {}),
|
|
92
316
|
};
|
|
93
317
|
}),
|
|
94
318
|
attachments: [],
|
|
@@ -103,19 +327,46 @@ export async function readChatHistoryFromTranscriptFile(request) {
|
|
|
103
327
|
* of asking downstream clients to infer missing user turns from assistant text.
|
|
104
328
|
*/
|
|
105
329
|
export function canonicalizeOpenClawGatewayHistoryResponse(history, request) {
|
|
106
|
-
const
|
|
107
|
-
|
|
330
|
+
const projectionGatewayId = request.projectionVersion === 3
|
|
331
|
+
? requireProjectionIdentity(request.projectionGatewayId, "gatewayId")
|
|
332
|
+
: request.projectionGatewayId ?? "clawconnect";
|
|
333
|
+
const requestedSessionKey = cleanHistoryString(history.sessionKey) ?? request.sessionKey;
|
|
334
|
+
const restored = restoreGatewayHistoryMessages(history.messages ?? []);
|
|
335
|
+
const projectionVersion = request.projectionVersion;
|
|
336
|
+
const materializedDeliveries = materializeOpenClawMessageToolDeliveries(restored);
|
|
337
|
+
const explicitSidecars = normalizeOpenClawAssistantMediaSidecars(materializedDeliveries.messages, requestedSessionKey, {
|
|
338
|
+
...(projectionVersion === 3 ? { projectionVersion: 3 } : {}),
|
|
339
|
+
...(request.sessionDefaults ? { sessionDefaults: request.sessionDefaults } : {}),
|
|
340
|
+
});
|
|
341
|
+
const automaticMediaReplies = normalizeOpenClawAutomaticMediaReplies(explicitSidecars.messages, requestedSessionKey, {
|
|
342
|
+
...(projectionVersion === 3 ? { projectionVersion: 3 } : {}),
|
|
343
|
+
...(request.sessionDefaults ? { sessionDefaults: request.sessionDefaults } : {}),
|
|
344
|
+
});
|
|
345
|
+
const normalizedHistory = materializedDeliveries.changed || restored !== history.messages || explicitSidecars.changed || automaticMediaReplies.changed
|
|
346
|
+
? { ...history, messages: automaticMediaReplies.messages }
|
|
347
|
+
: history;
|
|
348
|
+
const filtered = filterOpenClawHeartbeatHistoryResponse(normalizedHistory);
|
|
349
|
+
if (filtered.timelineSnapshot && projectionVersion !== 3) {
|
|
108
350
|
return filtered;
|
|
109
351
|
}
|
|
110
|
-
const sessionKey = cleanHistoryString(filtered.sessionKey) ?? request.sessionKey;
|
|
352
|
+
const sessionKey = canonicalizeOpenClawSessionScope(cleanHistoryString(filtered.sessionKey) ?? request.sessionKey, request.sessionDefaults) ?? (cleanHistoryString(filtered.sessionKey) ?? request.sessionKey);
|
|
111
353
|
const messages = filtered.messages ?? [];
|
|
112
|
-
const
|
|
113
|
-
|
|
354
|
+
const rawSourceSessionId = cleanHistoryString(filtered.sessionId);
|
|
355
|
+
const sourceSessionId = projectionVersion === 3
|
|
356
|
+
? requireProjectionIdentity(rawSourceSessionId, "sourceSessionId")
|
|
357
|
+
: undefined;
|
|
358
|
+
const agentId = openClawAgentIdFromSessionKey(sessionKey);
|
|
359
|
+
const sourceOrderScope = rawSourceSessionId
|
|
360
|
+
? projectionVersion === 3
|
|
361
|
+
? openClawSourceOrderScope({ agentId, sessionId: rawSourceSessionId })
|
|
362
|
+
: rawSourceSessionId
|
|
363
|
+
: messages.map(openClawTranscriptSource).find((value) => Boolean(value));
|
|
114
364
|
return {
|
|
115
365
|
...filtered,
|
|
116
366
|
sessionKey,
|
|
367
|
+
...(projectionVersion === 3 ? { projectionVersion: 3 } : {}),
|
|
117
368
|
timelineSnapshot: buildHistorySnapshotPage({
|
|
118
|
-
gatewayId:
|
|
369
|
+
gatewayId: projectionGatewayId,
|
|
119
370
|
sessionKey,
|
|
120
371
|
cursor: request.cursor ?? null,
|
|
121
372
|
hasMore: Boolean(filtered.hasMore),
|
|
@@ -123,12 +374,18 @@ export function canonicalizeOpenClawGatewayHistoryResponse(history, request) {
|
|
|
123
374
|
newestCursor: filtered.newestCursor ?? null,
|
|
124
375
|
orderPolicy: "transcript",
|
|
125
376
|
...(sourceOrderScope ? { sourceOrderScope } : {}),
|
|
126
|
-
messages: canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey
|
|
377
|
+
messages: canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey, {
|
|
378
|
+
agentId,
|
|
379
|
+
projectionGatewayId,
|
|
380
|
+
...(projectionVersion === 3 ? { projectionVersion: 3 } : {}),
|
|
381
|
+
...(sourceSessionId ? { sourceSessionId } : {}),
|
|
382
|
+
...(sourceOrderScope ? { sourceOrderScope } : {}),
|
|
383
|
+
}),
|
|
127
384
|
attachments: [],
|
|
128
385
|
}),
|
|
129
386
|
};
|
|
130
387
|
}
|
|
131
|
-
function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
388
|
+
function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey, projection) {
|
|
132
389
|
const finalAssistantIndexByRunId = new Map();
|
|
133
390
|
messages.forEach((message, index) => {
|
|
134
391
|
if (normalizeTimelineRole(message.role) !== "assistant")
|
|
@@ -138,7 +395,9 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
138
395
|
finalAssistantIndexByRunId.set(runId, index);
|
|
139
396
|
});
|
|
140
397
|
return messages.map((message, index) => {
|
|
141
|
-
const seq =
|
|
398
|
+
const seq = projection?.projectionVersion === 3
|
|
399
|
+
? requireProjectionSequence(messageSeq(message), index)
|
|
400
|
+
: messageSeq(message) ?? index + 1;
|
|
142
401
|
const role = normalizeTimelineRole(message.role);
|
|
143
402
|
const rawIdempotencyKey = historyString(message, "idempotencyKey", "idempotency_key")
|
|
144
403
|
?? openClawHistoryString(message, "idempotencyKey", "idempotency_key");
|
|
@@ -147,6 +406,9 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
147
406
|
?? normalizeTranscriptTurnId(clientMessageId);
|
|
148
407
|
const providerMessageId = historyString(message, "messageId", "message_id", "id")
|
|
149
408
|
?? openClawHistoryString(message, "id");
|
|
409
|
+
const sourceMessageId = projection?.projectionVersion === 3
|
|
410
|
+
? requireProjectionIdentity(providerMessageId, "sourceMessageId")
|
|
411
|
+
: providerMessageId;
|
|
150
412
|
const runId = historyString(message, "runId", "run_id")
|
|
151
413
|
?? openClawRunId(message)
|
|
152
414
|
?? normalizedInputId;
|
|
@@ -172,11 +434,32 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
172
434
|
toolCallId,
|
|
173
435
|
index,
|
|
174
436
|
finalAssistantIndexByRunId,
|
|
437
|
+
projectionVersion: projection?.projectionVersion,
|
|
175
438
|
});
|
|
439
|
+
const sourceSessionId = projection?.sourceSessionId;
|
|
440
|
+
const sourceOrderScope = projection?.sourceOrderScope;
|
|
441
|
+
const projectionMetadata = projection?.projectionVersion === 3
|
|
442
|
+
&& sourceSessionId && sourceOrderScope && sourceMessageId
|
|
443
|
+
? createProjectionMetadata({
|
|
444
|
+
gatewayType: "openclaw",
|
|
445
|
+
gatewayId: requireProjectionIdentity(projection.projectionGatewayId, "gatewayId"),
|
|
446
|
+
producerId: projection.agentId,
|
|
447
|
+
sourceSessionId,
|
|
448
|
+
sourceMessageId,
|
|
449
|
+
sourceOrderScope,
|
|
450
|
+
sourceOrderSeq: seq,
|
|
451
|
+
sourceRole: role,
|
|
452
|
+
...(historyString(message, "parentId", "parent_id")
|
|
453
|
+
? { parentSourceMessageId: historyString(message, "parentId", "parent_id") }
|
|
454
|
+
: {}),
|
|
455
|
+
timelineDelivery: "independent",
|
|
456
|
+
})
|
|
457
|
+
: undefined;
|
|
458
|
+
const timelineMessageId = projectionMetadata?.canonicalMessageId ?? messageId;
|
|
176
459
|
return {
|
|
177
460
|
turnId,
|
|
178
461
|
runId: runId ?? turnId,
|
|
179
|
-
messageId,
|
|
462
|
+
messageId: timelineMessageId,
|
|
180
463
|
role,
|
|
181
464
|
messageState: normalizeTimelineMessageState(message),
|
|
182
465
|
createdAt: normalizeTimelineCreatedAt(message) ?? fallbackTimelineCreatedAt(messages, index),
|
|
@@ -189,6 +472,7 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
189
472
|
...(extractAttachmentIds(canonicalContent).length > 0
|
|
190
473
|
? { attachmentIds: extractAttachmentIds(canonicalContent) }
|
|
191
474
|
: {}),
|
|
475
|
+
...(projectionMetadata ?? {}),
|
|
192
476
|
};
|
|
193
477
|
});
|
|
194
478
|
}
|
|
@@ -231,11 +515,16 @@ function normalizeTranscriptHistoryParams(rawParams, fallbackSessionKey) {
|
|
|
231
515
|
limit: normalizeHistoryLimit(record.limit),
|
|
232
516
|
...(cursor ? { cursor } : {}),
|
|
233
517
|
direction: normalizeHistoryDirection(record.direction),
|
|
518
|
+
...(record.projectionVersion === 3 ? { projectionVersion: 3 } : {}),
|
|
519
|
+
...(typeof record.projectionGatewayId === "string" && record.projectionGatewayId.trim().length > 0
|
|
520
|
+
? { projectionGatewayId: record.projectionGatewayId.trim() }
|
|
521
|
+
: {}),
|
|
234
522
|
};
|
|
235
523
|
}
|
|
236
|
-
async function readIndexedTranscriptMessages(transcriptPath, sessionKey) {
|
|
524
|
+
async function readIndexedTranscriptMessages(transcriptPath, sessionKey, projectionVersion, sessionDefaults) {
|
|
237
525
|
const stats = await stat(transcriptPath);
|
|
238
|
-
const
|
|
526
|
+
const cacheKey = `${transcriptPath}\u0000${projectionVersion === 3 ? "v3" : "v2"}`;
|
|
527
|
+
const cached = transcriptHistoryCache.get(cacheKey);
|
|
239
528
|
if (cached && cached.size === stats.size && cached.mtimeMs === stats.mtimeMs) {
|
|
240
529
|
return cached.messages;
|
|
241
530
|
}
|
|
@@ -246,25 +535,157 @@ async function readIndexedTranscriptMessages(transcriptPath, sessionKey) {
|
|
|
246
535
|
if (!trimmed) {
|
|
247
536
|
continue;
|
|
248
537
|
}
|
|
249
|
-
const message = parseTranscriptHistoryLine(trimmed, messages.length + 1);
|
|
538
|
+
const message = parseTranscriptHistoryLine(trimmed, messages.length + 1, projectionVersion === 3);
|
|
250
539
|
if (message) {
|
|
251
540
|
messages.push(message);
|
|
252
541
|
}
|
|
253
542
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// the raw <run>:assistant-media key and parentId are the authoritative
|
|
257
|
-
// relationship needed to keep the desktop and mobile projections aligned.
|
|
258
|
-
const foldedMessages = normalizeOpenClawAssistantMediaSidecars(messages, sessionKey).messages;
|
|
259
|
-
restoreTranscriptTurnLineage(foldedMessages);
|
|
260
|
-
const visibleMessages = filterOpenClawHeartbeatArtifacts(foldedMessages);
|
|
261
|
-
transcriptHistoryCache.set(transcriptPath, {
|
|
543
|
+
const visibleMessages = prepareTranscriptHistoryMessages(messages, sessionKey, projectionVersion, sessionDefaults);
|
|
544
|
+
transcriptHistoryCache.set(cacheKey, {
|
|
262
545
|
size: stats.size,
|
|
263
546
|
mtimeMs: stats.mtimeMs,
|
|
264
547
|
messages: visibleMessages,
|
|
265
548
|
});
|
|
266
549
|
return visibleMessages;
|
|
267
550
|
}
|
|
551
|
+
function sqliteTranscriptHistoryMessage(row, sessionId, strictProjectionV3 = false, visibleParentIds) {
|
|
552
|
+
const seq = typeof row.seq === "number" && Number.isFinite(row.seq) && row.seq > 0
|
|
553
|
+
? Math.round(row.seq)
|
|
554
|
+
: undefined;
|
|
555
|
+
if (!seq || typeof row.event_json !== "string")
|
|
556
|
+
return null;
|
|
557
|
+
let event;
|
|
558
|
+
try {
|
|
559
|
+
event = JSON.parse(row.event_json);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
if (!isRecord(event) || event.type !== "message" || !isRecord(event.message))
|
|
565
|
+
return null;
|
|
566
|
+
// `event.id`/`event.parentId` form the transcript's real lineage graph.
|
|
567
|
+
// Do not replace that graph with a synthetic seq id: assistant events often
|
|
568
|
+
// lack their own run id and are linked back to the originating user turn
|
|
569
|
+
// only through those event ids.
|
|
570
|
+
const sourceMessageId = cleanHistoryString(event.id);
|
|
571
|
+
const message = { ...event.message, seq };
|
|
572
|
+
if (strictProjectionV3 && !cleanHistoryString(message.id) && !sourceMessageId) {
|
|
573
|
+
throw new Error(`OpenClaw projection v3 sourceMessageId is missing at source seq ${seq}`);
|
|
574
|
+
}
|
|
575
|
+
if (!cleanHistoryString(message.id)) {
|
|
576
|
+
message.id = sourceMessageId ?? `sqlite-${sessionId}-${seq}`;
|
|
577
|
+
}
|
|
578
|
+
const timestamp = normalizeHistoryTimestamp(message.timestamp ?? event.timestamp);
|
|
579
|
+
if (timestamp !== undefined) {
|
|
580
|
+
message.timestamp = timestamp;
|
|
581
|
+
message.createdAt = cleanHistoryString(message.createdAt) ?? new Date(timestamp).toISOString();
|
|
582
|
+
}
|
|
583
|
+
const parentId = (sourceMessageId ? visibleParentIds?.get(sourceMessageId) : undefined)
|
|
584
|
+
?? cleanHistoryString(event.parentId)
|
|
585
|
+
?? cleanHistoryString(message.parentId);
|
|
586
|
+
if (parentId)
|
|
587
|
+
message.parentId = parentId;
|
|
588
|
+
return message;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* SQLite stores every transcript event in one parent graph, including hidden
|
|
592
|
+
* custom/thinking rows. Collapse only those hidden hops so visible messages
|
|
593
|
+
* retain deterministic source lineage and can inherit the originating mobile
|
|
594
|
+
* run id. Distinct message identities remain untouched.
|
|
595
|
+
*/
|
|
596
|
+
function resolveSqliteVisibleParentIds(rows) {
|
|
597
|
+
const nodes = new Map();
|
|
598
|
+
for (const row of rows) {
|
|
599
|
+
if (typeof row.event_json !== "string")
|
|
600
|
+
continue;
|
|
601
|
+
let event;
|
|
602
|
+
try {
|
|
603
|
+
event = JSON.parse(row.event_json);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (!isRecord(event))
|
|
609
|
+
continue;
|
|
610
|
+
const eventId = cleanHistoryString(event.id);
|
|
611
|
+
if (!eventId)
|
|
612
|
+
continue;
|
|
613
|
+
const visibleMessageId = event.type === "message" && isRecord(event.message)
|
|
614
|
+
? cleanHistoryString(event.message.id) ?? eventId
|
|
615
|
+
: undefined;
|
|
616
|
+
nodes.set(eventId, {
|
|
617
|
+
eventId,
|
|
618
|
+
...(cleanHistoryString(event.parentId) ? { parentId: cleanHistoryString(event.parentId) } : {}),
|
|
619
|
+
...(visibleMessageId ? { visibleMessageId } : {}),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const resolved = new Map();
|
|
623
|
+
for (const node of nodes.values()) {
|
|
624
|
+
if (!node.visibleMessageId || !node.parentId)
|
|
625
|
+
continue;
|
|
626
|
+
let parentId = node.parentId;
|
|
627
|
+
const visited = new Set();
|
|
628
|
+
while (parentId && !visited.has(parentId)) {
|
|
629
|
+
visited.add(parentId);
|
|
630
|
+
const parent = nodes.get(parentId);
|
|
631
|
+
if (!parent)
|
|
632
|
+
break;
|
|
633
|
+
if (parent.visibleMessageId) {
|
|
634
|
+
resolved.set(node.eventId, parent.visibleMessageId);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
parentId = parent.parentId;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return resolved;
|
|
641
|
+
}
|
|
642
|
+
function prepareTranscriptHistoryMessages(messages, sessionKey, projectionVersion, sessionDefaults) {
|
|
643
|
+
messages = materializeOpenClawMessageToolDeliveries(messages).messages;
|
|
644
|
+
// Fold the automatic assistant-media sidecar before lineage reconstruction.
|
|
645
|
+
// Lineage deliberately rewrites idempotency keys to mobile turn IDs, while
|
|
646
|
+
// the raw <run>:assistant-media key and parentId are the authoritative
|
|
647
|
+
// relationship needed to keep the desktop and mobile projections aligned.
|
|
648
|
+
const projectionOptions = projectionVersion === 3 ? { projectionVersion: 3 } : undefined;
|
|
649
|
+
const relationOptions = {
|
|
650
|
+
...(projectionOptions ?? {}),
|
|
651
|
+
...(sessionDefaults ? { sessionDefaults } : {}),
|
|
652
|
+
};
|
|
653
|
+
const explicitSidecars = normalizeOpenClawAssistantMediaSidecars(messages, sessionKey, relationOptions).messages;
|
|
654
|
+
restoreTranscriptTurnLineage(explicitSidecars, projectionVersion);
|
|
655
|
+
const foldedMessages = normalizeOpenClawAutomaticMediaReplies(explicitSidecars, sessionKey, relationOptions).messages;
|
|
656
|
+
return filterOpenClawHeartbeatArtifacts(foldedMessages);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The installed OpenClaw producer records a settled message-tool delivery as
|
|
660
|
+
* a `toolResult` row. That row is an execution detail, not a user-visible
|
|
661
|
+
* timeline item. Promote only the producer's strict, structured receipt to an
|
|
662
|
+
* independent assistant row; never use its prose content or generic
|
|
663
|
+
* `openclawDelivery.mediaUrls` display field as evidence.
|
|
664
|
+
*/
|
|
665
|
+
function materializeOpenClawMessageToolDeliveries(messages) {
|
|
666
|
+
let changed = false;
|
|
667
|
+
const materialized = messages.map((message) => {
|
|
668
|
+
if (message.role !== "toolResult" && message.role !== "tool_result") {
|
|
669
|
+
return message;
|
|
670
|
+
}
|
|
671
|
+
const receipt = adaptOpenClawMessageToolDelivery(message);
|
|
672
|
+
if (!receipt)
|
|
673
|
+
return message;
|
|
674
|
+
changed = true;
|
|
675
|
+
return {
|
|
676
|
+
...message,
|
|
677
|
+
role: "assistant",
|
|
678
|
+
runId: receipt.sourceRunId,
|
|
679
|
+
turnId: receipt.sourceRunId,
|
|
680
|
+
idempotencyKey: receipt.idempotencyKey,
|
|
681
|
+
// Keep the strict typed receipt on the canonical row until the relay
|
|
682
|
+
// replaces its local paths with uploaded attachment blocks.
|
|
683
|
+
openclawDelivery: receipt,
|
|
684
|
+
content: [],
|
|
685
|
+
};
|
|
686
|
+
});
|
|
687
|
+
return { messages: materialized, changed };
|
|
688
|
+
}
|
|
268
689
|
/**
|
|
269
690
|
* OpenClaw 会把内部心跳探测写入 transcript。只有精确的保留提示最终得到纯确认回执时,
|
|
270
691
|
* 才隐藏整段内部记录;若心跳产生真实告警,则保留完整内容供用户查看。
|
|
@@ -395,7 +816,7 @@ function resolveTextOnlyHistoryContent(content) {
|
|
|
395
816
|
}
|
|
396
817
|
return { text, hasNonTextContent };
|
|
397
818
|
}
|
|
398
|
-
function parseTranscriptHistoryLine(line, seq) {
|
|
819
|
+
function parseTranscriptHistoryLine(line, seq, strictProjectionV3 = false) {
|
|
399
820
|
let parsed;
|
|
400
821
|
try {
|
|
401
822
|
parsed = JSON.parse(line);
|
|
@@ -412,6 +833,9 @@ function parseTranscriptHistoryLine(line, seq) {
|
|
|
412
833
|
}
|
|
413
834
|
const message = { ...rawMessage, seq };
|
|
414
835
|
if (typeof message.id !== "string" || message.id.trim().length === 0) {
|
|
836
|
+
if (strictProjectionV3 && (typeof parsed.id !== "string" || parsed.id.trim().length === 0)) {
|
|
837
|
+
throw new Error(`OpenClaw projection v3 sourceMessageId is missing at source seq ${seq}`);
|
|
838
|
+
}
|
|
415
839
|
const id = typeof parsed.id === "string" && parsed.id.trim().length > 0 ? parsed.id.trim() : `transcript-${seq}`;
|
|
416
840
|
message.id = id;
|
|
417
841
|
}
|
|
@@ -434,7 +858,7 @@ function parseTranscriptHistoryLine(line, seq) {
|
|
|
434
858
|
* idempotency key through that chain so history and realtime resolve to the
|
|
435
859
|
* same canonical message without comparing text or timestamps.
|
|
436
860
|
*/
|
|
437
|
-
function restoreTranscriptTurnLineage(messages) {
|
|
861
|
+
function restoreTranscriptTurnLineage(messages, projectionVersion) {
|
|
438
862
|
const byId = new Map();
|
|
439
863
|
const lastAssistantByMobileTurn = new Map();
|
|
440
864
|
for (const message of messages) {
|
|
@@ -475,8 +899,10 @@ function restoreTranscriptTurnLineage(messages) {
|
|
|
475
899
|
}
|
|
476
900
|
// 一个 OpenClaw turn 可能包含若干工具中间消息;只有最后一个 assistant 输出
|
|
477
901
|
// 与实时 final 共用 canonical messageId,其他中间消息继续保留 transcript 身份。
|
|
478
|
-
|
|
479
|
-
message.
|
|
902
|
+
if (projectionVersion !== 3) {
|
|
903
|
+
for (const [turnId, message] of lastAssistantByMobileTurn.entries()) {
|
|
904
|
+
message.messageId = `assistant-${turnId}`;
|
|
905
|
+
}
|
|
480
906
|
}
|
|
481
907
|
}
|
|
482
908
|
function normalizeTranscriptTurnId(value) {
|
|
@@ -548,6 +974,18 @@ function normalizeHistoryLimit(value) {
|
|
|
548
974
|
function normalizeHistoryDirection(value) {
|
|
549
975
|
return value === "newer" ? "newer" : "older";
|
|
550
976
|
}
|
|
977
|
+
function requireProjectionIdentity(value, field) {
|
|
978
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
979
|
+
throw new Error(`OpenClaw projection v3 identity is missing ${field}`);
|
|
980
|
+
}
|
|
981
|
+
return value.trim();
|
|
982
|
+
}
|
|
983
|
+
function requireProjectionSequence(value, index) {
|
|
984
|
+
if (!Number.isSafeInteger(value) || (value ?? 0) <= 0) {
|
|
985
|
+
throw new Error(`OpenClaw projection v3 sourceOrderSeq is missing or invalid at row ${index}`);
|
|
986
|
+
}
|
|
987
|
+
return value;
|
|
988
|
+
}
|
|
551
989
|
function normalizeCursor(value) {
|
|
552
990
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
553
991
|
}
|
|
@@ -729,20 +1167,30 @@ function historyNumber(record, ...fields) {
|
|
|
729
1167
|
function isRecord(value) {
|
|
730
1168
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
731
1169
|
}
|
|
732
|
-
export function extractHistoryOutcome(history, context) {
|
|
1170
|
+
export function extractHistoryOutcome(history, context, preferredText) {
|
|
733
1171
|
const messages = history?.messages ?? [];
|
|
734
1172
|
if (messages.length === 0) {
|
|
735
1173
|
return null;
|
|
736
1174
|
}
|
|
737
|
-
const
|
|
738
|
-
if (
|
|
1175
|
+
const scope = findHistoryScope(messages, context);
|
|
1176
|
+
if (!scope) {
|
|
739
1177
|
return null;
|
|
740
1178
|
}
|
|
741
|
-
if (hasUnresolvedUserBefore(messages,
|
|
1179
|
+
if (scope.hasUser && hasUnresolvedUserBefore(messages, scope.startIndex)) {
|
|
742
1180
|
return null;
|
|
743
1181
|
}
|
|
744
1182
|
let latestError = null;
|
|
745
|
-
|
|
1183
|
+
let latestFinal = null;
|
|
1184
|
+
// `preferredText` is retained for call-site compatibility only. Assistant
|
|
1185
|
+
// prose is not a message identity and must never select an attachment row;
|
|
1186
|
+
// the stable turn/message/tool ids above and source order are authoritative.
|
|
1187
|
+
void preferredText;
|
|
1188
|
+
// When the history page starts after the user row, the first assistant row
|
|
1189
|
+
// carrying the same stable run id is the deterministic scope boundary. Do
|
|
1190
|
+
// not require a text/timestamp guess just because pagination omitted the
|
|
1191
|
+
// user record.
|
|
1192
|
+
const firstMessageIndex = scope.hasUser ? scope.startIndex + 1 : scope.startIndex;
|
|
1193
|
+
for (let index = firstMessageIndex; index < messages.length; index += 1) {
|
|
746
1194
|
const message = messages[index];
|
|
747
1195
|
if (message.role === "user") {
|
|
748
1196
|
return null;
|
|
@@ -751,8 +1199,15 @@ export function extractHistoryOutcome(history, context) {
|
|
|
751
1199
|
continue;
|
|
752
1200
|
}
|
|
753
1201
|
const text = extractHistoryMessageText(message);
|
|
754
|
-
|
|
755
|
-
|
|
1202
|
+
const mediaOnlyProjectionRow = history?.projectionVersion === 3
|
|
1203
|
+
&& text.length === 0
|
|
1204
|
+
&& nonTextHistoryContent(message).length > 0;
|
|
1205
|
+
if (!mediaOnlyProjectionRow && (text.length > 0 || hasHistoryMessageContent(message))) {
|
|
1206
|
+
// One OpenClaw run may contain several independent message-tool replies.
|
|
1207
|
+
// The last authoritative assistant row is the terminal history outcome;
|
|
1208
|
+
// returning the first row loses later media on fallback/replay.
|
|
1209
|
+
latestFinal = { kind: "final", text, message };
|
|
1210
|
+
continue;
|
|
756
1211
|
}
|
|
757
1212
|
if (typeof message.errorMessage === "string" &&
|
|
758
1213
|
message.errorMessage.trim().length > 0 &&
|
|
@@ -760,7 +1215,56 @@ export function extractHistoryOutcome(history, context) {
|
|
|
760
1215
|
latestError = message.errorMessage.trim();
|
|
761
1216
|
}
|
|
762
1217
|
}
|
|
763
|
-
return latestError ? { kind: "error", errorMessage: latestError } : null;
|
|
1218
|
+
return latestFinal ?? (latestError ? { kind: "error", errorMessage: latestError } : null);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Return all media delivery rows belonging to the matched OpenClaw turn.
|
|
1222
|
+
* A provider may emit only the first image on the live terminal while the
|
|
1223
|
+
* remaining message-tool deliveries are committed to history. This helper
|
|
1224
|
+
* keeps the user-turn boundary as the only scope and never matches by text.
|
|
1225
|
+
*/
|
|
1226
|
+
export function extractHistoryMediaContent(history, context) {
|
|
1227
|
+
const messages = history?.messages ?? [];
|
|
1228
|
+
const scope = findHistoryScope(messages, context);
|
|
1229
|
+
if (!scope || (scope.hasUser && hasUnresolvedUserBefore(messages, scope.startIndex)))
|
|
1230
|
+
return [];
|
|
1231
|
+
const media = [];
|
|
1232
|
+
const firstMessageIndex = scope.hasUser ? scope.startIndex + 1 : scope.startIndex;
|
|
1233
|
+
for (let index = firstMessageIndex; index < messages.length; index += 1) {
|
|
1234
|
+
const message = messages[index];
|
|
1235
|
+
if (message.role === "user")
|
|
1236
|
+
break;
|
|
1237
|
+
if (message.role === "assistant")
|
|
1238
|
+
media.push(...nonTextHistoryContent(message));
|
|
1239
|
+
}
|
|
1240
|
+
return media.filter((block) => typeof block.type === "string");
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Collect media rows only when the provider gives an explicit parent edge.
|
|
1244
|
+
* v3 message-tool rows remain independent canonical messages; this helper is
|
|
1245
|
+
* solely for an outgoing terminal payload that deliberately owns its child
|
|
1246
|
+
* sidecars, and never falls back to run/client/text/time matching.
|
|
1247
|
+
*/
|
|
1248
|
+
export function extractExplicitParentMediaContent(history, parentMessage) {
|
|
1249
|
+
if (!history || !parentMessage)
|
|
1250
|
+
return [];
|
|
1251
|
+
const parentMessageId = historyString(parentMessage, "messageId", "message_id", "id");
|
|
1252
|
+
if (!parentMessageId)
|
|
1253
|
+
return [];
|
|
1254
|
+
return (history.messages ?? []).flatMap((message) => {
|
|
1255
|
+
const explicitParentId = historyString(message, "parentId", "parent_id");
|
|
1256
|
+
if (explicitParentId !== parentMessageId)
|
|
1257
|
+
return [];
|
|
1258
|
+
return nonTextHistoryContent(message);
|
|
1259
|
+
}).filter((block) => typeof block.type === "string");
|
|
1260
|
+
}
|
|
1261
|
+
function nonTextHistoryContent(message) {
|
|
1262
|
+
return Array.isArray(message.content)
|
|
1263
|
+
? message.content.filter((block) => {
|
|
1264
|
+
const type = typeof block?.type === "string" ? block.type.trim().toLowerCase() : "";
|
|
1265
|
+
return ["image", "file", "audio", "voice", "video", "attachment"].includes(type);
|
|
1266
|
+
})
|
|
1267
|
+
: [];
|
|
764
1268
|
}
|
|
765
1269
|
function hasUnresolvedUserBefore(messages, userIndex) {
|
|
766
1270
|
for (let index = userIndex - 1; index >= 0; index -= 1) {
|
|
@@ -842,10 +1346,10 @@ function isToolOnlyHistoryBlockType(type) {
|
|
|
842
1346
|
|| normalizedType === "computercall"
|
|
843
1347
|
|| normalizedType === "computercalloutput";
|
|
844
1348
|
}
|
|
845
|
-
function
|
|
1349
|
+
function findHistoryScope(messages, context) {
|
|
846
1350
|
const canonicalRunId = normalizeTranscriptTurnId(context.canonicalRunId);
|
|
847
1351
|
if (!canonicalRunId) {
|
|
848
|
-
return
|
|
1352
|
+
return undefined;
|
|
849
1353
|
}
|
|
850
1354
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
851
1355
|
const message = messages[index];
|
|
@@ -859,9 +1363,26 @@ function findHistoryUserIndex(messages, context) {
|
|
|
859
1363
|
message.runId,
|
|
860
1364
|
].map(normalizeTranscriptTurnId);
|
|
861
1365
|
if (stableIds.includes(canonicalRunId)) {
|
|
862
|
-
return index;
|
|
1366
|
+
return { startIndex: index, hasUser: true };
|
|
863
1367
|
}
|
|
864
1368
|
}
|
|
865
|
-
|
|
1369
|
+
// `chat.history` is paginated, and a busy OpenClaw turn can append more
|
|
1370
|
+
// than the live enrichment page size after its user row (tool calls,
|
|
1371
|
+
// delivery mirrors, and the final commentary). The assistant delivery rows
|
|
1372
|
+
// still carry the authoritative run/turn id, so use the first matching row
|
|
1373
|
+
// as the scope boundary when the user row is outside this page. This is a
|
|
1374
|
+
// stable identity-based reconciliation, not a text or timestamp heuristic.
|
|
1375
|
+
const firstRunMessageIndex = messages.findIndex((message) => {
|
|
1376
|
+
const stableIds = [
|
|
1377
|
+
message.runId,
|
|
1378
|
+
message.turnId,
|
|
1379
|
+
message.idempotencyKey,
|
|
1380
|
+
message.clientMessageId,
|
|
1381
|
+
].map(normalizeTranscriptTurnId);
|
|
1382
|
+
return stableIds.includes(canonicalRunId);
|
|
1383
|
+
});
|
|
1384
|
+
return firstRunMessageIndex >= 0
|
|
1385
|
+
? { startIndex: firstRunMessageIndex, hasUser: false }
|
|
1386
|
+
: undefined;
|
|
866
1387
|
}
|
|
867
1388
|
//# sourceMappingURL=chat-history.js.map
|