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.
Files changed (118) hide show
  1. package/README.md +7 -3
  2. package/README.zh-CN.md +7 -3
  3. package/dist/commands/hermes-file-transfer-outcome.d.ts +18 -0
  4. package/dist/commands/hermes-file-transfer-outcome.js +40 -0
  5. package/dist/commands/hermes-file-transfer-outcome.js.map +1 -0
  6. package/dist/commands/send-file.js +39 -33
  7. package/dist/commands/send-file.js.map +1 -1
  8. package/dist/core/command-types.d.ts +5 -0
  9. package/dist/core/relay/chat-payload.js +32 -22
  10. package/dist/core/relay/chat-payload.js.map +1 -1
  11. package/dist/core/relay/file-upload.d.ts +3 -0
  12. package/dist/core/relay/file-upload.js +4 -0
  13. package/dist/core/relay/file-upload.js.map +1 -1
  14. package/dist/core/relay/mobile-chat-run-bridge.d.ts +53 -8
  15. package/dist/core/relay/mobile-chat-run-bridge.js +45 -0
  16. package/dist/core/relay/mobile-chat-run-bridge.js.map +1 -1
  17. package/dist/core/relay/office-payload.js +54 -22
  18. package/dist/core/relay/office-payload.js.map +1 -1
  19. package/dist/core/relay/source-commit.d.ts +41 -0
  20. package/dist/core/relay/source-commit.js +118 -0
  21. package/dist/core/relay/source-commit.js.map +1 -0
  22. package/dist/core/relay/timeline-event-builder.d.ts +6 -0
  23. package/dist/core/relay/timeline-event-builder.js +3 -0
  24. package/dist/core/relay/timeline-event-builder.js.map +1 -1
  25. package/dist/core/relay/timeline-event-log.d.ts +29 -0
  26. package/dist/core/relay/timeline-event-log.js +37 -0
  27. package/dist/core/relay/timeline-event-log.js.map +1 -1
  28. package/dist/core/relay/timeline-projection-v3.d.ts +63 -0
  29. package/dist/core/relay/timeline-projection-v3.js +87 -0
  30. package/dist/core/relay/timeline-projection-v3.js.map +1 -0
  31. package/dist/hermes/hermes-relay-manager.js +3 -0
  32. package/dist/hermes/hermes-relay-manager.js.map +1 -1
  33. package/dist/hermes/hermes-runtime-test-support.d.ts +3 -3
  34. package/dist/hermes/hermes-runtime-test-support.js +12 -6
  35. package/dist/hermes/hermes-runtime-test-support.js.map +1 -1
  36. package/dist/hermes/runtime/hermes-file-transfer-state.d.ts +33 -0
  37. package/dist/hermes/runtime/hermes-file-transfer-state.js +139 -0
  38. package/dist/hermes/runtime/hermes-file-transfer-state.js.map +1 -0
  39. package/dist/hermes/runtime/hermes-mobile-file-route-store.d.ts +17 -0
  40. package/dist/hermes/runtime/hermes-mobile-file-route-store.js +103 -0
  41. package/dist/hermes/runtime/hermes-mobile-file-route-store.js.map +1 -0
  42. package/dist/hermes/runtime/hermes-runtime-api-client.d.ts +1 -0
  43. package/dist/hermes/runtime/hermes-runtime-api-client.js +16 -0
  44. package/dist/hermes/runtime/hermes-runtime-api-client.js.map +1 -1
  45. package/dist/hermes/runtime/hermes-runtime-chat.d.ts +35 -4
  46. package/dist/hermes/runtime/hermes-runtime-chat.js +426 -51
  47. package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
  48. package/dist/hermes/runtime/hermes-runtime-command-router.js +9 -1
  49. package/dist/hermes/runtime/hermes-runtime-command-router.js.map +1 -1
  50. package/dist/hermes/runtime/hermes-runtime-cron.d.ts +1 -0
  51. package/dist/hermes/runtime/hermes-runtime-cron.js +34 -66
  52. package/dist/hermes/runtime/hermes-runtime-cron.js.map +1 -1
  53. package/dist/hermes/runtime/hermes-runtime-history-completion.d.ts +8 -1
  54. package/dist/hermes/runtime/hermes-runtime-history-completion.js +112 -41
  55. package/dist/hermes/runtime/hermes-runtime-history-completion.js.map +1 -1
  56. package/dist/hermes/runtime/hermes-runtime-history.js +92 -17
  57. package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
  58. package/dist/hermes/runtime/hermes-runtime-models.js +4 -1
  59. package/dist/hermes/runtime/hermes-runtime-models.js.map +1 -1
  60. package/dist/hermes/runtime/hermes-runtime-process.js +4 -3
  61. package/dist/hermes/runtime/hermes-runtime-process.js.map +1 -1
  62. package/dist/hermes/runtime/hermes-runtime-state-db.d.ts +6 -0
  63. package/dist/hermes/runtime/hermes-runtime-state-db.js +20 -1
  64. package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
  65. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js +12 -20
  66. package/dist/hermes/runtime/hermes-runtime-tool-log-watcher.js.map +1 -1
  67. package/dist/index.js +16 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/openclaw/config/provider-config.js +4 -1
  70. package/dist/openclaw/config/provider-config.js.map +1 -1
  71. package/dist/openclaw/relay/agent-identity-helpers.d.ts +9 -0
  72. package/dist/openclaw/relay/agent-identity-helpers.js +118 -0
  73. package/dist/openclaw/relay/agent-identity-helpers.js.map +1 -0
  74. package/dist/openclaw/relay/assistant-media-sidecar.d.ts +28 -1
  75. package/dist/openclaw/relay/assistant-media-sidecar.js +270 -10
  76. package/dist/openclaw/relay/assistant-media-sidecar.js.map +1 -1
  77. package/dist/openclaw/relay/chat-history.d.ts +37 -2
  78. package/dist/openclaw/relay/chat-history.js +570 -49
  79. package/dist/openclaw/relay/chat-history.js.map +1 -1
  80. package/dist/openclaw/relay/chat-run-identity.js +3 -1
  81. package/dist/openclaw/relay/chat-run-identity.js.map +1 -1
  82. package/dist/openclaw/relay/gateway-history-projection.d.ts +4 -0
  83. package/dist/openclaw/relay/gateway-history-projection.js +67 -0
  84. package/dist/openclaw/relay/gateway-history-projection.js.map +1 -0
  85. package/dist/openclaw/relay/openclaw-active-run-state.d.ts +67 -0
  86. package/dist/openclaw/relay/openclaw-active-run-state.js +283 -0
  87. package/dist/openclaw/relay/openclaw-active-run-state.js.map +1 -0
  88. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.d.ts +12 -0
  89. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js +48 -0
  90. package/dist/openclaw/relay/openclaw-message-tool-real-shape.fixture.js.map +1 -0
  91. package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +22 -0
  92. package/dist/openclaw/relay/openclaw-source-commit-watcher.js +64 -0
  93. package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -0
  94. package/dist/openclaw/relay/outgoing-media-relay.d.ts +35 -1
  95. package/dist/openclaw/relay/outgoing-media-relay.js +541 -123
  96. package/dist/openclaw/relay/outgoing-media-relay.js.map +1 -1
  97. package/dist/openclaw/relay/relay-manager-history-timing.d.ts +1 -4
  98. package/dist/openclaw/relay/relay-manager-history-timing.js +4 -4
  99. package/dist/openclaw/relay/relay-manager-history-timing.js.map +1 -1
  100. package/dist/openclaw/relay/relay-manager-payload-helpers.d.ts +28 -1
  101. package/dist/openclaw/relay/relay-manager-payload-helpers.js +219 -11
  102. package/dist/openclaw/relay/relay-manager-payload-helpers.js.map +1 -1
  103. package/dist/openclaw/relay/session-context.d.ts +15 -0
  104. package/dist/openclaw/relay/session-context.js +48 -4
  105. package/dist/openclaw/relay/session-context.js.map +1 -1
  106. package/dist/openclaw/relay/source-commit-projection.d.ts +25 -0
  107. package/dist/openclaw/relay/source-commit-projection.js +140 -0
  108. package/dist/openclaw/relay/source-commit-projection.js.map +1 -0
  109. package/dist/openclaw/relay/transcript-dedupe.js +11 -42
  110. package/dist/openclaw/relay/transcript-dedupe.js.map +1 -1
  111. package/dist/openclaw/relay-manager.js +329 -131
  112. package/dist/openclaw/relay-manager.js.map +1 -1
  113. package/dist/openclaw/runtime/local-runtime.js +4 -2
  114. package/dist/openclaw/runtime/local-runtime.js.map +1 -1
  115. package/package.json +1 -1
  116. package/dist/openclaw/session-store.d.ts +0 -6
  117. package/dist/openclaw/session-store.js +0 -231
  118. 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 { normalizeOpenClawAssistantMediaSidecars } from "./assistant-media-sidecar.js";
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
- const limit = normalizeHistoryLimit(request.limit);
33
- const direction = normalizeHistoryDirection(request.direction);
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 page = paginateHistoryMessages(messages, {
36
- limit,
37
- direction,
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: "clawconnect",
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
- ...(request.sessionId ? { sourceOrderScope: request.sessionId } : {}),
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 = messageSeq(message) ?? index + 1;
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: messageId ?? `${role}-${turnId}`,
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 filtered = filterOpenClawHeartbeatHistoryResponse(history);
107
- if (filtered.timelineSnapshot) {
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 sourceOrderScope = cleanHistoryString(filtered.sessionId)
113
- ?? messages.map(openClawTranscriptSource).find((value) => Boolean(value));
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: "clawconnect",
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 = messageSeq(message) ?? index + 1;
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 cached = transcriptHistoryCache.get(transcriptPath);
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
- // Fold the automatic assistant-media sidecar before lineage reconstruction.
255
- // Lineage deliberately rewrites idempotency keys to mobile turn IDs, while
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
- for (const [turnId, message] of lastAssistantByMobileTurn.entries()) {
479
- message.messageId = `assistant-${turnId}`;
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 userIndex = findHistoryUserIndex(messages, context);
738
- if (userIndex === -1) {
1175
+ const scope = findHistoryScope(messages, context);
1176
+ if (!scope) {
739
1177
  return null;
740
1178
  }
741
- if (hasUnresolvedUserBefore(messages, userIndex)) {
1179
+ if (scope.hasUser && hasUnresolvedUserBefore(messages, scope.startIndex)) {
742
1180
  return null;
743
1181
  }
744
1182
  let latestError = null;
745
- for (let index = userIndex + 1; index < messages.length; index += 1) {
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
- if (text.length > 0 || hasHistoryMessageContent(message)) {
755
- return { kind: "final", text, message };
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 findHistoryUserIndex(messages, context) {
1349
+ function findHistoryScope(messages, context) {
846
1350
  const canonicalRunId = normalizeTranscriptTurnId(context.canonicalRunId);
847
1351
  if (!canonicalRunId) {
848
- return -1;
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
- return -1;
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