@agent-native/core 0.188.0 → 0.189.0-nightly-20260925200527

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 (53) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/actions/list-document-versions.ts +102 -2
  3. package/corpus/templates/content/actions/restore-document-version.ts +50 -2
  4. package/corpus/templates/content/app/components/layout/Layout.tsx +6 -1
  5. package/corpus/templates/content/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
  6. package/corpus/templates/content/server/lib/document-history.ts +59 -8
  7. package/corpus/templates/content/server/lib/document-version-context.ts +5 -0
  8. package/corpus/templates/content/server/plugins/agent-chat.ts +164 -0
  9. package/corpus/templates/design/actions/list-design-versions.ts +3 -1
  10. package/corpus/templates/design/app/components/layout/Layout.tsx +17 -2
  11. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +10 -3
  12. package/corpus/templates/design/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
  13. package/corpus/templates/design/server/lib/design-versions.ts +136 -20
  14. package/corpus/templates/design/server/plugins/agent-chat.ts +18 -2
  15. package/corpus/templates/slides/actions/list-deck-versions.ts +62 -10
  16. package/corpus/templates/slides/app/components/layout/Layout.tsx +10 -2
  17. package/corpus/templates/slides/changelog/2026-09-25-chat-messages-can-be-reverted-to-their-autosaved-version-inc.md +6 -0
  18. package/corpus/templates/slides/server/lib/deck-versions.ts +36 -1
  19. package/corpus/templates/slides/server/plugins/agent-chat.ts +26 -2
  20. package/dist/automation/index.d.ts +1 -1
  21. package/dist/client/AssistantChat.js +112 -35
  22. package/dist/client/chat/assistant-chat-history-version.d.ts +1 -0
  23. package/dist/client/chat/message-components.d.ts +8 -1
  24. package/dist/client/chat/message-components.js +52 -11
  25. package/dist/client/resources/mcp-integration-catalog.js +1 -1
  26. package/dist/client/resources/mcp-integration-logos.js +29 -30
  27. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  28. package/dist/localization/core-messages/ar-SA.js +2 -0
  29. package/dist/localization/core-messages/de-DE.js +2 -0
  30. package/dist/localization/core-messages/en-US.d.ts +2 -0
  31. package/dist/localization/core-messages/en-US.js +2 -0
  32. package/dist/localization/core-messages/es-ES.js +2 -0
  33. package/dist/localization/core-messages/fr-FR.js +2 -0
  34. package/dist/localization/core-messages/hi-IN.js +2 -0
  35. package/dist/localization/core-messages/ja-JP.js +2 -0
  36. package/dist/localization/core-messages/ko-KR.js +2 -0
  37. package/dist/localization/core-messages/pt-BR.js +2 -0
  38. package/dist/localization/core-messages/zh-CN.js +2 -0
  39. package/dist/localization/core-messages/zh-TW.js +2 -0
  40. package/dist/localization/core-messages.d.ts +2 -0
  41. package/dist/notifications/routes.d.ts +3 -3
  42. package/dist/observability/routes.d.ts +4 -4
  43. package/dist/org/handlers.d.ts +1 -1
  44. package/dist/resources/handlers.d.ts +1 -1
  45. package/dist/secrets/routes.d.ts +6 -6
  46. package/dist/server/agent-chat/plugin-options.d.ts +2 -0
  47. package/dist/server/agent-chat-plugin.d.ts +1 -0
  48. package/dist/server/agent-chat-plugin.js +26 -0
  49. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  50. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  51. package/dist/server/onboarding-html.js +28 -5
  52. package/docs/content/deployment.mdx +12 -1
  53. package/package.json +1 -1
