@agent-native/core 0.196.0 → 0.196.1-nightly-20260928200103

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 (41) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/actions/create-recording.ts +1 -1
  3. package/corpus/templates/clips/actions/lib/auto-title-candidates.ts +42 -0
  4. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +1 -1
  5. package/corpus/templates/clips/actions/lib/transcript-text.ts +16 -0
  6. package/corpus/templates/clips/actions/list-ai-requests.ts +49 -31
  7. package/corpus/templates/clips/actions/list-recordings.ts +2 -4
  8. package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +12 -6
  9. package/corpus/templates/clips/actions/regenerate-title.ts +4 -1
  10. package/corpus/templates/clips/actions/request-transcript.ts +1 -1
  11. package/corpus/templates/clips/actions/save-browser-transcript.ts +1 -1
  12. package/corpus/templates/clips/app/components/editable-recording-title.tsx +1 -1
  13. package/corpus/templates/clips/app/components/library/recording-card.tsx +1 -1
  14. package/corpus/templates/clips/app/hooks/use-auto-title.ts +194 -219
  15. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -1
  16. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
  17. package/corpus/templates/clips/server/lib/transactional-email-store.ts +42 -2
  18. package/corpus/templates/clips/shared/ai-request-status.ts +6 -0
  19. package/corpus/templates/clips/{actions/lib → shared}/title-source.ts +9 -2
  20. package/dist/agent/production-agent.js +2 -0
  21. package/dist/agent/run-manager.d.ts +2 -0
  22. package/dist/agent/run-manager.js +9 -2
  23. package/dist/agent/thread-data-builder.d.ts +1 -0
  24. package/dist/agent/thread-data-builder.js +148 -0
  25. package/dist/automation/index.d.ts +1 -1
  26. package/dist/chat-threads/store.d.ts +1 -0
  27. package/dist/chat-threads/store.js +244 -21
  28. package/dist/client/AgentKitAssistantChat.js +23 -12
  29. package/dist/client/chat/agentkit-agent-native.js +409 -8
  30. package/dist/client/chat/agentkit-protocol.js +3 -0
  31. package/dist/collab/awareness.d.ts +2 -2
  32. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  33. package/dist/notifications/routes.d.ts +3 -3
  34. package/dist/observability/routes.d.ts +2 -2
  35. package/dist/resources/handlers.d.ts +1 -1
  36. package/dist/server/agent-chat-plugin.d.ts +2 -0
  37. package/dist/server/agent-chat-plugin.js +12 -7
  38. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  39. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  40. package/dist/server/realtime-token.d.ts +1 -1
  41. package/package.json +1 -1
