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

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 (51) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
  3. package/corpus/templates/clips/actions/create-recording.ts +1 -1
  4. package/corpus/templates/clips/actions/lib/auto-title-candidates.ts +42 -0
  5. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +1 -1
  6. package/corpus/templates/clips/actions/lib/transcript-text.ts +16 -0
  7. package/corpus/templates/clips/actions/list-ai-requests.ts +49 -31
  8. package/corpus/templates/clips/actions/list-recordings.ts +2 -4
  9. package/corpus/templates/clips/actions/list-transactional-email-ai-requests.ts +12 -6
  10. package/corpus/templates/clips/actions/regenerate-title.ts +4 -1
  11. package/corpus/templates/clips/actions/request-transcript.ts +1 -1
  12. package/corpus/templates/clips/actions/save-browser-transcript.ts +1 -1
  13. package/corpus/templates/clips/app/components/editable-recording-title.tsx +1 -1
  14. package/corpus/templates/clips/app/components/library/recording-card.tsx +1 -1
  15. package/corpus/templates/clips/app/hooks/use-auto-title.ts +194 -219
  16. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -1
  17. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
  18. package/corpus/templates/clips/changelog/2026-09-28-clip-share-previews-show-a-branded-image-while-the-recording.md +6 -0
  19. package/corpus/templates/clips/server/lib/slack-unfurls.ts +2 -0
  20. package/corpus/templates/clips/server/lib/transactional-email-store.ts +42 -2
  21. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +27 -15
  22. package/corpus/templates/clips/shared/ai-request-status.ts +6 -0
  23. package/corpus/templates/clips/shared/share-meta.ts +2 -27
  24. package/corpus/templates/clips/{actions/lib → shared}/title-source.ts +9 -2
  25. package/corpus/templates/slides/changelog/2026-09-28-typing-in-slides-agent-chat-keeps-every-character-in-order.md +6 -0
  26. package/corpus/templates/slides/scripts/edit-fidelity/README.md +11 -2
  27. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +135 -4
  28. package/dist/agent/production-agent.js +2 -0
  29. package/dist/agent/run-manager.d.ts +2 -0
  30. package/dist/agent/run-manager.js +9 -2
  31. package/dist/agent/thread-data-builder.d.ts +1 -0
  32. package/dist/agent/thread-data-builder.js +148 -0
  33. package/dist/chat-threads/store.d.ts +1 -0
  34. package/dist/chat-threads/store.js +244 -21
  35. package/dist/client/AgentKitAssistantChat.js +23 -12
  36. package/dist/client/chat/agentkit-agent-native.js +409 -8
  37. package/dist/client/chat/agentkit-protocol.js +3 -0
  38. package/dist/collab/awareness.d.ts +2 -2
  39. package/dist/collab/struct-routes.d.ts +1 -1
  40. package/dist/notifications/routes.d.ts +1 -1
  41. package/dist/observability/routes.d.ts +6 -6
  42. package/dist/org/handlers.d.ts +1 -1
  43. package/dist/resources/handlers.d.ts +1 -1
  44. package/dist/secrets/routes.d.ts +4 -4
  45. package/dist/server/agent-chat-plugin.d.ts +2 -0
  46. package/dist/server/agent-chat-plugin.js +12 -7
  47. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  48. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  49. package/dist/server/realtime-token.d.ts +1 -1
  50. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  51. package/package.json +2 -2
@@ -10,6 +10,7 @@ import {
10
10
  signShortLivedToken,
11
11
  verifyShortLivedToken,
12
12
  } from "@agent-native/core/server";
13
+ import { AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE } from "@agent-native/core/shared";
13
14
  import { resolveAccess } from "@agent-native/core/sharing";
14
15
  import { eq } from "drizzle-orm";
