@agent-native/core 0.203.0-nightly-20261007064822 → 0.203.0

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 (63) hide show
  1. package/dist/agent/engine/builder-engine.d.ts +1 -1
  2. package/dist/agent/model-config.d.ts +2 -2
  3. package/dist/agent/model-config.js +0 -2
  4. package/dist/agent/thread-data-builder.d.ts +6 -0
  5. package/dist/agent/thread-data-builder.js +747 -25
  6. package/dist/agent/thread-message-projection.d.ts +20 -0
  7. package/dist/agent/thread-message-projection.js +300 -0
  8. package/dist/automation/index.d.ts +1 -1
  9. package/dist/chat-threads/store.d.ts +4 -1
  10. package/dist/chat-threads/store.js +92 -17
  11. package/dist/cli/skills-content/turn-into-app-skill.d.ts +10 -4
  12. package/dist/cli/skills-content/turn-into-app-skill.js +2454 -705
  13. package/dist/cli/skills.d.ts +6 -0
  14. package/dist/cli/skills.js +8 -2
  15. package/dist/client/chat/agentkit-agent-native.js +839 -89
  16. package/dist/client/chat/agentkit-protocol.js +67 -17
  17. package/dist/collab/routes.d.ts +2 -2
  18. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  19. package/dist/mcp/build-server.d.ts +5 -1
  20. package/dist/mcp/build-server.js +56 -13
  21. package/dist/mcp/embed-app.js +6 -1
  22. package/dist/notifications/routes.d.ts +1 -1
  23. package/dist/observability/routes.d.ts +3 -3
  24. package/dist/org/index.d.ts +2 -2
  25. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  26. package/dist/resources/handlers.d.ts +1 -1
  27. package/dist/server/agent-chat/mcp-options.d.ts +4 -0
  28. package/dist/server/agent-chat-plugin.js +97 -22
  29. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  30. package/dist/server/onboarding-html.js +1 -1
  31. package/dist/server/realtime-token.d.ts +1 -1
  32. package/dist/server/transcribe-voice.d.ts +1 -1
  33. package/dist/shared/embed-auth.js +5 -4
  34. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  35. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +26 -24
  36. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/deployment-recovery.md +53 -0
  37. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/documentation-proof.md +7 -0
  38. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +284 -461
  39. package/dist/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +2 -2
  40. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +65 -90
  41. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/local-run-and-deploy.md +161 -0
  42. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/review-loop.md +384 -0
  43. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/source-brief.md +116 -0
  44. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/spreadsheet-source.md +206 -190
  45. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/ui-archetypes.md +381 -0
  46. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/ui-direction.md +323 -0
  47. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/ui-palettes.md +564 -0
  48. package/dist/triggers/actions/manage-automation.d.ts +7 -7
  49. package/docs/content/observability.mdx +136 -0
  50. package/package.json +4 -4
  51. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +26 -24
  52. package/src/templates/factory/.agents/skills/review-latest-feedback/references/deployment-recovery.md +53 -0
  53. package/src/templates/factory/.agents/skills/review-latest-feedback/references/documentation-proof.md +7 -0
  54. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +284 -461
  55. package/src/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +2 -2
  56. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +65 -90
  57. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/local-run-and-deploy.md +161 -0
  58. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/review-loop.md +384 -0
  59. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/source-brief.md +116 -0
  60. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/spreadsheet-source.md +206 -190
  61. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/ui-archetypes.md +381 -0
  62. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/ui-direction.md +323 -0
  63. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/ui-palettes.md +564 -0