@@ -7657,6 +7657,8 @@ export function createProductionAgentHandler(options) {
7657
7657
  backgroundFunction: runsInBackgroundFunction,
7658
7658
  noProgressTimeoutMs: options.runNoProgressTimeoutMs,
7659
7659
  turnId: effectiveTurnId,
7660
+ agentKitApprovalContinuation: internalContinuation &&
7661
+ Boolean(approvedToolCallsForExecution?.length),
7660
7662
  turnInitiator,
7661
7663
  parentId: requestParentId,
7662
7664
  waitUntil: getRequestRunContext()?.waitUntil,
@@ -6,6 +6,7 @@ export interface ActiveRun {
6
6
  threadId: string;
7
7
  parentId?: string | null;
8
8
  turnId: string;
9
+ agentKitApprovalContinuation?: boolean;
9
10
  events: RunEvent[];
10
11
  status: RunStatus;
11
12
  subscribers: Set<(event: RunEvent) => void>;
@@ -54,6 +55,7 @@ export interface StartRunOptions {
54
55
  softTimeoutMs?: number;
55
56
  useHostedSoftTimeoutDefault?: boolean;
56
57
  turnId?: string;
58
+ agentKitApprovalContinuation?: boolean;
57
59
  parentId?: string | null;
58
60
  backgroundFunction?: boolean;
59
61
  noProgressTimeoutMs?: number;
@@ -486,6 +486,9 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
486
486
  threadId,
487
487
  ...(options?.parentId !== undefined ? { parentId: options.parentId } : {}),
488
488
  turnId: options?.turnId ?? runId,
489
+ ...(options?.agentKitApprovalContinuation
490
+ ? { agentKitApprovalContinuation: true }
491
+ : {}),
489
492
  events: [],
490
493
  status: "running",
491
494
  subscribers: new Set(),
@@ -1795,7 +1798,8 @@ export async function getActiveRunForThreadAsync(threadId) {
1795
1798
  const memRun = getActiveRunForThread(threadId);
1796
1799
  if (memRun && (memRun.status === "running" || memRun.events.length > 0)) {
1797
1800
  const sqlSnapshot = await fetchRunThreadSnapshot(memRun.runId, threadId);
1798
- if (!sqlSnapshot && memRun.status !== "running") {
1801
+ if (memRun.status !== "running" &&
1802
+ (!sqlSnapshot || sqlSnapshot.status === "running")) {
1799
1803
  const successor = await fetchNewerNonTerminalRunForSameTurn(threadId, memRun);
1800
1804
  if (successor) {
1801
1805
  return {
@@ -1813,7 +1817,10 @@ export async function getActiveRunForThreadAsync(threadId) {
1813
1817
  };
1814
1818
  }
1815
1819
  }
1816
- const status = legacyWireRunStatus(sqlSnapshot?.status ?? memRun.status);
1820
+ const sqlStatus = sqlSnapshot?.status === "running" && memRun.status !== "running"
1821
+ ? memRun.status
1822
+ : (sqlSnapshot?.status ?? memRun.status);
1823
+ const status = legacyWireRunStatus(sqlStatus);
1817
1824
  const heartbeatAt = status === "running"
1818
1825
  ? Date.now()
1819
1826
  : (sqlSnapshot?.heartbeatAt ?? memRun.startedAt);
@@ -107,6 +107,7 @@ export declare function foldAssistantTurn(repo: any, assistantMsg: AssistantMess
107
107
  turnId?: string;
108
108
  runId?: string;
109
109
  parentId?: string | null;
110
+ agentKitOwnsContinuation?: boolean;
110
111
  }): any;
111
112
  export declare function normalizeThreadTitle(value: unknown): string;
112
113
  export declare function extractThreadMeta(repo: any): {
@@ -1039,6 +1039,149 @@ export function claimQueuedMessage(repo, messageId) {
1039
1039
  normalized[CLAIMED_QUEUED_MESSAGE_IDS_KEY] = [...claimed, messageId].slice(-MAX_CLAIMED_QUEUED_MESSAGE_IDS);
1040
1040
  return pruneClaimedQueuedMessages(normalized);
1041
1041
  }
1042
+ function snapshotEntryId(entry, kind) {
1043
+ if (!entry || typeof entry !== "object")
1044
+ return undefined;
1045
+ if (kind === "widget") {
1046
+ const messageId = entry.messageId;
1047
+ const widgetId = entry.widget?.id;
1048
+ return typeof messageId === "string" && typeof widgetId === "string"
1049
+ ? JSON.stringify([messageId, widgetId])
1050
+ : undefined;
1051
+ }
1052
+ return typeof entry.id === "string" ? entry.id : undefined;
1053
+ }
1054
+ function snapshotMessageRunIds(agentKit) {
1055
+ const runIds = new Map();
1056
+ for (const event of Array.isArray(agentKit?.events) ? agentKit.events : []) {
1057
+ if ((event?.type === "message.created" ||
1058
+ event?.type === "message.completed") &&
1059
+ event.message?.role === "assistant" &&
1060
+ typeof event.message.id === "string" &&
1061
+ typeof event.runId === "string") {
1062
+ runIds.set(event.message.id, event.runId);
1063
+ }
1064
+ }
1065
+ for (const run of Array.isArray(agentKit?.runs) ? agentKit.runs : []) {
1066
+ if (typeof run?.activeMessageId === "string" &&
1067
+ typeof run.id === "string") {
1068
+ runIds.set(run.activeMessageId, run.id);
1069
+ }
1070
+ }
1071
+ return runIds;
1072
+ }
1073
+ function snapshotAssistantTextKey(message, runIds) {
1074
+ const runId = (typeof message?.id === "string" ? runIds.get(message.id) : undefined) ??
1075
+ message?.metadata?.runId ??
1076
+ message?.metadata?.custom?.runId;
1077
+ if (message?.role !== "assistant" ||
1078
+ typeof runId !== "string" ||
1079
+ !Array.isArray(message.parts) ||
1080
+ message.parts.length === 0 ||
1081
+ !message.parts.every((part) => part?.type === "text" && typeof part.text === "string")) {
1082
+ return undefined;
1083
+ }
1084
+ return JSON.stringify([
1085
+ runId,
1086
+ message.parts.map((part) => [part.text, part.format ?? null]),
1087
+ ]);
1088
+ }
1089
+ function preferIncomingSnapshotEntry(kind, existing, incoming) {
1090
+ if (kind === "message") {
1091
+ const rank = (message) => message.status === "complete" ? 2 : message.status === "error" ? 1 : 0;
1092
+ if (rank(existing) !== rank(incoming))
1093
+ return rank(incoming) > rank(existing);
1094
+ const partCount = (message) => Array.isArray(message.parts) ? message.parts.length : 0;
1095
+ if (partCount(existing) !== partCount(incoming)) {
1096
+ return partCount(incoming) > partCount(existing);
1097
+ }
1098
+ const textLength = (message) => (Array.isArray(message.parts) ? message.parts : []).reduce((total, part) => total + (typeof part?.text === "string" ? part.text.length : 0), 0);
1099
+ return textLength(incoming) > textLength(existing);
1100
+ }
1101
+ if (kind === "toolCall") {
1102
+ return existing.status === "running" && incoming.status !== "running";
1103
+ }
1104
+ return (existing.widget?.state === "active" && incoming.widget?.state !== "active");
1105
+ }
1106
+ function mergeAgentKitHistoryArray(existing, incoming, kind, existingMessageRunIds, incomingMessageRunIds) {
1107
+ if (!Array.isArray(existing) && !Array.isArray(incoming))
1108
+ return undefined;
1109
+ const merged = Array.isArray(existing) ? [...existing] : [];
1110
+ const positions = new Map();
1111
+ const assistantTextPositions = new Map();
1112
+ merged.forEach((entry, index) => {
1113
+ const id = snapshotEntryId(entry, kind);
1114
+ if (id && !positions.has(id))
1115
+ positions.set(id, index);
1116
+ if (kind === "message") {
1117
+ const key = snapshotAssistantTextKey(entry, existingMessageRunIds);
1118
+ if (key) {
1119
+ assistantTextPositions.set(key, [
1120
+ ...(assistantTextPositions.get(key) ?? []),
1121
+ index,
1122
+ ]);
1123
+ }
1124
+ }
1125
+ });
1126
+ const matchedAssistantTextPositions = new Set();
1127
+ for (const entry of Array.isArray(incoming) ? incoming : []) {
1128
+ const id = snapshotEntryId(entry, kind);
1129
+ const idIndex = id ? positions.get(id) : undefined;
1130
+ const textKey = kind === "message"
1131
+ ? snapshotAssistantTextKey(entry, incomingMessageRunIds)
1132
+ : undefined;
1133
+ const textIndex = idIndex === undefined && textKey
1134
+ ? assistantTextPositions
1135
+ .get(textKey)
1136
+ ?.find((candidate) => !matchedAssistantTextPositions.has(candidate))
1137
+ : undefined;
1138
+ const index = idIndex ?? textIndex;
1139
+ if (index === undefined) {
1140
+ if (id)
1141
+ positions.set(id, merged.length);
1142
+ merged.push(entry);
1143
+ }
1144
+ else {
1145
+ if (kind === "message")
1146
+ matchedAssistantTextPositions.add(index);
1147
+ if (id && idIndex === undefined)
1148
+ positions.set(id, index);
1149
+ const preferIncoming = preferIncomingSnapshotEntry(kind, merged[index], entry);
1150
+ const preferCurrentMessageId = kind === "message" &&
1151
+ textIndex !== undefined &&
1152
+ !preferIncomingSnapshotEntry(kind, entry, merged[index]);
1153
+ if (preferIncoming || preferCurrentMessageId) {
1154
+ merged[index] = entry;
1155
+ }
1156
+ }
1157
+ }
1158
+ return merged;
1159
+ }
1160
+ function mergeAgentKitHistory(existing, incoming) {
1161
+ if (!existing ||
1162
+ typeof existing !== "object" ||
1163
+ Array.isArray(existing) ||
1164
+ !incoming ||
1165
+ typeof incoming !== "object" ||
1166
+ Array.isArray(incoming)) {
1167
+ return incoming ?? existing;
1168
+ }
1169
+ const previous = existing;
1170
+ const next = incoming;
1171
+ const merged = { ...previous, ...next };
1172
+ const existingMessageRunIds = snapshotMessageRunIds(previous);
1173
+ const incomingMessageRunIds = snapshotMessageRunIds(next);
1174
+ for (const [key, kind] of [
1175
+ ["messages", "message"],
1176
+ ["toolCalls", "toolCall"],
1177
+ ["widgets", "widget"],
1178
+ ]) {
1179
+ const entries = mergeAgentKitHistoryArray(previous[key], next[key], kind, existingMessageRunIds, incomingMessageRunIds);
1180
+ if (entries)
1181
+ merged[key] = entries;
1182
+ }
1183
+ return merged;
1184
+ }
1042
1185
  function pruneClaimedQueuedMessages(repo) {
1043
1186
  if (!Array.isArray(repo?.queuedMessages))
1044
1187
  return repo;
@@ -1079,6 +1222,9 @@ export function mergeThreadDataForClientSave(existingRepo, incomingRepo, options
1079
1222
  merged.queuedMessages === undefined) {
1080
1223
  merged.queuedMessages = existingNormalized.queuedMessages;
1081
1224
  }
1225
+ if (merged.agentKit !== undefined) {
1226
+ merged.agentKit = mergeAgentKitHistory(existingNormalized?.agentKit, merged.agentKit);
1227
+ }
1082
1228
  const existingMessages = Array.isArray(existingNormalized?.messages)
1083
1229
  ? existingNormalized.messages
1084
1230
  : null;
@@ -1509,6 +1655,8 @@ function appendFoldedContent(existing, incoming) {
1509
1655
  }).content;
1510
1656
  }
1511
1657
  export function foldAssistantTurn(repo, assistantMsg, options) {
1658
+ if (options.agentKitOwnsContinuation)
1659
+ return repo;
1512
1660
  const turnId = options.turnId;
1513
1661
  const runId = options.runId;
1514
1662
  if (!turnId)
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
+ error?: undefined;
142
143
  accepted: boolean;
143
144
  duplicate: boolean;
144
145
  eventId: string;
145
- error?: undefined;
146
146
  }>;
@@ -73,6 +73,7 @@ export interface ForkThreadSourceSnapshot {
73
73
  title?: string;
74
74
  preview?: string;
75
75
  messageCount?: number;
76
+ fromMessageId?: string;
76
77
  scope?: ChatThreadScope | null;
77
78
  }
78
79
  export declare function createThread(ownerEmail: string, opts?: {
@@ -256,28 +256,253 @@ function normalizeForkSourceSnapshot(source) {
256
256
  catch {
257
257
  return null;
258
258
  }
259
- const repoMessageCount = Array.isArray(parsed.messages)
260
- ? parsed.messages.length
261
- : 0;
262
- if (repoMessageCount <= 0)
259
+ const messageCount = countThreadMessages(parsed, 0);
260
+ if (messageCount <= 0)
263
261
  return null;
264
262
  return {
265
263
  threadData: JSON.stringify(parsed),
266
264
  title: typeof source.title === "string" ? source.title : "",
267
265
  preview: typeof source.preview === "string" ? source.preview : "",
268
- messageCount: repoMessageCount,
266
+ messageCount,
267
+ ...(typeof source.fromMessageId === "string"
268
+ ? { fromMessageId: source.fromMessageId }
269
+ : {}),
269
270
  ...(Object.prototype.hasOwnProperty.call(source, "scope")
270
271
  ? { scope: source.scope ?? null }
271
272
  : {}),
272
273
  };
273
274
  }
275
+ function countThreadMessages(value, fallback) {
276
+ const repo = normalizeThreadRepository(value);
277
+ if (!repo || typeof repo !== "object")
278
+ return fallback;
279
+ const repoMessageCount = Array.isArray(repo.messages)
280
+ ? repo.messages.length
281
+ : undefined;
282
+ const agentKitMessageCount = Array.isArray(repo.agentKit?.messages)
283
+ ? repo.agentKit.messages.length
284
+ : undefined;
285
+ if (repoMessageCount === undefined && agentKitMessageCount === undefined) {
286
+ return fallback;
287
+ }
288
+ return Math.max(repoMessageCount ?? 0, agentKitMessageCount ?? 0);
289
+ }
290
+ function forkThreadData(threadData, forkId, fromMessageId) {
291
+ const parsed = normalizeThreadRepository(JSON.parse(threadData));
292
+ const repository = parsed && typeof parsed === "object" && !Array.isArray(parsed)
293
+ ? parsed
294
+ : undefined;
295
+ const agentKit = repository?.agentKit;
296
+ if (!agentKit || typeof agentKit !== "object" || Array.isArray(agentKit)) {
297
+ return threadData;
298
+ }
299
+ const agentKitRecord = agentKit;
300
+ const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value)
301
+ ? value
302
+ : undefined;
303
+ const repositoryMessages = Array.isArray(repository.messages)
304
+ ? repository.messages
305
+ : [];
306
+ const protocolMessages = Array.isArray(agentKitRecord.messages)
307
+ ? agentKitRecord.messages
308
+ : [];
309
+ const messageId = (value) => {
310
+ const outer = asRecord(value);
311
+ const message = asRecord(outer?.message) ?? outer;
312
+ return typeof message?.id === "string" ? message.id : undefined;
313
+ };
314
+ const messageIds = new Set();
315
+ if (fromMessageId) {
316
+ for (const messages of [repositoryMessages, protocolMessages]) {
317
+ const throughIndex = messages.findIndex((message) => messageId(message) === fromMessageId);
318
+ if (throughIndex >= 0) {
319
+ for (const message of messages.slice(0, throughIndex + 1)) {
320
+ const id = messageId(message);
321
+ if (id)
322
+ messageIds.add(id);
323
+ }
324
+ }
325
+ }
326
+ if (!messageIds.has(fromMessageId)) {
327
+ throw new Error(`Unknown message for fork: ${fromMessageId}`);
328
+ }
329
+ }
330
+ else {
331
+ for (const message of [...repositoryMessages, ...protocolMessages]) {
332
+ const id = messageId(message);
333
+ if (id)
334
+ messageIds.add(id);
335
+ }
336
+ }
337
+ const events = Array.isArray(agentKitRecord.events)
338
+ ? agentKitRecord.events
339
+ : [];
340
+ const runs = Array.isArray(agentKitRecord.runs) ? agentKitRecord.runs : [];
341
+ const eventBoundaries = new Map();
342
+ for (const rawEvent of events) {
343
+ const event = asRecord(rawEvent);
344
+ const message = asRecord(event?.message);
345
+ const messageId = typeof event?.messageId === "string"
346
+ ? event.messageId
347
+ : typeof message?.id === "string"
348
+ ? message.id
349
+ : undefined;
350
+ if (event?.type === "message.completed" &&
351
+ typeof event?.runId === "string" &&
352
+ typeof event.sequence === "number" &&
353
+ messageId &&
354
+ messageIds.has(messageId)) {
355
+ eventBoundaries.set(event.runId, Math.max(eventBoundaries.get(event.runId) ?? 0, event.sequence));
356
+ }
357
+ }
358
+ for (const rawRun of runs) {
359
+ const run = asRecord(rawRun);
360
+ if (typeof run?.id === "string" &&
361
+ typeof run.activeMessageId === "string" &&
362
+ messageIds.has(run.activeMessageId) &&
363
+ typeof run.lastSequence === "number") {
364
+ eventBoundaries.set(run.id, Math.max(eventBoundaries.get(run.id) ?? 0, run.lastSequence));
365
+ }
366
+ }
367
+ const retainedRunIds = new Set(eventBoundaries.keys());
368
+ const nonterminalRunIds = new Set(runs.flatMap((rawRun) => {
369
+ const run = asRecord(rawRun);
370
+ return typeof run?.id === "string" &&
371
+ (run.status === "queued" ||
372
+ run.status === "running" ||
373
+ run.status === "awaiting_approval" ||
374
+ run.status === "awaiting_input")
375
+ ? [run.id]
376
+ : [];
377
+ }));
378
+ const activeMessageIds = new Set(runs.flatMap((rawRun) => {
379
+ const run = asRecord(rawRun);
380
+ return typeof run?.id === "string" &&
381
+ nonterminalRunIds.has(run.id) &&
382
+ typeof run.activeMessageId === "string"
383
+ ? [run.activeMessageId]
384
+ : [];
385
+ }));
386
+ const retainedMessageId = (value) => {
387
+ const id = messageId(value);
388
+ return Boolean(id && messageIds.has(id));
389
+ };
390
+ const remapThreadId = (value) => value && typeof value === "object" && !Array.isArray(value)
391
+ ? { ...value, threadId: forkId }
392
+ : value;
393
+ return JSON.stringify({
394
+ ...repository,
395
+ ...(Array.isArray(repository.messages)
396
+ ? { messages: repositoryMessages.filter(retainedMessageId) }
397
+ : {}),
398
+ agentKit: {
399
+ ...agentKitRecord,
400
+ ...(Array.isArray(agentKitRecord.messages)
401
+ ? {
402
+ messages: agentKitRecord.messages
403
+ .filter(retainedMessageId)
404
+ .map((message) => {
405
+ const record = asRecord(message);
406
+ return typeof record.id === "string" &&
407
+ activeMessageIds.has(record.id) &&
408
+ record.status === "streaming"
409
+ ? { ...record, status: "complete" }
410
+ : record;
411
+ }),
412
+ }
413
+ : {}),
414
+ ...(Array.isArray(agentKitRecord.events)
415
+ ? {
416
+ events: events.flatMap((rawEvent) => {
417
+ const event = asRecord(rawEvent);
418
+ const runId = event?.runId;
419
+ const boundary = typeof runId === "string"
420
+ ? eventBoundaries.get(runId)
421
+ : undefined;
422
+ if (typeof runId !== "string" ||
423
+ boundary === undefined ||
424
+ typeof event?.sequence !== "number" ||
425
+ event.sequence > boundary) {
426
+ return [];
427
+ }
428
+ if (nonterminalRunIds.has(runId) &&
429
+ (event.type === "activity.started" ||
430
+ event.type === "activity.updated")) {
431
+ const activity = asRecord(event.activity);
432
+ if (activity?.status === "running") {
433
+ return [
434
+ {
435
+ ...event,
436
+ type: "activity.completed",
437
+ activity: { ...activity, status: "cancelled" },
438
+ threadId: forkId,
439
+ },
440
+ ];
441
+ }
442
+ }
443
+ return [remapThreadId(rawEvent)];
444
+ }),
445
+ }
446
+ : {}),
447
+ ...(Array.isArray(agentKitRecord.runs)
448
+ ? {
449
+ runs: runs.flatMap((rawRun) => {
450
+ const run = asRecord(rawRun);
451
+ if (typeof run?.id !== "string" || !retainedRunIds.has(run.id)) {
452
+ return [];
453
+ }
454
+ const copied = {
455
+ ...run,
456
+ threadId: forkId,
457
+ };
458
+ if (nonterminalRunIds.has(run.id)) {
459
+ copied.status = "cancelled";
460
+ delete copied.activeMessageId;
461
+ }
462
+ return [copied];
463
+ }),
464
+ }
465
+ : {}),
466
+ ...(fromMessageId && Array.isArray(agentKitRecord.toolCalls)
467
+ ? {
468
+ toolCalls: agentKitRecord.toolCalls.filter((rawToolCall) => {
469
+ const toolCall = asRecord(rawToolCall);
470
+ if (typeof toolCall?.messageId === "string") {
471
+ return messageIds.has(toolCall.messageId);
472
+ }
473
+ return (typeof toolCall?.runId === "string" &&
474
+ retainedRunIds.has(toolCall.runId));
475
+ }),
476
+ }
477
+ : {}),
478
+ ...(Array.isArray(agentKitRecord.widgets)
479
+ ? {
480
+ widgets: agentKitRecord.widgets.filter((widget) => {
481
+ const record = asRecord(widget);
482
+ return (typeof record?.messageId === "string" &&
483
+ messageIds.has(record.messageId));
484
+ }),
485
+ }
486
+ : {}),
487
+ ...(Array.isArray(agentKitRecord.annotations)
488
+ ? {
489
+ annotations: agentKitRecord.annotations.filter((annotation) => {
490
+ const record = asRecord(annotation);
491
+ return (typeof record?.messageId === "string" &&
492
+ messageIds.has(record.messageId));
493
+ }),
494
+ }
495
+ : {}),
496
+ ...(fromMessageId ? { suggestions: [] } : {}),
497
+ activeRunIds: [],
498
+ },
499
+ });
500
+ }
274
501
  function deriveMessageCount(threadData, fallback) {
275
502
  if (typeof threadData !== "string" || !threadData.trim())
276
503
  return fallback;
277
504
  try {
278
- const repo = normalizeThreadRepository(JSON.parse(threadData));
279
- if (Array.isArray(repo.messages))
280
- return repo.messages.length;
505
+ return countThreadMessages(JSON.parse(threadData), fallback);
281
506
  }
282
507
  catch {
283
508
  // Keep the stored count if the JSON blob is malformed.
@@ -485,18 +710,17 @@ export async function forkThread(sourceId, ownerEmail, opts) {
485
710
  }
486
711
  else if (snapshot &&
487
712
  source.ownerEmail === ownerEmail &&
488
- snapshot.messageCount > source.messageCount) {
489
- // The source row exists but the in-memory snapshot is fresher — the agent
490
- // run flushed an older state to SQL, but the tab has additional unflushed
491
- // messages. Overlay the snapshot before cloning so the fork captures the
492
- // latest user-visible content. Guard with messageCount > stored to avoid
493
- // clobbering a fresher persisted row with a stale snapshot from another
494
- // tab.
713
+ (snapshot.fromMessageId || snapshot.messageCount > source.messageCount)) {
714
+ // Message-scoped forks intentionally carry a truncated snapshot even when
715
+ // the persisted source has later messages; full forks only overlay fresher
716
+ // client snapshots.
495
717
  source = {
496
718
  ...source,
497
719
  threadData: snapshot.threadData,
498
720
  title: snapshot.title || source.title,
499
- preview: snapshot.preview || source.preview,
721
+ preview: snapshot.fromMessageId
722
+ ? snapshot.preview
723
+ : snapshot.preview || source.preview,
500
724
  messageCount: snapshot.messageCount,
501
725
  };
502
726
  }
@@ -505,6 +729,7 @@ export async function forkThread(sourceId, ownerEmail, opts) {
505
729
  return null;
506
730
  }
507
731
  const id = opts?.id ?? generateId();
732
+ const threadData = forkThreadData(source.threadData, id, snapshot?.fromMessageId);
508
733
  const now = Date.now();
509
734
  const title = source.title ? `${source.title} (fork)` : "";
510
735
  const client = getDbExec();
@@ -516,7 +741,7 @@ export async function forkThread(sourceId, ownerEmail, opts) {
516
741
  ownerEmail,
517
742
  title,
518
743
  source.preview,
519
- source.threadData,
744
+ threadData,
520
745
  source.messageCount,
521
746
  now,
522
747
  now,
@@ -534,7 +759,7 @@ export async function forkThread(sourceId, ownerEmail, opts) {
534
759
  ownerEmail,
535
760
  title,
536
761
  preview: source.preview,
537
- threadData: source.threadData,
762
+ threadData,
538
763
  messageCount: source.messageCount,
539
764
  createdAt: now,
540
765
  updatedAt: now,
@@ -787,9 +1012,7 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
787
1012
  preserveExistingTopLevelKeys: options.preserveExistingTopLevelKeys ?? true,
788
1013
  });
789
1014
  nextThreadData = JSON.stringify(merged);
790
- if (Array.isArray(merged.messages)) {
791
- nextMessageCount = merged.messages.length;
792
- }
1015
+ nextMessageCount = countThreadMessages(merged, messageCount);
793
1016
  }
794
1017
  catch {
795
1018
  // Keep the caller's serialized value if either JSON blob is malformed.