@agent-native/core 0.180.0-nightly-20260915184036 → 0.180.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 (77) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +39 -0
  4. package/corpus/templates/calendar/INTERACTION-MATRIX.md +19 -17
  5. package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +21 -0
  6. package/corpus/templates/calendar/actions/view-screen.ts +10 -2
  7. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +11 -0
  8. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +174 -145
  9. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +20 -16
  10. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +1 -0
  11. package/corpus/templates/calendar/app/hooks/use-events.ts +16 -0
  12. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +126 -14
  13. package/corpus/templates/calendar/app/i18n-data.ts +90 -0
  14. package/corpus/templates/calendar/app/lib/calendar-navigation.ts +29 -9
  15. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +2 -0
  16. package/corpus/templates/calendar/app/lib/event-time-range.ts +34 -0
  17. package/corpus/templates/calendar/app/pages/CalendarView.tsx +245 -35
  18. package/corpus/templates/calendar/changelog/2026-09-13-calendar-sidebar-overflow-is-easier-to-discover.md +6 -0
  19. package/corpus/templates/calendar/changelog/2026-09-15-calendar-mirrors-notion-calendar-s-customizable-day-ranges-a.md +6 -0
  20. package/corpus/templates/calendar/shared/calendar-view-preferences.ts +36 -0
  21. package/corpus/templates/calendar/shared/timezone.ts +9 -2
  22. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +2 -0
  23. package/corpus/templates/clips/CHANGELOG.md +19 -0
  24. package/corpus/templates/clips/actions/save-browser-diagnostics.ts +43 -4
  25. package/corpus/templates/clips/app/components/player/browser-diagnostics-panel.tsx +114 -1
  26. package/corpus/templates/clips/app/i18n/en-US.ts +7 -0
  27. package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +108 -0
  28. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  29. package/corpus/templates/clips/changelog/2026-09-15-recordings-include-a-structured-timeline-of-browser-interact.md +6 -0
  30. package/corpus/templates/clips/chrome-extension/public/manifest.json +7 -0
  31. package/corpus/templates/clips/chrome-extension/src/background.ts +235 -9
  32. package/corpus/templates/clips/chrome-extension/src/content-history-bridge.ts +80 -0
  33. package/corpus/templates/clips/chrome-extension/src/content-script.ts +166 -5
  34. package/corpus/templates/clips/chrome-extension/vite.config.ts +4 -0
  35. package/corpus/templates/clips/server/db/schema.ts +3 -0
  36. package/corpus/templates/clips/server/lib/public-agent-context.ts +40 -2
  37. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  38. package/corpus/templates/clips/shared/browser-diagnostics.ts +171 -0
  39. package/corpus/templates/content/.agents/skills/content/SKILL.md +4 -2
  40. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +8 -1
  41. package/corpus/templates/content/AGENTS.md +5 -6
  42. package/corpus/templates/content/CHANGELOG.md +24 -0
  43. package/corpus/templates/content/actions/_content-database-row-migration.ts +67 -36
  44. package/corpus/templates/content/actions/_document-edit-mutation.ts +74 -23
  45. package/corpus/templates/content/actions/edit-document.ts +48 -8
  46. package/corpus/templates/content/actions/migrate-content-database-rows.ts +69 -28
  47. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -1
  48. package/corpus/templates/design/CHANGELOG.md +22 -0
  49. package/corpus/templates/factory/CHANGELOG.md +11 -0
  50. package/corpus/templates/forms/CHANGELOG.md +6 -0
  51. package/corpus/templates/mail/.agents/skills/mail-interaction-parity/references/interaction-matrix.md +24 -4
  52. package/corpus/templates/mail/CHANGELOG.md +68 -0
  53. package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +8 -0
  54. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +33 -13
  55. package/corpus/templates/mail/app/components/email/EmailListItem.tsx +23 -4
  56. package/corpus/templates/mail/app/components/email/EmailThread.tsx +56 -13
  57. package/corpus/templates/mail/app/components/email/InlineReplyComposer.tsx +23 -4
  58. package/corpus/templates/mail/app/components/email/RecipientInput.tsx +9 -0
  59. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +8 -1
  60. package/corpus/templates/mail/app/i18n/en-US.ts +31 -0
  61. package/corpus/templates/mail/changelog/2026-09-15-mail-labels-recipient-and-account-controls-for-keyboard-and-.md +6 -0
  62. package/corpus/templates/slides/CHANGELOG.md +32 -0
  63. package/dist/client/CommandMenu.js +14 -2
  64. package/dist/client/settings/AccountSettingsCard.js +3 -2
  65. package/dist/client/settings/SettingsTabsPage.js +9 -1
  66. package/dist/client/settings/agent-settings-search.js +5 -1
  67. package/dist/client/sign-out.d.ts +1 -0
  68. package/dist/client/sign-out.js +8 -0
  69. package/dist/collab/routes.d.ts +1 -1
  70. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  71. package/dist/observability/routes.d.ts +3 -3
  72. package/dist/org/handlers.d.ts +1 -1
  73. package/dist/resources/handlers.d.ts +1 -1
  74. package/dist/secrets/routes.d.ts +3 -3
  75. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  76. package/dist/server/transcribe-voice.d.ts +1 -1
  77. package/package.json +4 -4