15
16
  import {
@@ -159,6 +160,18 @@ function imageResponse(
159
160
  return new Response(body, { status, headers });
160
161
  }
161
162
 
163
+ function defaultSocialImageResponse(): Response {
164
+ return new Response(null, {
165
+ status: 302,
166
+ headers: {
167
+ Location: AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE,
168
+ "Cache-Control": "private, max-age=0, no-store",
169
+ "Referrer-Policy": "no-referrer",
170
+ "X-Content-Type-Options": "nosniff",
171
+ },
172
+ });
173
+ }
174
+
162
175
  async function fetchThumbnail(sourceUrl: string): Promise<Response> {
163
176
  let currentUrl = sourceUrl;
164
177
  const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;
@@ -325,20 +338,13 @@ export default defineEventHandler(async (event: H3Event) => {
325
338
  query.animated === "1"
326
339
  ? recording.animatedThumbnailUrl || recording.thumbnailUrl
327
340
  : recording.thumbnailUrl || recording.animatedThumbnailUrl;
328
- if (!sourceUrl) {
329
- setResponseStatus(event, 404);
330
- return { error: "Thumbnail not found" };
331
- }
341
+ if (!sourceUrl) return defaultSocialImageResponse();
332
342
 
333
343
  if (sourceUrl.startsWith("data:")) {
334
- return (
335
- dataUrlResponse(sourceUrl) ??
336
- imageResponse(null, "text/plain; charset=utf-8", 415)
337
- );
344
+ return dataUrlResponse(sourceUrl) ?? defaultSocialImageResponse();
338
345
  }
339
346
  if (isRecursiveThumbnailUrl(sourceUrl, recordingId)) {
340
- setResponseStatus(event, 404);
341
- return { error: "Thumbnail not found" };
347
+ return defaultSocialImageResponse();
342
348
  }
343
349
 
344
350
  let resolvedSourceUrl = sourceUrl;
@@ -360,17 +366,23 @@ export default defineEventHandler(async (event: H3Event) => {
360
366
  extra: { recordingId },
361
367
  },
362
368
  );
363
- setResponseStatus(event, 502);
364
- return { error: "The recording thumbnail could not be loaded." };
365
369
  }
366
- return response;
370
+ const contentType = response.headers
371
+ .get("content-type")
372
+ ?.split(";", 1)[0]
373
+ ?.trim()
374
+ .toLowerCase();
375
+ return response.ok &&
376
+ contentType &&
377
+ SAFE_RASTER_IMAGE_TYPES.has(contentType)
378
+ ? response
379
+ : defaultSocialImageResponse();
367
380
  } catch (error) {
368
381
  captureRouteError(error, {
369
382
  route: "api/thumbnail",
370
383
  extra: { recordingId },
371
384
  });
372
- setResponseStatus(event, 502);
373
- return { error: "The recording thumbnail could not be loaded." };
385
+ return defaultSocialImageResponse();
374
386
  }
375
387
  },
376
388
  );
@@ -48,3 +48,9 @@ export interface ClipsAiRequestStatus {
48
48
  requestedAt?: string;
49
49
  updatedAt?: string;
50
50
  }
51
+
52
+ /** A recording the browser bridge may auto-title once it is old enough. */
53
+ export interface AutoTitleCandidate {
54
+ id: string;
55
+ createdAt: string;
56
+ }
@@ -10,9 +10,6 @@ import {
10
10
  type SocialMetaDescriptor,
11
11
  } from "@agent-native/core/shared";
12
12
 
13
- import { buildAgentApiUrls } from "./agent-context";
14
- import { isLoomEmbedBackedRecording } from "./loom";
15
-
16
13
  export const CLIPS_DEFAULT_TITLE = "Untitled recording";
17
14
 