@@ -18,7 +18,7 @@ import {
18
18
  import { captureError } from "@agent-native/core/server";
19
19
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
20
20
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
21
- import { and, asc, desc, eq, isNull, sql } from "drizzle-orm";
21
+ import { and, asc, desc, eq, isNull, like, sql } from "drizzle-orm";
22
22
  import { nanoid } from "nanoid";
23
23
 
24
24
  import {
@@ -49,6 +49,7 @@ export interface DesignVersionChatContext {
49
49
  runId?: string;
50
50
  turnId?: string;
51
51
  actionName?: string;
52
+ phase?: "start" | "end";
52
53
  surface?: "editor";
53
54
  /** Which editor-surface caller wrote this checkpoint. Only set alongside
54
55
  * `surface: "editor"` — see Throttle 1 below for why it matters. */
@@ -172,6 +173,15 @@ function stableStringify(value: unknown): string {
172
173
  .join(",")}}`;
173
174
  }
174
175
 
176
+ function designVersionIdForIdempotencyKey(
177
+ designId: string,
178
+ idempotencyKey: string,
179
+ ) {
180
+ return `design-version-${createHash("sha256")
181
+ .update(stableStringify({ designId, idempotencyKey }))
182
+ .digest("hex")}`;
183
+ }
184
+
175
185
  function nextRevisionTimestamp(previous: string | null | undefined): string {
176
186
  const previousMs = previous ? Date.parse(previous) : Number.NaN;
177
187
  return new Date(
@@ -204,6 +214,9 @@ function parseChatContext(
204
214
  context[key] = candidate;
205
215
  }
206
216
  }
217
+ if (value.phase === "start" || value.phase === "end") {
218
+ context.phase = value.phase;
219
+ }
207
220
  if (value.surface === "editor") context.surface = "editor";
208
221
  if (value.caller === "frontend" || value.caller === "webmcp") {
209
222
  context.caller = value.caller;
@@ -414,7 +427,7 @@ function chatContextKey(
414
427
  if (!context || context.surface === "editor") return null;
415
428
  const scope = context.threadId ?? "";
416
429
  const turn = context.turnId ?? context.runId ?? "";
417
- return turn ? `${scope}:${turn}` : null;
430
+ return turn ? `${scope}:${turn}:${context.phase ?? ""}` : null;
418
431
  }
419
432
 
420
433
  function actionChatContext(
@@ -621,6 +634,7 @@ async function captureDesignVersion(
621
634
  chatContext?: DesignVersionChatContext;
622
635
  deletionGeometry?: ComponentDeletionGeometry;
623
636
  preferStoredFileContent?: boolean;
637
+ idempotencyKey?: string;
624
638
  },
625
639
  access: DesignAccess,
626
640
  database?: DesignDatabase,
@@ -735,16 +749,18 @@ async function captureDesignVersion(
735
749
  };
736
750
  }
737
751
  }
738
- const id = `design-version-${createHash("sha256")
739
- .update(
740
- stableStringify({
741
- designId,
742
- previousVersionId: latest?.id ?? "initial",
743
- chatContextKey: chatContextKey(options.chatContext),
744
- stateHash,
745
- }),
746
- )
747
- .digest("hex")}`;
752
+ const id = options.idempotencyKey
753
+ ? designVersionIdForIdempotencyKey(designId, options.idempotencyKey)
754
+ : `design-version-${createHash("sha256")
755
+ .update(
756
+ stableStringify({
757
+ designId,
758
+ previousVersionId: latest?.id ?? "initial",
759
+ chatContextKey: chatContextKey(options.chatContext),
760
+ stateHash,
761
+ }),
762
+ )
763
+ .digest("hex")}`;
748
764
  const snapshot = JSON.stringify({
749
765
  schemaVersion: 1,
750
766
  snapshotKind: "design-history",
@@ -877,6 +893,41 @@ export async function createDesignVersionSnapshot(
877
893
  });
878
894
  }
879
895
 
896
+ export async function createDesignChatBeginningSnapshot(
897
+ designId: string,
898
+ run: { threadId: string; runId: string },
899
+ ) {
900
+ return withDesignVersionLock(designId, async () => {
901
+ const access = await assertAccess("design", designId, "editor");
902
+ const rows = await getDb()
903
+ .select({ id: schema.designVersions.id })
904
+ .from(schema.designVersions)
905
+ .where(
906
+ and(
907
+ eq(schema.designVersions.designId, designId),
908
+ eq(
909
+ schema.designVersions.id,
910
+ designVersionIdForIdempotencyKey(
911
+ designId,
912
+ `chat-start:${run.threadId}`,
913
+ ),
914
+ ),
915
+ ),
916
+ )
917
+ .limit(1);
918
+ if (rows.length) return null;
919
+ return captureDesignVersion(
920
+ designId,
921
+ {
922
+ label: "Before chat",
923
+ chatContext: { ...run, phase: "start" },
924
+ idempotencyKey: `chat-start:${run.threadId}`,
925
+ },
926
+ access,
927
+ );
928
+ });
929
+ }
930
+
880
931
  function checkpointSkipReason(
881
932
  error: unknown,
882
933
  ): DesignVersionCheckpointSkipReason {
@@ -1140,6 +1191,7 @@ export async function snapshotDesignBeforeAgentEditInVersionLock(
1140
1191
  export async function listDesignVersions(
1141
1192
  designId: string,
1142
1193
  limit: number,
1194
+ threadId?: string,
1143
1195
  ): Promise<{
1144
1196
  designId: string;
1145
1197
  count: number;
@@ -1147,7 +1199,8 @@ export async function listDesignVersions(
1147
1199
  versions: DesignVersionListEntry[];
1148
1200
  }> {
1149
1201
  await assertAccess("design", designId, "viewer");
1150
- const rows = await getDb()
1202
+ const db = getDb();
1203
+ const rows = await db
1151
1204
  .select({
1152
1205
  id: schema.designVersions.id,
1153
1206
  label: schema.designVersions.label,
@@ -1163,11 +1216,46 @@ export async function listDesignVersions(
1163
1216
  desc(schema.designVersions.id),
1164
1217
  )
1165
1218
  .limit(limit);
1219
+ const beginningRows = await db
1220
+ .select({
1221
+ id: schema.designVersions.id,
1222
+ label: schema.designVersions.label,
1223
+ createdAt: schema.designVersions.createdAt,
1224
+ chatContext: schema.designVersions.chatContext,
1225
+ fileCount: schema.designVersions.fileCount,
1226
+ })
1227
+ .from(schema.designVersions)
1228
+ .where(
1229
+ threadId
1230
+ ? and(
1231
+ eq(schema.designVersions.designId, designId),
1232
+ eq(
1233
+ schema.designVersions.id,
1234
+ designVersionIdForIdempotencyKey(
1235
+ designId,
1236
+ `chat-start:${threadId}`,
1237
+ ),
1238
+ ),
1239
+ )
1240
+ : and(
1241
+ eq(schema.designVersions.designId, designId),
1242
+ like(schema.designVersions.chatContext, '%"phase":"start"%'),
1243
+ ),
1244
+ )
1245
+ .orderBy(
1246
+ asc(isNull(schema.designVersions.createdAt)),
1247
+ asc(schema.designVersions.createdAt),
1248
+ )
1249
+ .limit(threadId ? 1 : limit);
1250
+ const rowsById = new Map(
1251
+ [...rows, ...beginningRows].map((row) => [row.id, row]),
1252
+ );
1166
1253
 
1167
1254
  const regular: DesignVersionListEntry[] = [];
1168
1255
  const chat = new Map<string, DesignVersionListEntry>();
1256
+ const activeStartEntries: DesignVersionListEntry[] = [];
1169
1257
  let invalidCount = 0;
1170
- for (const row of rows) {
1258
+ for (const row of rowsById.values()) {
1171
1259
  let chatContext: DesignVersionChatContext | undefined;
1172
1260
  try {
1173
1261
  chatContext = parseStoredChatContext(row.chatContext);
@@ -1195,19 +1283,47 @@ export async function listDesignVersions(
1195
1283
  regular.push(entry);
1196
1284
  continue;
1197
1285
  }
1198
- const previous = chat.get(key);
1199
1286
  if (
1200
- !previous ||
1201
- versionTime(entry.createdAt) < versionTime(previous.createdAt)
1287
+ threadId &&
1288
+ chatContext?.threadId === threadId &&
1289
+ chatContext.phase === "start"
1202
1290
  ) {
1203
- chat.set(key, entry);
1291
+ activeStartEntries.push(entry);
1292
+ continue;
1204
1293
  }
1294
+ const previous = chat.get(key);
1295
+ const replacePrevious =
1296
+ !previous ||
1297
+ (chatContext?.phase === "end"
1298
+ ? versionTime(entry.createdAt) > versionTime(previous.createdAt)
1299
+ : versionTime(entry.createdAt) < versionTime(previous.createdAt));
1300
+ if (replacePrevious) chat.set(key, entry);
1205
1301
  }
1206
1302
 
1207
- const versions = [...regular, ...chat.values()].sort(
1303
+ const versions = [...regular, ...chat.values(), ...activeStartEntries].sort(
1208
1304
  (left, right) => versionTime(right.createdAt) - versionTime(left.createdAt),
1209
1305
  );
1210
- return { designId, count: versions.length, invalidCount, versions };
1306
+ const limitedVersions = versions.slice(0, limit);
1307
+ const activeStart = threadId
1308
+ ? versions.find(
1309
+ (version) =>
1310
+ version.chatContext?.threadId === threadId &&
1311
+ version.chatContext.phase === "start",
1312
+ )
1313
+ : undefined;
1314
+ if (activeStart && !limitedVersions.includes(activeStart)) {
1315
+ limitedVersions[limitedVersions.length - 1] = activeStart;
1316
+ limitedVersions.sort(
1317
+ (left, right) =>
1318
+ versionTime(right.createdAt) - versionTime(left.createdAt),
1319
+ );
1320
+ }
1321
+ return {
1322
+ designId,
1323
+ count: limitedVersions.length,
1324
+ invalidCount,
1325
+ versions: limitedVersions,
1326
+ };
1211
1327
  }
1212
1328
 
1213
1329
  interface RestoreFile {
@@ -232,21 +232,37 @@ async function autosaveDesignAfterAgentTurn(
232
232
  },
233
233
  ): Promise<void> {
234
234
  if (scope.type !== "design" || !(await hasDesignEdit(run, scope.id))) return;
235
+ if (!run.threadId || !run.runId) return;
235
236
 
236
237
  const { createDesignVersionSnapshot } =
237
238
  await import("../lib/design-versions.js");
238
239
  await createDesignVersionSnapshot(scope.id, {
239
240
  label: "Chat autosave",
240
241
  chatContext: {
241
- ...(run.threadId ? { threadId: run.threadId } : {}),
242
- ...(run.runId ? { runId: run.runId } : {}),
242
+ threadId: run.threadId,
243
+ runId: run.runId,
243
244
  ...(run.turnId ? { turnId: run.turnId } : {}),
245
+ phase: "end",
244
246
  },
245
247
  });
246
248
  }
247
249
 
250
+ async function autosaveDesignBeforeAgentTurn(
251
+ scope: { type: string; id: string },
252
+ run: { threadId?: string; runId?: string },
253
+ ): Promise<void> {
254
+ if (scope.type !== "design" || !run.threadId || !run.runId) return;
255
+ const { createDesignChatBeginningSnapshot } =
256
+ await import("../lib/design-versions.js");
257
+ await createDesignChatBeginningSnapshot(scope.id, {
258
+ threadId: run.threadId,
259
+ runId: run.runId,
260
+ });
261
+ }
262
+
248
263
  export default createAgentChatPlugin({
249
264
  appId: "design",
265
+ onAgentTurnStart: autosaveDesignBeforeAgentTurn,
250
266
  onAgentTurnComplete: autosaveDesignAfterAgentTurn,
251
267
  actions: guardRepromptActionRegistry(
252
268
  loadActionsFromStaticRegistry(actionsRegistry),
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { assertAccess } from "@agent-native/core/sharing";
3
- import { and, desc, eq } from "drizzle-orm";
3
+ import { and, asc, desc, eq, like } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
@@ -50,13 +50,18 @@ export default defineAction({
50
50
  schema: z.object({
51
51
  deckId: z.string().describe("Deck ID"),
52
52
  limit: z.coerce.number().int().min(1).max(100).default(50),
53
+ threadId: z.string().min(1).optional(),
53
54
  }),
54
55
  http: { method: "GET" },
55
- run: async ({ deckId, limit }) => {
56
+ run: async ({ deckId, limit, threadId }) => {
56
57
  const access = await assertAccess("deck", deckId, "viewer");
57
58
  const ownerEmail = access.resource.ownerEmail as string;
58
59
  const db = getDb();
59
60
 
61
+ const where = and(
62
+ eq(schema.deckVersions.deckId, deckId),
63
+ eq(schema.deckVersions.ownerEmail, ownerEmail),
64
+ );
60
65
  const versions = await db
61
66
  .select({
62
67
  id: schema.deckVersions.id,
@@ -68,19 +73,66 @@ export default defineAction({
68
73
  createdAt: schema.deckVersions.createdAt,
69
74
  })
70
75
  .from(schema.deckVersions)
71
- .where(
72
- and(
73
- eq(schema.deckVersions.deckId, deckId),
74
- eq(schema.deckVersions.ownerEmail, ownerEmail),
75
- ),
76
- )
76
+ .where(where)
77
77
  .orderBy(desc(schema.deckVersions.createdAt))
78
78
  .limit(limit);
79
+ const beginningVersions = await db
80
+ .select({
81
+ id: schema.deckVersions.id,
82
+ deckId: schema.deckVersions.deckId,
83
+ title: schema.deckVersions.title,
84
+ data: schema.deckVersions.data,
85
+ changeLabel: schema.deckVersions.changeLabel,
86
+ chatContext: schema.deckVersions.chatContext,
87
+ createdAt: schema.deckVersions.createdAt,
88
+ })
89
+ .from(schema.deckVersions)
90
+ .where(
91
+ threadId
92
+ ? and(
93
+ where,
94
+ eq(schema.deckVersions.changeGroup, `start:thread:${threadId}`),
95
+ )
96
+ : and(
97
+ where,
98
+ like(schema.deckVersions.chatContext, '%"phase":"start"%'),
99
+ ),
100
+ )
101
+ .orderBy(
102
+ threadId
103
+ ? asc(schema.deckVersions.createdAt)
104
+ : desc(schema.deckVersions.createdAt),
105
+ )
106
+ .limit(threadId ? 1 : limit);
107
+ const versionsById = new Map(
108
+ [...beginningVersions, ...versions].map((version) => [
109
+ version.id,
110
+ version,
111
+ ]),
112
+ );
113
+ const allVersions = [...versionsById.values()];
114
+ allVersions.sort(
115
+ (left, right) =>
116
+ new Date(right.createdAt).getTime() -
117
+ new Date(left.createdAt).getTime(),
118
+ );
119
+ const limitedVersions = allVersions.slice(0, limit);
120
+ const activeStart = threadId
121
+ ? allVersions.find((version) => beginningVersions.includes(version))
122
+ : undefined;
123
+ if (activeStart && !limitedVersions.includes(activeStart)) {
124
+ limitedVersions[limitedVersions.length - 1] = activeStart;
125
+ limitedVersions.sort(
126
+ (left, right) =>
127
+ new Date(right.createdAt).getTime() -
128
+ new Date(left.createdAt).getTime(),
129
+ );
130
+ }
79
131
 
80
132
  return {
81
133
  deckId,
82
- count: versions.length,
83
- versions: versions.map((version) => {
134
+ count: limitedVersions.length,
135
+ versions: limitedVersions.map((version) => {
84
136
  let chatContext;
85
137
  try {
86
138
  chatContext = parseDeckVersionChatContext(version.chatContext);
@@ -21,6 +21,7 @@ import { IconMenu2 } from "@tabler/icons-react";
21
21
  import { useEffect, useMemo, useRef, useState } from "react";
22
22
  import { useLocation, useNavigate } from "react-router";
23
23
 
24
+ import { useDecks } from "@/context/DeckContext";
24
25
  import { useSidebarCollapsed } from "@/hooks/use-sidebar-collapsed";
25
26
  import {
26
27
  buildSlidesAgentContext,
@@ -67,6 +68,7 @@ export function Layout({ children }: LayoutProps) {
67
68
  const location = useLocation();
68
69
  const navigate = useNavigate();
69
70
  const t = useT();
71
+ const { flushDeckSave } = useDecks();
70
72
  const creativeContextEnabled = useCreativeContextLab();
71
73
  const isChatRoute =
72
74
  location.pathname === "/chat" || location.pathname.startsWith("/chat/");
@@ -155,9 +157,14 @@ export function Layout({ children }: LayoutProps) {
155
157
  if (!deckScope) return undefined;
156
158
  const deckId = deckScope.id;
157
159
  return {
160
+ beforeStart: () => flushDeckSave(deckId),
158
161
  list: {
159
162
  action: "list-deck-versions",
160
- args: { deckId, limit: 100 },
163
+ args: (threadId) => ({
164
+ deckId,
165
+ limit: 100,
166
+ ...(threadId ? { threadId } : {}),
167
+ }),
161
168
  getVersions: (result: unknown) => {
162
169
  const versions =
163
170
  result && typeof result === "object"
@@ -174,9 +181,10 @@ export function Layout({ children }: LayoutProps) {
174
181
  deckId,
175
182
  versionId: version.id,
176
183
  }),
184
+ beforeRestore: () => flushDeckSave(deckId),
177
185
  },
178
186
  };
179
- }, [deckScope]);
187
+ }, [deckScope, flushDeckSave]);
180
188
 
181
189
  useAgentChatHomeHandoffLinks({
182
190
  storageKey: "slides",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Chat messages can be reverted to their autosaved version, including a shortcut to restore the start of a chat.
@@ -13,12 +13,14 @@ export interface DeckVersionChatContext {
13
13
  threadId?: string;
14
14
  runId?: string;
15
15
  turnId?: string;
16
+ phase?: "start" | "end";
16
17
  }
17
18
 
18
19
  function contextFromFields(value: {
19
20
  threadId?: unknown;
20
21
  runId?: unknown;
21
22
  turnId?: unknown;
23
+ phase?: unknown;
22
24
  }): DeckVersionChatContext | undefined {
23
25
  const context: DeckVersionChatContext = {};
24
26
  for (const key of ["threadId", "runId", "turnId"] as const) {
@@ -26,6 +28,9 @@ function contextFromFields(value: {
26
28
  context[key] = value[key];
27
29
  }
28
30
  }
31
+ if (value.phase === "start" || value.phase === "end") {
32
+ context.phase = value.phase;
33
+ }
29
34
  return Object.keys(context).length > 0 ? context : undefined;
30
35
  }
31
36
 
@@ -139,6 +144,7 @@ export async function createDeckVersionSnapshot(
139
144
  .limit(1);
140
145
 
141
146
  if (
147
+ options.chatContext?.phase !== "start" &&
142
148
  latestVersion &&
143
149
  latestVersion.title === source.title &&
144
150
  deckContentSignature(latestVersion.data) ===
@@ -150,8 +156,12 @@ export async function createDeckVersionSnapshot(
150
156
  const requestedChatContext = serializeDeckVersionChatContext(
151
157
  options.chatContext,
152
158
  );
153
- const changeGroup =
159
+ const turnGroup =
154
160
  options.chatContext?.turnId ?? options.chatContext?.runId ?? undefined;
161
+ const changeGroup =
162
+ options.chatContext?.phase === "start" && turnGroup
163
+ ? `start:thread:${options.chatContext.threadId ?? turnGroup}`
164
+ : turnGroup;
155
165
  if (
156
166
  requestedChatContext &&
157
167
  requestedChatContext === normalizedChatContext(latestVersion?.chatContext)
@@ -196,3 +206,28 @@ export async function createDeckVersionSnapshot(
196
206
 
197
207
  return { created: true, id };
198
208
  }
209
+
210
+ export async function createDeckChatBeginningSnapshot(
211
+ source: DeckSnapshotSource,
212
+ run: { threadId: string; runId: string },
213
+ ): Promise<{ created: boolean; id?: string; reason?: string }> {
214
+ const rows = await getDb()
215
+ .select({ id: schema.deckVersions.id })
216
+ .from(schema.deckVersions)
217
+ .where(
218
+ and(
219
+ eq(schema.deckVersions.deckId, source.id),
220
+ eq(schema.deckVersions.ownerEmail, source.ownerEmail),
221
+ eq(schema.deckVersions.changeGroup, `start:thread:${run.threadId}`),
222
+ ),
223
+ )
224
+ .limit(1);
225
+ if (rows.length) {
226
+ return { created: false, reason: "beginning-exists" };
227
+ }
228
+ return createDeckVersionSnapshot(source, {
229
+ force: true,
230
+ label: "Before chat",
231
+ chatContext: { ...run, phase: "start" },
232
+ });
233
+ }
@@ -7,7 +7,10 @@ import { assertAccess } from "@agent-native/core/sharing";
7
7
  import actionsRegistry from "../../.generated/actions-registry.js";
8
8
  import { resolveSlidesRequestAuthContext } from "../handlers/request-auth-context.js";
9
9
  import { prepareSlidesChatAttachments } from "../lib/chat-attachments.js";
10
- import { deckVersionChatContextFromRun } from "../lib/deck-versions.js";
10
+ import {
11
+ createDeckChatBeginningSnapshot,
12
+ deckVersionChatContextFromRun,
13
+ } from "../lib/deck-versions.js";
11
14
  import "../register-secrets.js";
12
15
 
13
16
  const SLIDES_BACKGROUND_RUN_SOFT_TIMEOUT_MS = 13 * 60_000;
@@ -138,6 +141,7 @@ async function autosaveDeckAfterAgentTurn(
138
141
  },
139
142
  ): Promise<void> {
140
143
  if (scope.type !== "deck" || !hasDeckEdit(run, scope.id)) return;
144
+ if (!run.threadId || !run.runId) return;
141
145
 
142
146
  const access = await assertAccess("deck", scope.id, "editor");
143
147
  const deck = access.resource as {
@@ -147,15 +151,35 @@ async function autosaveDeckAfterAgentTurn(
147
151
  ownerEmail: string;
148
152
  };
149
153
  const { createDeckVersionSnapshot } = await import("../lib/deck-versions.js");
154
+ const chatContext = deckVersionChatContextFromRun(run);
150
155
  await createDeckVersionSnapshot(deck, {
151
156
  force: true,
152
157
  label: "Chat autosave",
153
- chatContext: deckVersionChatContextFromRun(run),
158
+ chatContext: chatContext ? { ...chatContext, phase: "end" } : undefined,
159
+ });
160
+ }
161
+
162
+ async function autosaveDeckBeforeAgentTurn(
163
+ scope: { type: string; id: string },
164
+ run: { threadId?: string; runId?: string },
165
+ ): Promise<void> {
166
+ if (scope.type !== "deck" || !run.threadId || !run.runId) return;
167
+ const access = await assertAccess("deck", scope.id, "editor");
168
+ const deck = access.resource as {
169
+ id: string;
170
+ title: string;
171
+ data: string;
172
+ ownerEmail: string;
173
+ };
174
+ await createDeckChatBeginningSnapshot(deck, {
175
+ threadId: run.threadId,
176
+ runId: run.runId,
154
177
  });
155
178
  }
156
179
 
157
180
  export default createAgentChatPlugin({
158
181
  appId: "slides",
182
+ onAgentTurnStart: autosaveDeckBeforeAgentTurn,
159
183
  onAgentTurnComplete: autosaveDeckAfterAgentTurn,
160
184
  actions: loadActionsFromStaticRegistry(actionsRegistry),
161
185
  initialToolNames: INITIAL_TOOL_NAMES,
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
186
186
  duplicate?: undefined;
187
187
  eventId?: undefined;
188
188
  } | {
189
- error?: undefined;
190
189
  accepted: boolean;
191
190
  duplicate: boolean;
192
191
  eventId: string;
192
+ error?: undefined;
193
193
  }>;