@@ -13,6 +13,8 @@ import {
13
13
  import { chunks } from "./_batch-utils.js";
14
14
 
15
15
  const propertyType = z.enum(["text", "url", "date", "multi_select"]);
16
+ export const MAX_MIGRATION_ROWS = 250;
17
+ export const MAX_MIGRATION_PLAN_BYTES = 5 * 1024 * 1024;
16
18
  const option = z.object({
17
19
  id: z.string().min(1),
18
20
  name: z.string().min(1),
@@ -28,42 +30,56 @@ const option = z.object({
28
30
  "red",
29
31
  ]),
30
32
  });
31
- export const migrationPlanSchema = z.object({
32
- databaseId: z.string().min(1),
33
- databaseDocumentId: z.string().min(1),
34
- idempotencyKey: z.string().min(1).max(200),
35
- expectedRowCount: z.number().int().min(1).max(100),
36
- propertyDefinitions: z
37
- .array(
38
- z.object({
39
- id: z.string().min(1),
40
- name: z.string().min(1),
41
- type: propertyType,
42
- visibility: z.enum(DOCUMENT_PROPERTY_VISIBILITIES),
43
- options: z.array(option).max(100).optional(),
44
- }),
45
- )
46
- .max(100),
47
- rows: z
48
- .array(
49
- z.object({
50
- itemId: z.string().min(1),
51
- documentId: z.string().min(1),
52
- expectedUpdatedAt: z.string().min(1),
53
- content: z.string(),
54
- propertyValues: z.array(
55
- z.object({ propertyId: z.string().min(1), value: z.unknown() }),
56
- ),
57
- protectedPropertyValues: z
58
- .array(
59
- z.object({ propertyId: z.string().min(1), valueJson: z.string() }),
60
- )
61
- .max(100),
62
- }),
63
- )
64
- .max(100),
65
- legacyPropertyIds: z.array(z.string().min(1)).max(100).default([]),
66
- });
33
+ export const migrationPlanSchema = z
34
+ .object({
35
+ databaseId: z.string().min(1),
36
+ databaseDocumentId: z.string().min(1),
37
+ idempotencyKey: z.string().min(1).max(200),
38
+ expectedRowCount: z.number().int().min(1).max(MAX_MIGRATION_ROWS),
39
+ propertyDefinitions: z
40
+ .array(
41
+ z.object({
42
+ id: z.string().min(1),
43
+ name: z.string().min(1),
44
+ type: propertyType,
45
+ visibility: z.enum(DOCUMENT_PROPERTY_VISIBILITIES),
46
+ options: z.array(option).max(100).optional(),
47
+ }),
48
+ )
49
+ .max(100),
50
+ rows: z
51
+ .array(
52
+ z.object({
53
+ itemId: z.string().min(1),
54
+ documentId: z.string().min(1),
55
+ expectedUpdatedAt: z.string().min(1),
56
+ content: z.string(),
57
+ propertyValues: z.array(
58
+ z.object({ propertyId: z.string().min(1), value: z.unknown() }),
59
+ ),
60
+ protectedPropertyValues: z
61
+ .array(
62
+ z.object({
63
+ propertyId: z.string().min(1),
64
+ valueJson: z.string(),
65
+ }),
66
+ )
67
+ .max(100),
68
+ }),
69
+ )
70
+ .max(MAX_MIGRATION_ROWS),
71
+ legacyPropertyIds: z.array(z.string().min(1)).max(100).default([]),
72
+ })
73
+ .superRefine((plan, context) => {
74
+ if (
75
+ Buffer.byteLength(JSON.stringify(plan), "utf8") > MAX_MIGRATION_PLAN_BYTES
76
+ ) {
77
+ context.addIssue({
78
+ code: "custom",
79
+ message: `Migration plan is limited to ${MAX_MIGRATION_PLAN_BYTES} bytes.`,
80
+ });
81
+ }
82
+ });
67
83
  export type MigrationPlan = z.infer<typeof migrationPlanSchema>;
68
84
 
69
85
  export function canonical(value: unknown): string {
@@ -462,6 +478,21 @@ export function snapshotDigest(
462
478
  });
463
479
  }
