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.
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 +1 -0
  12. package/dist/core/relay/file-upload.js +3 -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 +19 -2
  75. package/dist/openclaw/relay/assistant-media-sidecar.js +131 -15
  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 +565 -51
  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 +30 -3
  95. package/dist/openclaw/relay/outgoing-media-relay.js +407 -118
  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 +323 -135
  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";
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
- 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,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 explicitSidecars = normalizeOpenClawAssistantMediaSidecars(history.messages ?? [], requestedSessionKey);
108
- const automaticMediaReplies = normalizeOpenClawAutomaticMediaReplies(explicitSidecars.messages, requestedSessionKey);
109
- const normalizedHistory = explicitSidecars.changed || automaticMediaReplies.changed
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 sourceOrderScope = cleanHistoryString(filtered.sessionId)
119
- ?? 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));
120
364
  return {
121
365
  ...filtered,
122
366
  sessionKey,
367
+ ...(projectionVersion === 3 ? { projectionVersion: 3 } : {}),
123
368
  timelineSnapshot: buildHistorySnapshotPage({
124
- gatewayId: "clawconnect",
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 = messageSeq(message) ?? index + 1;
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 cached = transcriptHistoryCache.get(transcriptPath);
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
- // Fold the automatic assistant-media sidecar before lineage reconstruction.
261
- // Lineage deliberately rewrites idempotency keys to mobile turn IDs, while
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
- for (const [turnId, message] of lastAssistantByMobileTurn.entries()) {
486
- message.messageId = `assistant-${turnId}`;
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 userIndex = findHistoryUserIndex(messages, context);
745
- if (userIndex === -1) {
1175
+ const scope = findHistoryScope(messages, context);
1176
+ if (!scope) {
746
1177
  return null;
747
1178
  }
748
- if (hasUnresolvedUserBefore(messages, userIndex)) {
1179
+ if (scope.hasUser && hasUnresolvedUserBefore(messages, scope.startIndex)) {
749
1180
  return null;
750
1181
  }
751
1182
  let latestError = null;
752
- 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) {
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
- if (text.length > 0 || hasHistoryMessageContent(message)) {
762
- 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;
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 findHistoryUserIndex(messages, context) {
1349
+ function findHistoryScope(messages, context) {
853
1350
  const canonicalRunId = normalizeTranscriptTurnId(context.canonicalRunId);
854
1351
  if (!canonicalRunId) {
855
- return -1;
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
- 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;
873
1387
  }
874
1388
  //# sourceMappingURL=chat-history.js.map