18
15
  export type ClipsShareMetaRecording = {
@@ -32,8 +29,6 @@ export type ClipsShareMetaRecording = {
32
29
  isLoomEmbedBacked?: boolean;
33
30
  };
34
31
 
35
- const SOCIAL_FRAME_AT_MS = 350;
36
-
37
32
  export type PreferredThumbnailVariant = "still" | "animated";
38
33
 
39
34
  export function preferredThumbnailVariant(
@@ -146,31 +141,11 @@ export function resolveClipsSocialImageUrl(options: {
146
141
  }
147
142
 
148
143
  if (!canUseSocialImage(recording)) return undefined;
149
- if (
150
- !origin ||
151
- recording.isLoomEmbedBacked === true ||
152
- isLoomEmbedBackedRecording(recording)
153
- ) {
154
- console.warn("clips.thumbnail.publish_fallback", {
155
- recordingId: recording.id,
156
- fallback: "default-image",
157
- });
158
- return AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE;
159
- }
160
-
161
144
  console.warn("clips.thumbnail.publish_fallback", {
162
145
  recordingId: recording.id,
163
- fallback: "live-frame",
146
+ fallback: "default-image",
164
147
  });
165
- const frameUrl = new URL(
166
- buildAgentApiUrls(recording.id, {
167
- origin,
168
- basePath,
169
- }).frameUrl(SOCIAL_FRAME_AT_MS),
170
- );
171
- const version = recording.updatedAt?.trim();
172
- if (version) frameUrl.searchParams.set("v", version);
173
- return frameUrl.toString();
148
+ return AGENT_NATIVE_DEFAULT_SOCIAL_IMAGE;
174
149
  }
175
150
 
176
151
  export function buildClipsShareMeta(options: {
@@ -15,13 +15,20 @@ export function isDefaultTitle(title: string | null | undefined): boolean {
15
15
  return !trimmed || trimmed === DEFAULT_RECORDING_TITLE;
16
16
  }
17
17
 
18
+ /** Title sources an auto-generated title may overwrite. */
19
+ export const AUTO_REPLACEABLE_TITLE_SOURCES = [
20
+ "default",
21
+ "context",
22
+ ] as const satisfies readonly RecordingTitleSource[];
23
+
18
24
  export function isAutoTitleReplaceable(
19
25
  title: string | null | undefined,
20
26
  titleSource: string | null | undefined,
21
27
  ): boolean {
22
28
  return (
23
29
  isDefaultTitle(title) ||
24
- titleSource === "default" ||
25
- titleSource === "context"
30
+ (AUTO_REPLACEABLE_TITLE_SOURCES as readonly string[]).includes(
31
+ titleSource ?? "",
32
+ )
26
33
  );
27
34
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-28
4
+ ---
5
+
6
+ Typing in Slides agent chat keeps every character in order
@@ -15,6 +15,13 @@ Run the script from `templates/slides`:
15
15
  pnpm exec tsx scripts/edit-fidelity/run.ts [case-filter] [options]
16
16
  ```
17
17
 
18
+ Run the focused chat caret regression while a synthetic slide text edit session
19
+ is open in Chromium:
20
+
21
+ ```bash
22
+ pnpm exec tsx scripts/edit-fidelity/run.ts --typing-chat
23
+ ```
24
+
18
25
  By default the harness starts its own scratch dev server with this command,
19
26
  run from the repo root:
20
27
 
@@ -47,6 +54,7 @@ because it creates and rewrites decks.
47
54
  | `--resume <run>` | Reuse `<run>`'s output and keep every result that did not error |
48
55
  | `--cpu-throttle N` | Slow each editor page's CPU N times, to reproduce timing-dependent saves |
49
56
  | `--headed` | Show the browser |
57
+ | `--typing-chat` | Run only the Agent chat typing regression with a slide text edit session open |
50
58
 
51
59
  Exit codes:
52
60
 
@@ -136,8 +144,9 @@ them per slide. For each target and scenario:
136
144
  stylesheet per slide font, so `fonts.ready` alone can resolve before the
137
145
  slide's font is requested. Scenarios never contaminate each other: a
138
146
  `pagehide` keepalive write from the previous page that carries other
139
- content for this slide could land after the restore, so it errors the
140
- scenario.
147
+ content for this slide could land after the restore. The harness retains
148
+ those request bodies, waits for the restored content to settle when one
149
+ could overwrite it, and errors if the fixture did not survive.
141
150
  2. **View.** Capture `view.png` and a style snapshot of the slide.
142
151
  3. **Enter edit.** Try click, then a second click, then double-click. The
143
152
  gesture that worked is recorded. A click must leave a caret within one
@@ -134,6 +134,7 @@ const cpuThrottle = numOpt("--cpu-throttle", 1);
134
134
  const update = argv.includes("--update");
135
135
  const acceptFailing = argv.includes("--accept-failing");
136
136
  const headed = argv.includes("--headed");
137
+ const typingChatOnly = argv.includes("--typing-chat");
137
138
  for (const s of scenarios) {
138
139
  if (!SCENARIOS.includes(s))
139
140
  fatal(`unknown scenario ${s}; expected ${SCENARIOS.join(",")}`);
@@ -532,6 +533,101 @@ async function exitEdit(
532
533
  return waitFor(async () => !(await editorState(page, slideId)).editing, 5000);
533
534
  }
534
535
 
536
+ async function runChatTypingRegression(page: Page, base: string) {
537
+ await page.route("**/_agent-native/agent-engine/status", (route: any) =>
538
+ route.fulfill({
539
+ status: 200,
540
+ contentType: "application/json",
541
+ body: JSON.stringify({ configured: true, chatEligible: true }),
542
+ }),
543
+ );
544
+ await page.goto(`${base}/home`, { waitUntil: "domcontentloaded" });
545
+ await ensureSignedIn(page);
546
+ const created = await action(page, "create-deck", {
547
+ title: "[edit-fidelity] chat typing regression",
548
+ slides: [
549
+ {
550
+ id: "chat-typing-slide",
551
+ content:
552
+ '<div class="fmd-slide"><p>Slide text edit stays open</p></div>',
553
+ },
554
+ ],
555
+ });
556
+ const deckId = String(created.id ?? created.deckId);
557
+ await openSlide(page, base, deckId, 0, "chat-typing-slide");
558
+ const [target] = await listTargets(page, "chat-typing-slide");
559
+ if (!target) throw new Error("synthetic slide has no editable text target");
560
+ if (!(await enterEdit(page, "chat-typing-slide", target.point, []))) {
561
+ throw new Error("could not open the synthetic slide text edit session");
562
+ }
563
+
564
+ await page.evaluate(() =>
565
+ window.dispatchEvent(
566
+ new CustomEvent("agent-panel:open", { detail: { focus: true } }),
567
+ ),
568
+ );
569
+ const selector =
570
+ '.agent-sidebar-panel[data-agent-sidebar-state="open"] [data-agent-composer-slot="editor-input"]';
571
+ const composer = page.locator(selector);
572
+ await composer.waitFor({ state: "visible", timeout: 45_000 });
573
+ if (!(await editorState(page, "chat-typing-slide")).editing) {
574
+ throw new Error(
575
+ "opening the Agent sidebar ended the slide text edit session",
576
+ );
577
+ }
578
+ await page.waitForFunction(
579
+ (inputSelector: string) =>
580
+ document
581
+ .querySelector<HTMLElement>(inputSelector)
582
+ ?.getAttribute("contenteditable") === "true",
583
+ selector,
584
+ { timeout: 20_000 },
585
+ );
586
+ await composer.focus();
587
+
588
+ await composer.evaluate((element: HTMLElement) => {
589
+ (window as any).__typingRegressionComposer = element;
590
+ });
591
+
592
+ const first = "Fast typing should keep every character in order.";
593
+ const second = " A pause must not reset the caret either.";
594
+ await composer.pressSequentially(first);
595
+ await sleep(400);
596
+ await composer.pressSequentially(second);
597
+
598
+ const expected = first + second;
599
+ const result = await page.evaluate((selector: string) => {
600
+ const editor = document.querySelector<HTMLElement>(selector);
601
+ const selection = window.getSelection();
602
+ const focusNode = selection?.focusNode;
603
+ return {
604
+ text: editor?.innerText ?? null,
605
+ sameNode: editor === (window as any).__typingRegressionComposer,
606
+ focused: document.activeElement === editor,
607
+ caretOffset:
608
+ editor && focusNode && editor.contains(focusNode)
609
+ ? (selection?.focusOffset ?? null)
610
+ : null,
611
+ };
612
+ }, selector);
613
+ const editAfterTyping = await editorState(page, "chat-typing-slide");
614
+ const problems: string[] = [];
615
+ if (result.text !== expected) {
616
+ problems.push(`text mismatch: ${JSON.stringify(result.text)}`);
617
+ }
618
+ if (!result.sameNode) problems.push("composer remounted while typing");
619
+ if (!result.focused) problems.push("composer lost focus while typing");
620
+ if (result.caretOffset !== expected.length) {
621
+ problems.push(
622
+ `caret ended at ${result.caretOffset}, expected ${expected.length}`,
623
+ );
624
+ }
625
+ if (!editAfterTyping.editing) {
626
+ problems.push("slide text edit session ended while typing in chat");
627
+ }
628
+ return problems;
629
+ }
630
+
535
631
  /**
536
632
  * Polls the stored slide until it stops changing and no write the page sent
537
633
  * is still in flight. Saves are debounced, so "no change yet" is only trusted
@@ -897,11 +993,30 @@ async function runScenario(
897
993
  page.on("requestfailed", onWriteDone);
898
994
 
899
995
  try {
996
+ await page.goto(`${ctx.base}/home`, { waitUntil: "domcontentloaded" });
997
+ const priorPagehideWrites = await takeKeepaliveWrites(page);
998
+ const priorPagehideMismatches = keepaliveMismatches(
999
+ priorPagehideWrites,
1000
+ slideId,
1001
+ ctx.stored,
1002
+ );
900
1003
  await restoreSlide(page, deckId, slideId, ctx.stored);
1004
+ if (priorPagehideMismatches.length) {
1005
+ const restored = await settleSaved(
1006
+ page,
1007
+ deckId,
1008
+ slideId,
1009
+ () => inFlight.size,
1010
+ 15_000,
1011
+ );
1012
+ if (restored !== ctx.stored) {
1013
+ throw new Error(
1014
+ `${priorPagehideMismatches.length} prior pagehide write(s) overwrote the restored slide`,
1015
+ );
1016
+ }
1017
+ }
901
1018
  await openSlide(page, ctx.base, deckId, ctx.slideIndex, slideId);
902
- // Leaving the previous page fires pagehide after restoreSlide, so a
903
- // keepalive flush it sent can land after the restore and change the
904
- // slide this scenario starts from.
1019
+ // Earlier writes were checked after settling; only new writes can still overwrite the fixture.
905
1020
  const leftBehind = keepaliveMismatches(
906
1021
  await takeKeepaliveWrites(page),
907
1022
  slideId,
@@ -909,7 +1024,7 @@ async function runScenario(
909
1024
  );
910
1025
  if (leftBehind.length) {
911
1026
  throw new Error(
912
- `leaving the previous page sent ${leftBehind.length} keepalive write(s) that can overwrite the restored slide`,
1027
+ `${leftBehind.length} pagehide keepalive write(s) can overwrite the restored slide`,
913
1028
  );
914
1029
  }
915
1030
  const current = (await listTargets(page, slideId))[target.index];
@@ -1815,6 +1930,22 @@ async function main() {
1815
1930
  await warmUp(warm, base);
1816
1931
  await warm.close();
1817
1932
 
1933
+ if (typingChatOnly) {
1934
+ const page = await context.newPage();
1935
+ const problems = await runChatTypingRegression(page, base);
1936
+ await page.close();
1937
+ if (problems.length) {
1938
+ console.error(
1939
+ `[edit-fidelity] chat typing regression: ${problems.join("; ")}`,
1940
+ );
1941
+ return 1;
1942
+ }
1943
+ console.log(
1944
+ "[edit-fidelity] chat typing regression passed with a slide edit session open",
1945
+ );
1946
+ return 0;
1947
+ }
1948
+
1818
1949
  console.log(
1819
1950
  `[edit-fidelity] ${base} · ${cases.length} case(s) · scenarios ${scenarios.join(",")} · out ${outRoot}`,
1820
1951
  );
@@ -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)
@@ -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?: {