464
480
 
481
+ export function snapshotBodyRevisionDigest(
482
+ snapshot: Awaited<ReturnType<typeof snapshotMigration>>,
483
+ ) {
484
+ return digest([
485
+ {
486
+ documentId: snapshot.databaseDocument.id,
487
+ bodyRevision: snapshot.databaseDocument.bodyRevision,
488
+ },
489
+ ...snapshot.rows.map((row: any) => ({
490
+ documentId: row.document.id,
491
+ bodyRevision: row.document.bodyRevision,
492
+ })),
493
+ ]);
494
+ }
495
+
465
496
  export async function applyMigration(
466
497
  tx: any,
467
498
  plan: MigrationPlan,
@@ -64,7 +64,8 @@ function validateDocumentEditSnapshot(
64
64
  document: typeof schema.documents.$inferSelect | undefined,
65
65
  base: { revision: number; contentHash: string },
66
66
  baseRevision: string,
67
- edits: DocumentTextEdit[],
67
+ edits: DocumentTextEdit[] | undefined,
68
+ initializeContent?: string,
68
69
  ) {
69
70
  if (!document) {
70
71
  throw new ActionContractError("Document not found.", {
@@ -88,7 +89,19 @@ function validateDocumentEditSnapshot(
88
89
  currentContentHash: beforeHash,
89
90
  });
90
91
  }
91
- const resolved = resolveDocumentTextEdits(beforeContent, edits);
92
+ const resolved = edits
93
+ ? resolveDocumentTextEdits(beforeContent, edits)
94
+ : beforeContent === ""
95
+ ? {
96
+ ok: true as const,
97
+ content: initializeContent as string,
98
+ ranges: [{ start: 0, end: 0 }],
99
+ }
100
+ : conflict(
101
+ "DOCUMENT_BODY_NOT_EMPTY",
102
+ "Document initialization requires a literally empty body.",
103
+ { currentRevision: baseRevision },
104
+ );
92
105
  if (!resolved.ok) {
93
106
  conflict(
94
107
  resolved.error.kind === "missing"
@@ -178,19 +191,24 @@ export interface DocumentEditCreativeContext {
178
191
  }>;
179
192
  }
180
193
 
181
- export async function mutateDocumentBody(args: {
182
- documentId: string;
183
- baseRevision: string;
184
- idempotencyKey: string;
185
- edits: DocumentTextEdit[];
186
- creativeContext?: DocumentEditCreativeContext;
187
- creativeContextDigest?: unknown;
188
- resolveCreativeContext?: () => Promise<
189
- DocumentEditCreativeContext | undefined
190
- >;
191
- ctx: ActionRunContext;
192
- db?: Db;
193
- }): Promise<DocumentEditMutationResult> {
194
+ type DocumentBodyMutation =
195
+ | { edits: DocumentTextEdit[]; initializeContent?: never }
196
+ | { edits?: never; initializeContent: string };
197
+
198
+ export async function mutateDocumentBody(
199
+ args: {
200
+ documentId: string;
201
+ baseRevision: string;
202
+ idempotencyKey: string;
203
+ creativeContext?: DocumentEditCreativeContext;
204
+ creativeContextDigest?: unknown;
205
+ resolveCreativeContext?: () => Promise<
206
+ DocumentEditCreativeContext | undefined
207
+ >;
208
+ ctx: ActionRunContext;
209
+ db?: Db;
210
+ } & DocumentBodyMutation,
211
+ ): Promise<DocumentEditMutationResult> {
194
212
  const db = args.db ?? getDb();
195
213
  const scope = callerScope(args.ctx);
196
214
  const base = parseDocumentRevisionToken(args.baseRevision);
@@ -203,16 +221,47 @@ export async function mutateDocumentBody(args: {
203
221
  },
204
222
  );
205
223
  }
206
- const normalizedEdits = args.edits.map(({ find, replace }) => ({
224
+ const normalizedEdits = args.edits?.map(({ find, replace }) => ({
207
225
  find,
208
226
  replace,
209
227
  }));
210
- const payloadDigest = digest({
211
- documentId: args.documentId,
212
- baseRevision: args.baseRevision,
213
- edits: normalizedEdits,
214
- creativeContext: args.creativeContextDigest ?? args.creativeContext ?? null,
215
- });
228
+ const initializationContent = args.initializeContent;
229
+ if (!normalizedEdits && initializationContent === undefined) {
230
+ throw new ActionContractError("A document body mutation is required.", {
231
+ errorCode: "DOCUMENT_EDIT_MODE_REQUIRED",
232
+ statusCode: 400,
233
+ });
234
+ }
235
+ if (
236
+ initializationContent !== undefined &&
237
+ initializationContent.trim().length === 0
238
+ ) {
239
+ throw new ActionContractError(
240
+ "initializeContent must contain non-whitespace content.",
241
+ {
242
+ errorCode: "DOCUMENT_INITIALIZATION_CONTENT_REQUIRED",
243
+ statusCode: 400,
244
+ },
245
+ );
246
+ }
247
+ const payloadDigest = digest(
248
+ normalizedEdits
249
+ ? {
250
+ documentId: args.documentId,
251
+ baseRevision: args.baseRevision,
252
+ edits: normalizedEdits,
253
+ creativeContext:
254
+ args.creativeContextDigest ?? args.creativeContext ?? null,
255
+ }
256
+ : {
257
+ documentId: args.documentId,
258
+ baseRevision: args.baseRevision,
259
+ operation: "initialize",
260
+ initializeContent: initializationContent,
261
+ creativeContext:
262
+ args.creativeContextDigest ?? args.creativeContext ?? null,
263
+ },
264
+ );
216
265
 
217
266
  const readReplay = async (client: Db) => {
218
267
  const [stored] = await client
@@ -250,6 +299,7 @@ export async function mutateDocumentBody(args: {
250
299
  base,
251
300
  args.baseRevision,
252
301
  normalizedEdits,
302
+ initializationContent,
253
303
  );
254
304
  const creativeContext = args.resolveCreativeContext
255
305
  ? await args.resolveCreativeContext()
@@ -274,6 +324,7 @@ export async function mutateDocumentBody(args: {
274
324
  base,
275
325
  args.baseRevision,
276
326
  normalizedEdits,
327
+ initializationContent,
277
328
  );
278
329
  const { beforeContent, beforeHash, resolved } = validated;
279
330
 
@@ -352,7 +403,7 @@ export async function mutateDocumentBody(args: {
352
403
  }));
353
404
  const result: DocumentEditMutationResult = {
354
405
  applied: changed ? resolved.ranges.length : 0,
355
- total: normalizedEdits.length,
406
+ total: normalizedEdits?.length ?? 1,
356
407
  receipt: {
357
408
  receiptId,
358
409
  outcome: changed ? "applied" : "unchanged",
@@ -96,6 +96,16 @@ const editDocumentSchema = z.object({
96
96
  .describe(
97
97
  "JSON array of {find, replace} objects for a snapshot-stable batch; use instead of find/replace.",
98
98
  ),
99
+ initializeContent: z
100
+ .string()
101
+ .min(1)
102
+ .refine((value) => value.trim().length > 0, {
103
+ message: "initializeContent must contain non-whitespace content.",
104
+ })
105
+ .optional()
106
+ .describe(
107
+ "Exact Markdown containing non-whitespace content, used only to initialize a literally empty document body; mutually exclusive with find, replace, and edits.",
108
+ ),
99
109
  contextPackId: z
100
110
  .string()
101
111
  .optional()
@@ -207,7 +217,7 @@ async function resolveEditCreativeContext(args: {
207
217
 
208
218
  export default defineAction({
209
219
  description:
210
- "Surgically edit an existing document's Markdown with exact search-and-replace operations. Prefer this over update-document when preserving the rest of the document; every find string must match exactly once in the immutable base. First call get-document, then pass its baseRevision and a caller-generated idempotencyKey.",
220
+ "Edit an existing document's Markdown with exact search-and-replace operations, or initialize a literally empty body with initializeContent. Every find string must match exactly once in the immutable base. First call get-document, then pass its baseRevision and a caller-generated idempotencyKey.",
211
221
  deferLoading: false,
212
222
  mcpTool: true,
213
223
  agentInputSchema: externalEditDocumentSchema,
@@ -223,9 +233,24 @@ export default defineAction({
223
233
  const isAgentCaller =
224
234
  ctx?.caller === "tool" || ctx?.caller === "mcp" || ctx?.caller === "a2a";
225
235
 
226
- let edits: TextEdit[];
236
+ let edits: TextEdit[] = [];
237
+ const initializesBody = args.initializeContent !== undefined;
227
238
 
228
- if (Array.isArray(args.edits)) {
239
+ if (
240
+ initializesBody &&
241
+ (args.find !== undefined ||
242
+ args.replace !== undefined ||
243
+ args.edits !== undefined)
244
+ ) {
245
+ throw new ActionContractError(
246
+ "initializeContent is mutually exclusive with find, replace, and edits.",
247
+ { errorCode: "DOCUMENT_EDIT_MODE_CONFLICT", statusCode: 400 },
248
+ );
249
+ }
250
+
251
+ if (initializesBody) {
252
+ edits = [];
253
+ } else if (Array.isArray(args.edits)) {
229
254
  edits = args.edits;
230
255
  } else if (args.edits !== undefined) {
231
256
  throw new Error("--edits must be a JSON array");
@@ -233,7 +258,10 @@ export default defineAction({
233
258
  if (!args.find) throw new Error("--find cannot be empty");
234
259
  edits = [{ find: args.find, replace: args.replace ?? "" }];
235
260
  } else {
236
- throw new Error("Either --find or --edits is required");
261
+ throw new ActionContractError(
262
+ "One of initializeContent, find, or edits is required.",
263
+ { errorCode: "DOCUMENT_EDIT_MODE_REQUIRED", statusCode: 400 },
264
+ );
237
265
  }
238
266
 
239
267
  const access = await assertAccess("document", id, "editor");
@@ -243,13 +271,19 @@ export default defineAction({
243
271
  ctx?.caller === "mcp" ||
244
272
  ctx?.caller === "webmcp" ||
245
273
  ctx?.caller === "a2a";
246
- if (isExternalCaller) {
274
+ if (isExternalCaller || initializesBody) {
247
275
  if (!args.baseRevision || !args.idempotencyKey) {
248
276
  throw new ActionContractError(
249
277
  "External document edits require baseRevision and idempotencyKey from get-document.",
250
278
  { errorCode: "DOCUMENT_EDIT_PROTOCOL_REQUIRED", statusCode: 400 },
251
279
  );
252
280
  }
281
+ if (!ctx) {
282
+ throw new ActionContractError(
283
+ "Revisioned document edits require an authenticated caller context.",
284
+ { errorCode: "CALLER_SCOPE_REQUIRED", statusCode: 401 },
285
+ );
286
+ }
253
287
  const isLinkedLocalSource =
254
288
  existing.sourceMode === "local-files" &&
255
289
  existing.sourceKind !== "folder" &&
@@ -265,11 +299,14 @@ export default defineAction({
265
299
  },
266
300
  );
267
301
  }
302
+ const mutation = initializesBody
303
+ ? { initializeContent: args.initializeContent as string }
304
+ : { edits };
268
305
  const result = await mutateDocumentBody({
269
306
  documentId: id,
270
307
  baseRevision: args.baseRevision,
271
308
  idempotencyKey: args.idempotencyKey,
272
- edits,
309
+ ...mutation,
273
310
  creativeContextDigest: {
274
311
  contextPackId: args.contextPackId ?? null,
275
312
  contextModeOverride: args.contextModeOverride ?? null,
@@ -290,7 +327,10 @@ export default defineAction({
290
327
  edit: {
291
328
  descriptor: {
292
329
  kind: "text",
293
- quote: edits[0]?.replace.slice(0, 80) ?? "",
330
+ quote:
331
+ args.initializeContent?.slice(0, 80) ??
332
+ edits?.[0]?.replace.slice(0, 80) ??
333
+ "",
294
334
  },
295
335
  label: existing.title || undefined,
296
336
  },
@@ -307,7 +347,7 @@ export default defineAction({
307
347
  output_id: id,
308
348
  output_type: "document",
309
349
  edit_count: result.applied,
310
- refine_type: "exact_replace",
350
+ refine_type: initializesBody ? "full_update" : "exact_replace",
311
351
  },
312
352
  ctx,
313
353
  );
@@ -7,6 +7,7 @@ import { z } from "zod";
7
7
 
8
8
  import { getDb, schema } from "../server/db/index.js";
9
9
  import { bodyRevisionForContent } from "../server/lib/document-body-revision.js";
10
+ import { chunks } from "./_batch-utils.js";
10
11
  import {
11
12
  lockContentDatabaseMutation,
12
13
  touchContentDatabase,
@@ -17,6 +18,7 @@ import {
17
18
  deterministicId,
18
19
  digest,
19
20
  migrationPlanSchema,
21
+ snapshotBodyRevisionDigest,
20
22
  snapshotDigest,
21
23
  snapshotMigration,
22
24
  serializeMigrationValue,
@@ -77,6 +79,21 @@ function parseJson(text: string) {
77
79
  throw new Error("Migration receipt is corrupt.");
78
80
  }
79
81
  }
82
+
83
+ function migrationStateMatches(
84
+ snapshot: Awaited<ReturnType<typeof snapshotMigration>>,
85
+ receipt: { postDigest: string; resultJson: string },
86
+ ) {
87
+ const result = parseJson(receipt.resultJson);
88
+ if (!("bodyRevisionDigest" in result))
89
+ return snapshotDigest(snapshot) === receipt.postDigest;
90
+ if (typeof result.bodyRevisionDigest !== "string")
91
+ throw new Error("Migration receipt body revision guard is corrupt.");
92
+ return (
93
+ snapshotBodyRevisionDigest(snapshot) === result.bodyRevisionDigest &&
94
+ snapshotDigest(snapshot) === receipt.postDigest
95
+ );
96
+ }
80
97
  function receiptResult(receipt: any, replayed: boolean) {
81
98
  const {
82
99
  plan: _plan,
@@ -105,22 +122,36 @@ async function lockCurrentDatabaseMemberships(tx: any, databaseId: string) {
105
122
  );
106
123
  }
107
124
 
125
+ export const MAX_MIGRATION_FLUSH_CONCURRENCY = 20;
126
+
108
127
  async function flushMigrationDocuments(rows: Array<{ documentId: string }>) {
109
- const accesses = await Promise.all(
110
- rows.map((row) => assertAccess("document", row.documentId, "editor")),
111
- );
112
- const flushes = await Promise.allSettled(
113
- rows.map((row, index) =>
114
- flushOpenDocumentEditorToSql({
115
- documentId: row.documentId,
116
- ownerEmail: accesses[index]?.resource.ownerEmail,
117
- }),
118
- ),
119
- );
120
- const failed = flushes.find(
121
- (flush): flush is PromiseRejectedResult => flush.status === "rejected",
122
- );
123
- if (failed) throw failed.reason;
128
+ const accesses: Awaited<ReturnType<typeof assertAccess>>[] = [];
129
+ for (const batch of chunks(rows, MAX_MIGRATION_FLUSH_CONCURRENCY)) {
130
+ accesses.push(
131
+ ...(await Promise.all(
132
+ batch.map((row) => assertAccess("document", row.documentId, "editor")),
133
+ )),
134
+ );
135
+ }
136
+ for (
137
+ let offset = 0;
138
+ offset < rows.length;
139
+ offset += MAX_MIGRATION_FLUSH_CONCURRENCY
140
+ ) {
141
+ const batch = rows.slice(offset, offset + MAX_MIGRATION_FLUSH_CONCURRENCY);
142
+ const flushes = await Promise.allSettled(
143
+ batch.map((row, index) =>
144
+ flushOpenDocumentEditorToSql({
145
+ documentId: row.documentId,
146
+ ownerEmail: accesses[offset + index]?.resource.ownerEmail,
147
+ }),
148
+ ),
149
+ );
150
+ const failed = flushes.find(
151
+ (flush): flush is PromiseRejectedResult => flush.status === "rejected",
152
+ );
153
+ if (failed) throw failed.reason;
154
+ }
124
155
  }
125
156
 
126
157
  export async function runMigration(args: MigrationInput) {
@@ -164,9 +195,10 @@ export async function runMigration(args: MigrationInput) {
164
195
  if (existing.state !== "applied" && existing.state !== "verified")
165
196
  throw new Error(`Migration receipt is already ${existing.state}.`);
166
197
  if (
167
- snapshotDigest(
198
+ !migrationStateMatches(
168
199
  await snapshotMigration(tx, args.plan.databaseId),
169
- ) !== existing.postDigest
200
+ existing,
201
+ )
170
202
  )
171
203
  throw new Error(
172
204
  "Applied migration has drifted; replay is refused.",
@@ -212,7 +244,7 @@ export async function runMigration(args: MigrationInput) {
212
244
  if (receipt.state === "rolled_back") {
213
245
  if (
214
246
  result.transitionExpectedPostDigest !== args.expectedPostDigest ||
215
- snapshotDigest(current) !== receipt.postDigest
247
+ !migrationStateMatches(current, receipt)
216
248
  )
217
249
  throw new Error(
218
250
  "Terminal migration result has drifted; replay is refused.",
@@ -223,7 +255,7 @@ export async function runMigration(args: MigrationInput) {
223
255
  throw new Error(`Migration receipt is already ${receipt.state}.`);
224
256
  if (
225
257
  receipt.postDigest !== args.expectedPostDigest ||
226
- snapshotDigest(current) !== receipt.postDigest
258
+ !migrationStateMatches(current, receipt)
227
259
  )
228
260
  throw new Error(
229
261
  "Migration has drifted; guarded operation is refused.",
@@ -304,8 +336,10 @@ export async function runMigration(args: MigrationInput) {
304
336
  "Expected post-migration digest does not match receipt.",
305
337
  );
306
338
  if (
307
- snapshotDigest(await snapshotMigration(tx, args.databaseId)) !==
308
- receipt.postDigest
339
+ !migrationStateMatches(
340
+ await snapshotMigration(tx, args.databaseId),
341
+ receipt,
342
+ )
309
343
  )
310
344
  throw new Error(
311
345
  args.phase === "verify"
@@ -353,7 +387,7 @@ export async function runMigration(args: MigrationInput) {
353
387
  `Migration receipt is already ${existing.state}.`,
354
388
  );
355
389
  const current = await snapshotMigration(tx, args.plan.databaseId);
356
- if (snapshotDigest(current) !== existing.postDigest)
390
+ if (!migrationStateMatches(current, existing))
357
391
  throw new Error(
358
392
  "Applied migration has drifted; replay is refused.",
359
393
  );
@@ -433,6 +467,7 @@ export async function runMigration(args: MigrationInput) {
433
467
  orderedIds,
434
468
  written: args.plan.rows.length,
435
469
  verified: false,
470
+ bodyRevisionDigest: snapshotBodyRevisionDigest(current),
436
471
  plan: args.plan,
437
472
  };
438
473
  const claimed = await tx
@@ -492,8 +527,10 @@ export async function runMigration(args: MigrationInput) {
492
527
  "Expected post-migration digest does not match receipt.",
493
528
  );
494
529
  if (
495
- snapshotDigest(await snapshotMigration(tx, args.databaseId)) !==
496
- receipt.postDigest
530
+ !migrationStateMatches(
531
+ await snapshotMigration(tx, args.databaseId),
532
+ receipt,
533
+ )
497
534
  )
498
535
  throw new Error("Migration has drifted; verification is refused.");
499
536
  return receiptResult(receipt, true);
@@ -509,8 +546,10 @@ export async function runMigration(args: MigrationInput) {
509
546
  "Expected post-migration digest does not match receipt.",
510
547
  );
511
548
  if (
512
- snapshotDigest(await snapshotMigration(tx, args.databaseId)) !==
513
- receipt.postDigest
549
+ !migrationStateMatches(
550
+ await snapshotMigration(tx, args.databaseId),
551
+ receipt,
552
+ )
514
553
  )
515
554
  throw new Error(
516
555
  "Terminal migration result has drifted; replay is refused.",
@@ -525,7 +564,7 @@ export async function runMigration(args: MigrationInput) {
525
564
  "Expected post-migration digest does not match receipt.",
526
565
  );
527
566
  const current = await snapshotMigration(tx, args.databaseId);
528
- if (snapshotDigest(current) !== receipt.postDigest)
567
+ if (!migrationStateMatches(current, receipt))
529
568
  throw new Error("Migration has drifted; verification is refused.");
530
569
  const plan = parseJson(receipt.resultJson).plan;
531
570
  if (!plan)
@@ -656,7 +695,7 @@ export async function runMigration(args: MigrationInput) {
656
695
  );
657
696
  }
658
697
  const current = await snapshotMigration(tx, args.databaseId);
659
- if (snapshotDigest(current) !== receipt.postDigest)
698
+ if (!migrationStateMatches(current, receipt))
660
699
  throw new Error("Migration has drifted; guarded operation is refused.");
661
700
  const rollback = parseJson(receipt.rollbackJson);
662
701
  const now = new Date().toISOString();
@@ -707,6 +746,7 @@ export async function runMigration(args: MigrationInput) {
707
746
  const resultJson = {
708
747
  phase: "rollback",
709
748
  transitionExpectedPostDigest: receipt.postDigest,
749
+ bodyRevisionDigest: snapshotBodyRevisionDigest(restored),
710
750
  counts: {
711
751
  rows: (rollback.versions ?? []).length,
712
752
  properties: ids.length,
@@ -788,6 +828,7 @@ export async function runMigration(args: MigrationInput) {
788
828
  const resultJson = {
789
829
  phase: "finalize",
790
830
  transitionExpectedPostDigest: receipt.postDigest,
831
+ bodyRevisionDigest: snapshotBodyRevisionDigest(finalized),
791
832
  counts: { rows: 0, properties: legacyIds.length },
792
833
  verified: true,
793
834
  };
@@ -32,7 +32,7 @@ export default createAgentChatPlugin({
32
32
  mcp: {
33
33
  externalAgents: { writes: "allowlisted" },
34
34
  instructions:
35
- "Find documents with list-documents or search-documents; read one with get-document (pull-document for the raw markdown you will edit). Author content yourself and persist it with create-document, or edit-document for a targeted change and update-document for a full replace. For Notion or other provider data use provider-api-catalog → provider-api-docs → provider-api-request instead of guessing endpoints.",
35
+ "Find documents with list-documents or search-documents; read with get-document (pull-document for raw Markdown). Author and persist content with create-document. For body changes use revision-guarded edit-document; pass initializeContent only when get-document returns an empty body. Use update-document for metadata and browser rewrites. For provider data use provider-api-catalog → provider-api-docs → provider-api-request.",
36
36
  },
37
37
  anonymousOwner: resolvePublicViewerOwner,
38
38
  extraContext: publicDocumentExtraContext,
@@ -9,8 +9,30 @@ from the command menu (Cmd+K → "What's new") or from Settings.
9
9
 
10
10
  - A message that only mentions a design, or pastes Design's own guidance back into chat, no longer ends with a false "couldn't confirm that a Design artifact was saved" reply.
11
11
 
12
+ ## 2026-09-14
13
+
14
+ ### Added
15
+
16
+ - Component duplicates stay linked to their main across Screens, and detaching keeps their current appearance
17
+
18
+ ### Fixed
19
+
20
+ - Rapid layout and padding edits preserve previously committed styles.
21
+ - Screen creation preserves drawn dimensions and layer identity for immediate visual edits.
22
+ - Selecting an element in a live React app now anchors to the authored component file and owner instead of the JSX runtime, so source edits and repeated-instance counts target the real code
23
+ - Undo and redo follow the order of edits across screens.
24
+ - Zoom to selection includes selected layers across Screens and the board, and multiline text stays a single Text layer.
25
+
26
+ ### Changed
27
+
28
+ - Creative Context is now in Labs and off by default
29
+
12
30
  ## 2026-09-13
13
31
 
32
+ ### Improved
33
+
34
+ - Screen layout, style edits, keyboard shortcuts, and edits to AI-generated designs work more reliably through undo and reload
35
+
14
36
  ### Fixed
15
37
 
16
38
  - Connect Code imports explain when a design-system name is already in use
@@ -3,6 +3,17 @@
3
3
  All notable user-facing changes to Chat are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new").
5
5
 
6
+ ## 2026-09-14
7
+
8
+ ### Improved
9
+
10
+ - Factory PR babysit uses a fair queue so re-queued pull requests are not starved, re-prioritizes open PRs when new review work arrives without a commit, and posts a v2 ping comment with per-thread disposition guidance.
11
+ - PR babysit now terminalizes merged and closed pull requests instead of re-listing them with "Seen before".
12
+
13
+ ### Fixed
14
+
15
+ - Factory automations keep display names, Slack channels, and custom prompts through metadata repair, tab switches, and accidental clears.
16
+
6
17
  ## 2026-09-11
7
18
 
8
19
  ### Improved
@@ -3,6 +3,12 @@
3
3
  All notable user-facing changes to Agent-Native Forms are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new").
5
5
 
6
+ ## 2026-09-15
7
+
8
+ ### Fixed
9
+
10
+ - Asking the agent for a second, unrelated form now creates a separate form instead of overwriting the one already open in the chat.
11
+
6
12
  ## 2026-09-12
7
13
 
8
14
  ### Fixed