@@ -0,0 +1,20 @@
1
+ export declare function threadMessageRecord(value: unknown): Record<string, unknown> | undefined;
2
+ /** AgentKit messages that represent durable root assistant messages. */
3
+ export declare function projectRootAssistantMessages(input: {
4
+ rootMessages: unknown;
5
+ snapshotMessages: unknown;
6
+ events?: unknown;
7
+ runs?: unknown;
8
+ toolCalls?: unknown;
9
+ }): {
10
+ representedRootMessageIds: Set<string>;
11
+ snapshotMessageIdsByRootMessageId: Map<string, string>;
12
+ };
13
+ /** Root assistant IDs represented by the AgentKit snapshot. */
14
+ export declare function representedRootAssistantMessageIds(input: {
15
+ rootMessages: unknown;
16
+ snapshotMessages: unknown;
17
+ events?: unknown;
18
+ runs?: unknown;
19
+ toolCalls?: unknown;
20
+ }): Set<string>;
@@ -0,0 +1,300 @@
1
+ const REPOSITORY_PART_MEDIA_TYPE = "application/x-agent-native-repository-part";
2
+ function record(value) {
3
+ return value && typeof value === "object" && !Array.isArray(value)
4
+ ? value
5
+ : undefined;
6
+ }
7
+ function message(value) {
8
+ const outer = record(value);
9
+ return record(outer?.message) ?? outer;
10
+ }
11
+ export function threadMessageRecord(value) {
12
+ return message(value);
13
+ }
14
+ function parts(value) {
15
+ const content = value.parts ?? value.content;
16
+ const values = Array.isArray(content)
17
+ ? content
18
+ : typeof content === "string"
19
+ ? [{ type: "text", text: content }]
20
+ : [];
21
+ return values.flatMap((part) => {
22
+ const entry = record(part);
23
+ if (!entry)
24
+ return [];
25
+ return entry.type === "tool-call"
26
+ ? [
27
+ {
28
+ type: "data",
29
+ mediaType: REPOSITORY_PART_MEDIA_TYPE,
30
+ data: entry,
31
+ },
32
+ ]
33
+ : [entry];
34
+ });
35
+ }
36
+ function explicitRunId(value) {
37
+ const runId = record(value.metadata)?.runId;
38
+ return typeof runId === "string" ? runId : undefined;
39
+ }
40
+ function foldedRunIds(value) {
41
+ const custom = record(record(value.metadata)?.custom);
42
+ const folded = Array.isArray(custom?.foldedRunIds)
43
+ ? custom.foldedRunIds.filter((runId) => typeof runId === "string")
44
+ : [];
45
+ const runId = explicitRunId(value);
46
+ return [...new Set([...folded, ...(runId ? [runId] : [])])];
47
+ }
48
+ function eventRunIds(events, runs) {
49
+ const result = new Map();
50
+ const remember = (messageId, runId) => {
51
+ if (typeof messageId !== "string" || typeof runId !== "string")
52
+ return;
53
+ const ids = result.get(messageId) ?? new Set();
54
+ ids.add(runId);
55
+ result.set(messageId, ids);
56
+ };
57
+ for (const value of Array.isArray(events) ? events : []) {
58
+ const event = record(value);
59
+ const eventMessage = record(event?.message);
60
+ if ((event?.type === "message.created" ||
61
+ event?.type === "message.completed") &&
62
+ eventMessage?.role === "assistant") {
63
+ remember(eventMessage.id ?? event?.messageId, event?.runId);
64
+ }
65
+ }
66
+ for (const value of Array.isArray(runs) ? runs : []) {
67
+ const run = record(value);
68
+ remember(run?.activeMessageId, run?.id);
69
+ }
70
+ return result;
71
+ }
72
+ function eventMessageIdsByRun(runIdsByMessageId) {
73
+ const result = new Map();
74
+ for (const [messageId, runIds] of runIdsByMessageId) {
75
+ for (const runId of runIds) {
76
+ const messageIds = result.get(runId) ?? new Set();
77
+ messageIds.add(messageId);
78
+ result.set(runId, messageIds);
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ function assistantRunId(value, runIdsByMessageId) {
84
+ if (value.role !== "assistant")
85
+ return undefined;
86
+ const eventRunIds = typeof value.id === "string" ? runIdsByMessageId.get(value.id) : undefined;
87
+ return eventRunIds?.size === 1 ? [...eventRunIds][0] : explicitRunId(value);
88
+ }
89
+ function text(value, ignoreNonTextParts = false) {
90
+ if (value.role !== "assistant")
91
+ return undefined;
92
+ const messageParts = parts(value);
93
+ if (!ignoreNonTextParts &&
94
+ messageParts.some((part) => part.type !== "text" &&
95
+ part.type !== "reasoning" &&
96
+ !(part.type === "data" &&
97
+ part.mediaType === REPOSITORY_PART_MEDIA_TYPE &&
98
+ record(part.data)?.type === "tool-call"))) {
99
+ return undefined;
100
+ }
101
+ return messageParts
102
+ .filter((part) => part.type === "text" && typeof part.text === "string")
103
+ .map((part) => part.text)
104
+ .join("");
105
+ }
106
+ function reasoning(value) {
107
+ return parts(value)
108
+ .flatMap((part) => part.type === "reasoning" && typeof part.text === "string"
109
+ ? [part.text]
110
+ : [])
111
+ .join("");
112
+ }
113
+ function isPrefixCompatible(left, right) {
114
+ return left.startsWith(right) || right.startsWith(left);
115
+ }
116
+ function rootToolCalls(value) {
117
+ return parts(value).flatMap((part) => {
118
+ if (part.type !== "data" || part.mediaType !== REPOSITORY_PART_MEDIA_TYPE) {
119
+ return [];
120
+ }
121
+ const data = record(part.data);
122
+ return data?.type === "tool-call" ? [data] : [];
123
+ });
124
+ }
125
+ function sameValue(left, right) {
126
+ if (Object.is(left, right))
127
+ return true;
128
+ if (Array.isArray(left) || Array.isArray(right)) {
129
+ return (Array.isArray(left) &&
130
+ Array.isArray(right) &&
131
+ left.length === right.length &&
132
+ left.every((value, index) => sameValue(value, right[index])));
133
+ }
134
+ const leftRecord = record(left);
135
+ const rightRecord = record(right);
136
+ if (!leftRecord || !rightRecord)
137
+ return false;
138
+ const leftKeys = Object.keys(leftRecord).sort();
139
+ const rightKeys = Object.keys(rightRecord).sort();
140
+ return (leftKeys.length === rightKeys.length &&
141
+ leftKeys.every((key, index) => key === rightKeys[index] &&
142
+ sameValue(leftRecord[key], rightRecord[key])));
143
+ }
144
+ function representedToolCallMessageIds(value, toolCalls) {
145
+ const calls = rootToolCalls(value);
146
+ const byId = new Map((Array.isArray(toolCalls) ? toolCalls : []).flatMap((entry) => {
147
+ const call = record(entry);
148
+ return typeof call?.id === "string" ? [[call.id, call]] : [];
149
+ }));
150
+ const messageIds = new Set();
151
+ for (const rootCall of calls) {
152
+ const id = typeof rootCall.toolCallId === "string"
153
+ ? rootCall.toolCallId
154
+ : typeof rootCall.id === "string"
155
+ ? rootCall.id
156
+ : undefined;
157
+ const call = id ? byId.get(id) : undefined;
158
+ if (!call ||
159
+ (typeof rootCall.toolName === "string" &&
160
+ rootCall.toolName !== call.name) ||
161
+ (Object.prototype.hasOwnProperty.call(rootCall, "args") &&
162
+ !sameValue(rootCall.args, call.input)) ||
163
+ (Object.prototype.hasOwnProperty.call(rootCall, "result") &&
164
+ !sameValue(rootCall.result, call.output))) {
165
+ return undefined;
166
+ }
167
+ if (typeof call.messageId === "string")
168
+ messageIds.add(call.messageId);
169
+ }
170
+ return messageIds;
171
+ }
172
+ function contentMatches(root, candidate, toolMessageIds) {
173
+ const rootText = text(root);
174
+ const candidateText = text(candidate);
175
+ if (rootText === undefined || candidateText === undefined)
176
+ return false;
177
+ const sameText = Boolean(rootText && candidateText && isPrefixCompatible(rootText, candidateText));
178
+ const reasoningMatches = Boolean(reasoning(root) && reasoning(root) === reasoning(candidate));
179
+ const toolCallMatch = Boolean(typeof candidate.id === "string" && toolMessageIds?.has(candidate.id));
180
+ if (rootToolCalls(root).length > 0 && toolMessageIds === undefined) {
181
+ return false;
182
+ }
183
+ return ((sameText && (rootText.length > 0 || candidateText.length > 0)) ||
184
+ (reasoningMatches && !rootText && !candidateText) ||
185
+ (toolCallMatch && !rootText && !candidateText));
186
+ }
187
+ function foldedProjectionMatches(root, snapshotMessagesByRunId, foldedIds, messageIdsByRun) {
188
+ if (foldedIds.length < 2)
189
+ return false;
190
+ const groups = foldedIds.map((runId) => (snapshotMessagesByRunId.get(runId) ?? []).filter((candidate) => candidate.id !== root.id));
191
+ const presentIndices = groups.flatMap((group, index) => group.length > 0 ? [index] : []);
192
+ if (presentIndices.length === 0)
193
+ return false;
194
+ const first = presentIndices[0];
195
+ const last = presentIndices.at(-1);
196
+ if (last - first + 1 !== presentIndices.length)
197
+ return false;
198
+ const projectedText = groups
199
+ .slice(first, last + 1)
200
+ .flatMap((group, index) => {
201
+ const runId = foldedIds[first + index];
202
+ if (group.some((candidate) => explicitRunId(candidate) !== runId &&
203
+ messageIdsByRun.get(runId)?.size !== 1)) {
204
+ return [undefined];
205
+ }
206
+ return group.map((candidate) => text(candidate, true));
207
+ });
208
+ if (projectedText.some((value) => value === undefined))
209
+ return false;
210
+ const candidateText = projectedText.join("");
211
+ const rootText = text(root);
212
+ if (!rootText || !candidateText)
213
+ return false;
214
+ const isPrefix = first === 0 && rootText.startsWith(candidateText);
215
+ const isSuffix = last === foldedIds.length - 1 && rootText.endsWith(candidateText);
216
+ return isPrefix || isSuffix;
217
+ }
218
+ /** AgentKit messages that represent durable root assistant messages. */
219
+ export function projectRootAssistantMessages(input) {
220
+ const rootMessages = Array.isArray(input.rootMessages)
221
+ ? input.rootMessages.flatMap((value) => {
222
+ const entry = message(value);
223
+ return entry ? [entry] : [];
224
+ })
225
+ : [];
226
+ const snapshotMessages = Array.isArray(input.snapshotMessages)
227
+ ? input.snapshotMessages.flatMap((value) => {
228
+ const entry = message(value);
229
+ return entry ? [entry] : [];
230
+ })
231
+ : [];
232
+ const runIdsByMessageId = eventRunIds(input.events, input.runs);
233
+ const messageIdsByRun = eventMessageIdsByRun(runIdsByMessageId);
234
+ const snapshotMessagesByRunId = new Map();
235
+ for (const candidate of snapshotMessages) {
236
+ const runId = assistantRunId(candidate, runIdsByMessageId);
237
+ if (!runId)
238
+ continue;
239
+ const candidates = snapshotMessagesByRunId.get(runId);
240
+ if (candidates)
241
+ candidates.push(candidate);
242
+ else
243
+ snapshotMessagesByRunId.set(runId, [candidate]);
244
+ }
245
+ const represented = new Set();
246
+ const directMatches = new Map();
247
+ for (const root of rootMessages) {
248
+ if (root.role !== "assistant" || typeof root.id !== "string")
249
+ continue;
250
+ const runId = explicitRunId(root);
251
+ if (!runId)
252
+ continue;
253
+ const toolMessageIds = representedToolCallMessageIds(root, input.toolCalls);
254
+ const sameRunCandidates = (snapshotMessagesByRunId.get(runId) ?? []).filter((candidate) => candidate.id !== root.id);
255
+ const sameRunMatches = sameRunCandidates.filter((candidate) => contentMatches(root, candidate, toolMessageIds));
256
+ const rootText = text(root);
257
+ const matchingCandidate = sameRunMatches[0];
258
+ const exactTextMatch = matchingCandidate !== undefined &&
259
+ rootText !== undefined &&
260
+ text(matchingCandidate) === rootText;
261
+ const clientCanMatchPartial = matchingCandidate !== undefined &&
262
+ (explicitRunId(matchingCandidate) === runId ||
263
+ (typeof matchingCandidate.id === "string" &&
264
+ runIdsByMessageId.get(matchingCandidate.id)?.size === 1 &&
265
+ messageIdsByRun.get(runId)?.size === 1));
266
+ if (sameRunMatches.length === 1 &&
267
+ (exactTextMatch ||
268
+ (sameRunCandidates.length === 1 && clientCanMatchPartial))) {
269
+ represented.add(root.id);
270
+ if (typeof matchingCandidate.id === "string") {
271
+ directMatches.set(root.id, matchingCandidate.id);
272
+ }
273
+ continue;
274
+ }
275
+ if (toolMessageIds !== undefined &&
276
+ foldedProjectionMatches(root, snapshotMessagesByRunId, foldedRunIds(root), messageIdsByRun)) {
277
+ represented.add(root.id);
278
+ }
279
+ }
280
+ const directMatchCountBySnapshotId = new Map();
281
+ for (const snapshotId of directMatches.values()) {
282
+ directMatchCountBySnapshotId.set(snapshotId, (directMatchCountBySnapshotId.get(snapshotId) ?? 0) + 1);
283
+ }
284
+ const snapshotMessageIdsByRootMessageId = new Map();
285
+ for (const [rootId, snapshotId] of directMatches) {
286
+ if (directMatchCountBySnapshotId.get(snapshotId) !== 1) {
287
+ represented.delete(rootId);
288
+ continue;
289
+ }
290
+ snapshotMessageIdsByRootMessageId.set(rootId, snapshotId);
291
+ }
292
+ return {
293
+ representedRootMessageIds: represented,
294
+ snapshotMessageIdsByRootMessageId,
295
+ };
296
+ }
297
+ /** Root assistant IDs represented by the AgentKit snapshot. */
298
+ export function representedRootAssistantMessageIds(input) {
299
+ return projectRootAssistantMessages(input).representedRootMessageIds;
300
+ }
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
- error?: undefined;
143
142
  accepted: boolean;
144
143
  duplicate: boolean;
145
144
  eventId: string;
145
+ error?: undefined;
146
146
  }>;
@@ -1,4 +1,5 @@
1
1
  import type { AgentRunOptions } from "@agent-native/agentkit/protocol";
2
+ import { type ThreadAnnotationSnapshotConflict } from "../agent/thread-data-builder.js";
2
3
  import { type AccessContext } from "../sharing/access.js";
3
4
  import { type ShareRole } from "../sharing/schema.js";
4
5
  export declare function withThreadDataLock<T>(threadId: string, fn: () => Promise<T>): Promise<T>;
@@ -180,6 +181,8 @@ export interface UpdateThreadDataOptions {
180
181
  preserveExistingQueuedMessages?: boolean;
181
182
  preserveExistingTopLevelKeys?: boolean;
182
183
  preserveCurrentMetadata?: boolean;
184
+ preserveCurrentTitleAndPreview?: boolean;
185
+ onAnnotationConflict?: (conflict: ThreadAnnotationSnapshotConflict) => void;
183
186
  transformThreadData?: (currentThreadData: string) => string | {
184
187
  threadData: string;
185
188
  preview?: string;
@@ -187,7 +190,7 @@ export interface UpdateThreadDataOptions {
187
190
  maxAttempts?: number;
188
191
  ignoreConflicts?: boolean;
189
192
  }
190
- export declare function updateThreadData(id: string, threadData: string, title: string, preview: string, messageCount: number, options?: UpdateThreadDataOptions): Promise<void>;
193
+ export declare function updateThreadData(id: string, threadData: string, title: string, preview: string, messageCount: number, options?: UpdateThreadDataOptions): Promise<boolean>;
191
194
  export interface ThreadEngineMeta {
192
195
  engineName: string;
193
196
  model: string;
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { mergeThreadDataForClientSave, normalizeThreadRepository, normalizeThreadTitle, } from "../agent/thread-data-builder.js";
3
+ import { representedRootAssistantMessageIds, threadMessageRecord, } from "../agent/thread-message-projection.js";
3
4
  import { getDbExec } from "../db/client.js";
4
5
  import { createGetDb } from "../db/create-get-db.js";
5
6
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
@@ -286,7 +287,78 @@ function countThreadMessages(value, fallback) {
286
287
  if (repoMessageCount === undefined && agentKitMessageCount === undefined) {
287
288
  return fallback;
288
289
  }
289
- return Math.max(repoMessageCount ?? 0, agentKitMessageCount ?? 0);
290
+ const rootMessages = Array.isArray(repo.messages) ? repo.messages : [];
291
+ const agentKitMessages = Array.isArray(repo.agentKit?.messages)
292
+ ? repo.agentKit.messages
293
+ : [];
294
+ const mirroredRootIds = representedRootAssistantMessageIds({
295
+ rootMessages,
296
+ snapshotMessages: agentKitMessages,
297
+ events: repo.agentKit?.events,
298
+ runs: repo.agentKit?.runs,
299
+ toolCalls: repo.agentKit?.toolCalls,
300
+ });
301
+ const agentKitUserMessageId = (entry) => {
302
+ const message = threadMessageRecord(entry);
303
+ if (message?.role !== "user")
304
+ return undefined;
305
+ const metadata = message.metadata;
306
+ const custom = metadata && typeof metadata === "object" && !Array.isArray(metadata)
307
+ ? metadata.custom
308
+ : undefined;
309
+ const agentKitMessageId = custom && typeof custom === "object" && !Array.isArray(custom)
310
+ ? custom.agentKitMessageId
311
+ : undefined;
312
+ return typeof agentKitMessageId === "string"
313
+ ? agentKitMessageId
314
+ : undefined;
315
+ };
316
+ const snapshotUserMessageCounts = new Map();
317
+ for (const entry of agentKitMessages) {
318
+ const message = threadMessageRecord(entry);
319
+ if (message?.role !== "user" || typeof message.id !== "string")
320
+ continue;
321
+ snapshotUserMessageCounts.set(message.id, (snapshotUserMessageCounts.get(message.id) ?? 0) + 1);
322
+ }
323
+ const rootUserMirrorIdsByAgentKitId = new Map();
324
+ for (const entry of rootMessages) {
325
+ const message = threadMessageRecord(entry);
326
+ const agentKitId = agentKitUserMessageId(entry);
327
+ if (typeof message?.id !== "string" || !agentKitId)
328
+ continue;
329
+ const rootIds = rootUserMirrorIdsByAgentKitId.get(agentKitId);
330
+ if (rootIds)
331
+ rootIds.push(message.id);
332
+ else
333
+ rootUserMirrorIdsByAgentKitId.set(agentKitId, [message.id]);
334
+ }
335
+ const mirroredRootUserIds = new Set();
336
+ for (const [agentKitId, rootIds] of rootUserMirrorIdsByAgentKitId) {
337
+ if (rootIds.length === 1 &&
338
+ snapshotUserMessageCounts.get(agentKitId) === 1) {
339
+ mirroredRootUserIds.add(rootIds[0]);
340
+ }
341
+ }
342
+ const messageIds = new Set();
343
+ let unkeyedMessages = 0;
344
+ const countMessage = (entry) => {
345
+ const message = threadMessageRecord(entry);
346
+ if (typeof message?.id === "string")
347
+ messageIds.add(message.id);
348
+ else
349
+ unkeyedMessages += 1;
350
+ };
351
+ for (const entry of rootMessages) {
352
+ const message = threadMessageRecord(entry);
353
+ if (typeof message?.id === "string" &&
354
+ (mirroredRootIds.has(message.id) || mirroredRootUserIds.has(message.id))) {
355
+ continue;
356
+ }
357
+ countMessage(entry);
358
+ }
359
+ for (const entry of agentKitMessages)
360
+ countMessage(entry);
361
+ return messageIds.size + unkeyedMessages;
290
362
  }
291
363
  function forkThreadData(threadData, forkId, fromMessageId) {
292
364
  const parsed = normalizeThreadRepository(JSON.parse(threadData));
@@ -1010,32 +1082,31 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
1010
1082
  try {
1011
1083
  const current = await getThread(id);
1012
1084
  if (!current)
1013
- return;
1085
+ return false;
1014
1086
  const transformed = options.transformThreadData?.(current.threadData);
1015
1087
  const incomingThreadData = typeof transformed === "string"
1016
1088
  ? transformed
1017
1089
  : (transformed?.threadData ?? threadData);
1018
1090
  let nextThreadData = incomingThreadData;
1019
1091
  let nextMessageCount = messageCount;
1020
- try {
1021
- const merged = mergeThreadDataForClientSave(parseThreadData(current.threadData), parseThreadData(incomingThreadData), {
1022
- preserveExistingQueuedMessages: options.preserveExistingQueuedMessages ?? true,
1023
- preserveExistingTopLevelKeys: options.preserveExistingTopLevelKeys ?? true,
1024
- });
1025
- nextThreadData = JSON.stringify(merged);
1026
- nextMessageCount = countThreadMessages(merged, messageCount);
1027
- }
1028
- catch {
1029
- // Keep the caller's serialized value if either JSON blob is malformed.
1030
- }
1092
+ const annotationConflicts = [];
1093
+ const merged = mergeThreadDataForClientSave(parseThreadData(current.threadData), parseThreadData(incomingThreadData), {
1094
+ preserveExistingQueuedMessages: options.preserveExistingQueuedMessages ?? true,
1095
+ preserveExistingTopLevelKeys: options.preserveExistingTopLevelKeys ?? true,
1096
+ onAnnotationConflict: (conflict) => annotationConflicts.push(conflict),
1097
+ });
1098
+ nextThreadData = JSON.stringify(merged);
1099
+ nextMessageCount = countThreadMessages(merged, messageCount);
1031
1100
  const nextUpdatedAt = Math.max(Date.now(), current.updatedAt + 1);
1032
1101
  // Completion persistence can race the separate generated-title save.
1033
1102
  // Keep a title already committed by that save when this caller only has
1034
1103
  // its stale empty snapshot.
1035
- const nextTitle = options.preserveCurrentMetadata
1104
+ const preserveCurrentTitleAndPreview = options.preserveCurrentMetadata ||
1105
+ options.preserveCurrentTitleAndPreview;
1106
+ const nextTitle = preserveCurrentTitleAndPreview
1036
1107
  ? current.title
1037
1108
  : title || current.title;
1038
- const nextPreview = options.preserveCurrentMetadata
1109
+ const nextPreview = preserveCurrentTitleAndPreview
1039
1110
  ? current.preview
1040
1111
  : typeof transformed === "object" && transformed.preview !== undefined
1041
1112
  ? transformed.preview
@@ -1054,8 +1125,11 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
1054
1125
  ],
1055
1126
  });
1056
1127
  if (result.rowsAffected > 0) {
1128
+ for (const conflict of annotationConflicts) {
1129
+ options.onAnnotationConflict?.(conflict);
1130
+ }
1057
1131
  emitChatThreadChange(id);
1058
- return;
1132
+ return true;
1059
1133
  }
1060
1134
  lastConflict = true;
1061
1135
  }
@@ -1074,12 +1148,13 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
1074
1148
  throw lastError;
1075
1149
  if (lastConflict) {
1076
1150
  if (options.ignoreConflicts)
1077
- return;
1151
+ return false;
1078
1152
  const error = new Error(`Failed to update chat thread ${id} after concurrent write conflicts.`);
1079
1153
  error.statusCode = 409;
1080
1154
  error.statusMessage = error.message;
1081
1155
  throw error;
1082
1156
  }
1157
+ return false;
1083
1158
  }
1084
1159
  /**
1085
1160
  * Read the engine pinned to a thread (stored in thread_data JSON).