clawconnect-agent 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/README.zh-CN.md +7 -3
- package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
- package/dist/commands/hermes-file-transfer-outcome.js +40 -0
- package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
- package/dist/commands/send-file.js +39 -33
- package/dist/commands/send-file.js.map +1 -1
- package/dist/core/command-types.d.ts +5 -0
- package/dist/core/relay/chat-payload.js +32 -22
- package/dist/core/relay/chat-payload.js.map +1 -1
- package/dist/core/relay/file-upload.d.ts +1 -0
- package/dist/core/relay/file-upload.js +3 -0
- package/dist/core/relay/file-upload.js.map +1 -1
- package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
- package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
- package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
- package/dist/core/relay/office-payload.js +54 -22
- package/dist/core/relay/office-payload.js.map +1 -1
- package/dist/core/relay/source-commit.d.ts +41 -0
- package/dist/core/relay/source-commit.js +118 -0
- package/dist/core/relay/source-commit.js.map +1 -0
- package/dist/core/relay/timeline-event-builder.d.ts +6 -0
- package/dist/core/relay/timeline-event-builder.js +3 -0
- package/dist/core/relay/timeline-event-builder.js.map +1 -1
- package/dist/core/relay/timeline-event-log.d.ts +29 -0
- package/dist/core/relay/timeline-event-log.js +37 -0
- package/dist/core/relay/timeline-event-log.js.map +1 -1
- package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
- package/dist/core/relay/timeline-projection-v3.js +87 -0
- package/dist/core/relay/timeline-projection-v3.js.map +1 -0
- package/dist/hermes/hermes-relay-manager.js +3 -0
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
- package/dist/hermes/hermes-runtime-test-support.js +12 -6
- package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
- package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
- package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
- package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
- package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
- package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
- package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
- package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
- package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
- package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
- package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
- package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
- package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/openclaw/config/provider-config.js +4 -1
- package/dist/openclaw/config/provider-config.js.map +1 -1
- package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
- package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
- package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
- package/dist/openclaw/relay/assistant-media-sidecar.d.ts +19 -2
- package/dist/openclaw/relay/assistant-media-sidecar.js +131 -15
- package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
- package/dist/openclaw/relay/chat-history.d.ts +37 -2
- package/dist/openclaw/relay/chat-history.js +565 -51
- package/dist/openclaw/relay/chat-history.js.map +1 -1
- package/dist/openclaw/relay/chat-run-identity.js +3 -1
- package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
- package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
- package/dist/openclaw/relay/gateway-history-projection.js +67 -0
- package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
- package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
- package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
- package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
- package/dist/openclaw/relay/outgoing-media-relay.d.ts +30 -3
- package/dist/openclaw/relay/outgoing-media-relay.js +407 -118
- package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
- package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
- package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
- package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
- package/dist/openclaw/relay/session-context.d.ts +15 -0
- package/dist/openclaw/relay/session-context.js +48 -4
- package/dist/openclaw/relay/session-context.js.map +1 -1
- package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
- package/dist/openclaw/relay/source-commit-projection.js +140 -0
- package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
- package/dist/openclaw/relay/transcript-dedupe.js +11 -42
- package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
- package/dist/openclaw/relay-manager.js +323 -135
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/openclaw/runtime/local-runtime.js +4 -2
- package/dist/openclaw/runtime/local-runtime.js.map +1 -1
- package/package.json +1 -1
- package/dist/openclaw/session-store.d.ts +0 -6
- package/dist/openclaw/session-store.js +0 -231
- package/dist/openclaw/session-store.js.map +0 -1
|
@@ -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";
|
|
7
|
+
import { canonicalizeOpenClawSessionScope, resolveOpenClawSessionTranscript, sessionKeyCandidates, } from "./session-context.js";
|
|
8
|
+
import { resolveOpenClawStateDir } from "../runtime/openclaw-paths.js";
|
|
5
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,25 +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) {
|
|
330
|
+
const projectionGatewayId = request.projectionVersion === 3
|
|
331
|
+
? requireProjectionIdentity(request.projectionGatewayId, "gatewayId")
|
|
332
|
+
: request.projectionGatewayId ?? "clawconnect";
|
|
106
333
|
const requestedSessionKey = cleanHistoryString(history.sessionKey) ?? request.sessionKey;
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
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
|
|
110
346
|
? { ...history, messages: automaticMediaReplies.messages }
|
|
111
347
|
: history;
|
|
112
348
|
const filtered = filterOpenClawHeartbeatHistoryResponse(normalizedHistory);
|
|
113
|
-
if (filtered.timelineSnapshot) {
|
|
349
|
+
if (filtered.timelineSnapshot && projectionVersion !== 3) {
|
|
114
350
|
return filtered;
|
|
115
351
|
}
|
|
116
|
-
const sessionKey = cleanHistoryString(filtered.sessionKey) ?? request.sessionKey;
|
|
352
|
+
const sessionKey = canonicalizeOpenClawSessionScope(cleanHistoryString(filtered.sessionKey) ?? request.sessionKey, request.sessionDefaults) ?? (cleanHistoryString(filtered.sessionKey) ?? request.sessionKey);
|
|
117
353
|
const messages = filtered.messages ?? [];
|
|
118
|
-
const
|
|
119
|
-
|
|
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));
|
|
120
364
|
return {
|
|
121
365
|
...filtered,
|
|
122
366
|
sessionKey,
|
|
367
|
+
...(projectionVersion === 3 ? { projectionVersion: 3 } : {}),
|
|
123
368
|
timelineSnapshot: buildHistorySnapshotPage({
|
|
124
|
-
gatewayId:
|
|
369
|
+
gatewayId: projectionGatewayId,
|
|
125
370
|
sessionKey,
|
|
126
371
|
cursor: request.cursor ?? null,
|
|
127
372
|
hasMore: Boolean(filtered.hasMore),
|
|
@@ -129,12 +374,18 @@ export function canonicalizeOpenClawGatewayHistoryResponse(history, request) {
|
|
|
129
374
|
newestCursor: filtered.newestCursor ?? null,
|
|
130
375
|
orderPolicy: "transcript",
|
|
131
376
|
...(sourceOrderScope ? { sourceOrderScope } : {}),
|
|
132
|
-
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
|
+
}),
|
|
133
384
|
attachments: [],
|
|
134
385
|
}),
|
|
135
386
|
};
|
|
136
387
|
}
|
|
137
|
-
function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
388
|
+
function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey, projection) {
|
|
138
389
|
const finalAssistantIndexByRunId = new Map();
|
|
139
390
|
messages.forEach((message, index) => {
|
|
140
391
|
if (normalizeTimelineRole(message.role) !== "assistant")
|
|
@@ -144,7 +395,9 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
144
395
|
finalAssistantIndexByRunId.set(runId, index);
|
|
145
396
|
});
|
|
146
397
|
return messages.map((message, index) => {
|
|
147
|
-
const seq =
|
|
398
|
+
const seq = projection?.projectionVersion === 3
|
|
399
|
+
? requireProjectionSequence(messageSeq(message), index)
|
|
400
|
+
: messageSeq(message) ?? index + 1;
|
|
148
401
|
const role = normalizeTimelineRole(message.role);
|
|
149
402
|
const rawIdempotencyKey = historyString(message, "idempotencyKey", "idempotency_key")
|
|
150
403
|
?? openClawHistoryString(message, "idempotencyKey", "idempotency_key");
|
|
@@ -153,6 +406,9 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
153
406
|
?? normalizeTranscriptTurnId(clientMessageId);
|
|
154
407
|
const providerMessageId = historyString(message, "messageId", "message_id", "id")
|
|
155
408
|
?? openClawHistoryString(message, "id");
|
|
409
|
+
const sourceMessageId = projection?.projectionVersion === 3
|
|
410
|
+
? requireProjectionIdentity(providerMessageId, "sourceMessageId")
|
|
411
|
+
: providerMessageId;
|
|
156
412
|
const runId = historyString(message, "runId", "run_id")
|
|
157
413
|
?? openClawRunId(message)
|
|
158
414
|
?? normalizedInputId;
|
|
@@ -178,11 +434,32 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
178
434
|
toolCallId,
|
|
179
435
|
index,
|
|
180
436
|
finalAssistantIndexByRunId,
|
|
437
|
+
projectionVersion: projection?.projectionVersion,
|
|
181
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;
|
|
182
459
|
return {
|
|
183
460
|
turnId,
|
|
184
461
|
runId: runId ?? turnId,
|
|
185
|
-
messageId,
|
|
462
|
+
messageId: timelineMessageId,
|
|
186
463
|
role,
|
|
187
464
|
messageState: normalizeTimelineMessageState(message),
|
|
188
465
|
createdAt: normalizeTimelineCreatedAt(message) ?? fallbackTimelineCreatedAt(messages, index),
|
|
@@ -195,6 +472,7 @@ function canonicalizeOpenClawGatewayHistoryMessages(messages, sessionKey) {
|
|
|
195
472
|
...(extractAttachmentIds(canonicalContent).length > 0
|
|
196
473
|
? { attachmentIds: extractAttachmentIds(canonicalContent) }
|
|
197
474
|
: {}),
|
|
475
|
+
...(projectionMetadata ?? {}),
|
|
198
476
|
};
|
|
199
477
|
});
|
|
200
478
|
}
|
|
@@ -237,11 +515,16 @@ function normalizeTranscriptHistoryParams(rawParams, fallbackSessionKey) {
|
|
|
237
515
|
limit: normalizeHistoryLimit(record.limit),
|
|
238
516
|
...(cursor ? { cursor } : {}),
|
|
239
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
|
+
: {}),
|
|
240
522
|
};
|
|
241
523
|
}
|
|
242
|
-
async function readIndexedTranscriptMessages(transcriptPath, sessionKey) {
|
|
524
|
+
async function readIndexedTranscriptMessages(transcriptPath, sessionKey, projectionVersion, sessionDefaults) {
|
|
243
525
|
const stats = await stat(transcriptPath);
|
|
244
|
-
const
|
|
526
|
+
const cacheKey = `${transcriptPath}\u0000${projectionVersion === 3 ? "v3" : "v2"}`;
|
|
527
|
+
const cached = transcriptHistoryCache.get(cacheKey);
|
|
245
528
|
if (cached && cached.size === stats.size && cached.mtimeMs === stats.mtimeMs) {
|
|
246
529
|
return cached.messages;
|
|
247
530
|
}
|
|
@@ -252,26 +535,157 @@ async function readIndexedTranscriptMessages(transcriptPath, sessionKey) {
|
|
|
252
535
|
if (!trimmed) {
|
|
253
536
|
continue;
|
|
254
537
|
}
|
|
255
|
-
const message = parseTranscriptHistoryLine(trimmed, messages.length + 1);
|
|
538
|
+
const message = parseTranscriptHistoryLine(trimmed, messages.length + 1, projectionVersion === 3);
|
|
256
539
|
if (message) {
|
|
257
540
|
messages.push(message);
|
|
258
541
|
}
|
|
259
542
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// the raw <run>:assistant-media key and parentId are the authoritative
|
|
263
|
-
// relationship needed to keep the desktop and mobile projections aligned.
|
|
264
|
-
const explicitSidecars = normalizeOpenClawAssistantMediaSidecars(messages, sessionKey).messages;
|
|
265
|
-
restoreTranscriptTurnLineage(explicitSidecars);
|
|
266
|
-
const foldedMessages = normalizeOpenClawAutomaticMediaReplies(explicitSidecars, sessionKey).messages;
|
|
267
|
-
const visibleMessages = filterOpenClawHeartbeatArtifacts(foldedMessages);
|
|
268
|
-
transcriptHistoryCache.set(transcriptPath, {
|
|
543
|
+
const visibleMessages = prepareTranscriptHistoryMessages(messages, sessionKey, projectionVersion, sessionDefaults);
|
|
544
|
+
transcriptHistoryCache.set(cacheKey, {
|
|
269
545
|
size: stats.size,
|
|
270
546
|
mtimeMs: stats.mtimeMs,
|
|
271
547
|
messages: visibleMessages,
|
|
272
548
|
});
|
|
273
549
|
return visibleMessages;
|
|
274
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
|
+
}
|
|
275
689
|
/**
|
|
276
690
|
* OpenClaw 会把内部心跳探测写入 transcript。只有精确的保留提示最终得到纯确认回执时,
|
|
277
691
|
* 才隐藏整段内部记录;若心跳产生真实告警,则保留完整内容供用户查看。
|
|
@@ -402,7 +816,7 @@ function resolveTextOnlyHistoryContent(content) {
|
|
|
402
816
|
}
|
|
403
817
|
return { text, hasNonTextContent };
|
|
404
818
|
}
|
|
405
|
-
function parseTranscriptHistoryLine(line, seq) {
|
|
819
|
+
function parseTranscriptHistoryLine(line, seq, strictProjectionV3 = false) {
|
|
406
820
|
let parsed;
|
|
407
821
|
try {
|
|
408
822
|
parsed = JSON.parse(line);
|
|
@@ -419,6 +833,9 @@ function parseTranscriptHistoryLine(line, seq) {
|
|
|
419
833
|
}
|
|
420
834
|
const message = { ...rawMessage, seq };
|
|
421
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
|
+
}
|
|
422
839
|
const id = typeof parsed.id === "string" && parsed.id.trim().length > 0 ? parsed.id.trim() : `transcript-${seq}`;
|
|
423
840
|
message.id = id;
|
|
424
841
|
}
|
|
@@ -441,7 +858,7 @@ function parseTranscriptHistoryLine(line, seq) {
|
|
|
441
858
|
* idempotency key through that chain so history and realtime resolve to the
|
|
442
859
|
* same canonical message without comparing text or timestamps.
|
|
443
860
|
*/
|
|
444
|
-
function restoreTranscriptTurnLineage(messages) {
|
|
861
|
+
function restoreTranscriptTurnLineage(messages, projectionVersion) {
|
|
445
862
|
const byId = new Map();
|
|
446
863
|
const lastAssistantByMobileTurn = new Map();
|
|
447
864
|
for (const message of messages) {
|
|
@@ -482,8 +899,10 @@ function restoreTranscriptTurnLineage(messages) {
|
|
|
482
899
|
}
|
|
483
900
|
// 一个 OpenClaw turn 可能包含若干工具中间消息;只有最后一个 assistant 输出
|
|
484
901
|
// 与实时 final 共用 canonical messageId,其他中间消息继续保留 transcript 身份。
|
|
485
|
-
|
|
486
|
-
message.
|
|
902
|
+
if (projectionVersion !== 3) {
|
|
903
|
+
for (const [turnId, message] of lastAssistantByMobileTurn.entries()) {
|
|
904
|
+
message.messageId = `assistant-${turnId}`;
|
|
905
|
+
}
|
|
487
906
|
}
|
|
488
907
|
}
|
|
489
908
|
function normalizeTranscriptTurnId(value) {
|
|
@@ -555,6 +974,18 @@ function normalizeHistoryLimit(value) {
|
|
|
555
974
|
function normalizeHistoryDirection(value) {
|
|
556
975
|
return value === "newer" ? "newer" : "older";
|
|
557
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
|
+
}
|
|
558
989
|
function normalizeCursor(value) {
|
|
559
990
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
560
991
|
}
|
|
@@ -736,20 +1167,30 @@ function historyNumber(record, ...fields) {
|
|
|
736
1167
|
function isRecord(value) {
|
|
737
1168
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
738
1169
|
}
|
|
739
|
-
export function extractHistoryOutcome(history, context) {
|
|
1170
|
+
export function extractHistoryOutcome(history, context, preferredText) {
|
|
740
1171
|
const messages = history?.messages ?? [];
|
|
741
1172
|
if (messages.length === 0) {
|
|
742
1173
|
return null;
|
|
743
1174
|
}
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
1175
|
+
const scope = findHistoryScope(messages, context);
|
|
1176
|
+
if (!scope) {
|
|
746
1177
|
return null;
|
|
747
1178
|
}
|
|
748
|
-
if (hasUnresolvedUserBefore(messages,
|
|
1179
|
+
if (scope.hasUser && hasUnresolvedUserBefore(messages, scope.startIndex)) {
|
|
749
1180
|
return null;
|
|
750
1181
|
}
|
|
751
1182
|
let latestError = null;
|
|
752
|
-
|
|
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) {
|
|
753
1194
|
const message = messages[index];
|
|
754
1195
|
if (message.role === "user") {
|
|
755
1196
|
return null;
|
|
@@ -758,8 +1199,15 @@ export function extractHistoryOutcome(history, context) {
|
|
|
758
1199
|
continue;
|
|
759
1200
|
}
|
|
760
1201
|
const text = extractHistoryMessageText(message);
|
|
761
|
-
|
|
762
|
-
|
|
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;
|
|
763
1211
|
}
|
|
764
1212
|
if (typeof message.errorMessage === "string" &&
|
|
765
1213
|
message.errorMessage.trim().length > 0 &&
|
|
@@ -767,7 +1215,56 @@ export function extractHistoryOutcome(history, context) {
|
|
|
767
1215
|
latestError = message.errorMessage.trim();
|
|
768
1216
|
}
|
|
769
1217
|
}
|
|
770
|
-
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
|
+
: [];
|
|
771
1268
|
}
|
|
772
1269
|
function hasUnresolvedUserBefore(messages, userIndex) {
|
|
773
1270
|
for (let index = userIndex - 1; index >= 0; index -= 1) {
|
|
@@ -849,10 +1346,10 @@ function isToolOnlyHistoryBlockType(type) {
|
|
|
849
1346
|
|| normalizedType === "computercall"
|
|
850
1347
|
|| normalizedType === "computercalloutput";
|
|
851
1348
|
}
|
|
852
|
-
function
|
|
1349
|
+
function findHistoryScope(messages, context) {
|
|
853
1350
|
const canonicalRunId = normalizeTranscriptTurnId(context.canonicalRunId);
|
|
854
1351
|
if (!canonicalRunId) {
|
|
855
|
-
return
|
|
1352
|
+
return undefined;
|
|
856
1353
|
}
|
|
857
1354
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
858
1355
|
const message = messages[index];
|
|
@@ -866,9 +1363,26 @@ function findHistoryUserIndex(messages, context) {
|
|
|
866
1363
|
message.runId,
|
|
867
1364
|
].map(normalizeTranscriptTurnId);
|
|
868
1365
|
if (stableIds.includes(canonicalRunId)) {
|
|
869
|
-
return index;
|
|
1366
|
+
return { startIndex: index, hasUser: true };
|
|
870
1367
|
}
|
|
871
1368
|
}
|
|
872
|
-
|
|
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;
|
|
873
1387
|
}
|
|
874
1388
|
//# sourceMappingURL=chat-history.js.map
|