@agent-native/core 0.84.9 → 0.84.12

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 (80) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +43 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-store.ts +29 -6
  5. package/corpus/core/src/cli/skills.ts +6 -3
  6. package/corpus/core/src/client/agent-chat-adapter.ts +31 -11
  7. package/corpus/core/src/client/require-session.tsx +20 -1
  8. package/corpus/core/src/client/sharing/ShareButton.tsx +9 -3
  9. package/corpus/core/src/collab/storage.ts +40 -5
  10. package/corpus/core/src/server/auth.ts +6 -0
  11. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +7 -6
  12. package/corpus/templates/assets/AGENTS.md +15 -0
  13. package/corpus/templates/assets/actions/_helpers.ts +3 -1
  14. package/corpus/templates/assets/actions/create-generation-preset.ts +12 -1
  15. package/corpus/templates/assets/actions/generate-asset.ts +5 -1
  16. package/corpus/templates/assets/actions/generate-image-batch.ts +6 -1
  17. package/corpus/templates/assets/actions/generate-image.ts +18 -11
  18. package/corpus/templates/assets/actions/open-asset-picker.ts +4 -2
  19. package/corpus/templates/assets/actions/update-generation-preset.ts +16 -3
  20. package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
  21. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  22. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +42 -7
  23. package/corpus/templates/assets/app/routes/library.tsx +4 -2
  24. package/corpus/templates/assets/changelog/2026-07-01-generation-presets-can-now-composite-your-brand-s-canonical-.md +6 -0
  25. package/corpus/templates/assets/changelog/2026-07-01-tagging-a-preset-now-briefs-the-agent-on-that-preset-s-aesth.md +6 -0
  26. package/corpus/templates/assets/server/lib/preset-chat-context.ts +118 -0
  27. package/corpus/templates/assets/server/plugins/agent-chat.ts +5 -0
  28. package/corpus/templates/assets/shared/api.ts +1 -0
  29. package/corpus/templates/design/actions/export-html.ts +4 -2
  30. package/corpus/templates/design/actions/export-pdf.ts +3 -1
  31. package/corpus/templates/design/actions/export-svg.ts +4 -2
  32. package/corpus/templates/design/actions/export-zip.ts +10 -3
  33. package/corpus/templates/design/actions/get-design-snapshot.ts +41 -4
  34. package/corpus/templates/design/actions/present-design-variants.ts +21 -6
  35. package/corpus/templates/design/actions/update-file.ts +68 -17
  36. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +3 -2
  37. package/corpus/templates/design/app/components/design/EditPanel.tsx +35 -3
  38. package/corpus/templates/design/app/components/design/MotionDock.tsx +46 -11
  39. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +80 -30
  40. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +60 -10
  41. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +2 -0
  42. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +4 -0
  43. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  44. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +2 -2
  45. package/corpus/templates/design/app/i18n-data.ts +1 -1
  46. package/corpus/templates/design/app/pages/DesignEditor.tsx +493 -109
  47. package/corpus/templates/design/app/pages/VisualEdit.tsx +4 -16
  48. package/corpus/templates/design/changelog/2026-07-01-improved-variant-pick-follow-ups-so-selected-directions-cont.md +6 -0
  49. package/corpus/templates/design/e2e/helpers.ts +2 -2
  50. package/dist/agent/run-store.d.ts.map +1 -1
  51. package/dist/agent/run-store.js +28 -6
  52. package/dist/agent/run-store.js.map +1 -1
  53. package/dist/cli/skills.d.ts.map +1 -1
  54. package/dist/cli/skills.js +6 -3
  55. package/dist/cli/skills.js.map +1 -1
  56. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  57. package/dist/client/agent-chat-adapter.js +26 -11
  58. package/dist/client/agent-chat-adapter.js.map +1 -1
  59. package/dist/client/require-session.d.ts +14 -0
  60. package/dist/client/require-session.d.ts.map +1 -1
  61. package/dist/client/require-session.js +19 -1
  62. package/dist/client/require-session.js.map +1 -1
  63. package/dist/client/sharing/ShareButton.d.ts +4 -2
  64. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  65. package/dist/client/sharing/ShareButton.js +3 -1
  66. package/dist/client/sharing/ShareButton.js.map +1 -1
  67. package/dist/collab/routes.d.ts +1 -1
  68. package/dist/collab/storage.d.ts.map +1 -1
  69. package/dist/collab/storage.js +31 -5
  70. package/dist/collab/storage.js.map +1 -1
  71. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  72. package/dist/notifications/routes.d.ts +2 -2
  73. package/dist/observability/routes.d.ts +6 -6
  74. package/dist/resources/handlers.d.ts +2 -2
  75. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  76. package/dist/server/auth.d.ts.map +1 -1
  77. package/dist/server/auth.js +7 -0
  78. package/dist/server/auth.js.map +1 -1
  79. package/dist/server/transcribe-voice.d.ts +1 -1
  80. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4908
31
+ - template files: 4912
@@ -1,5 +1,48 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.84.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 9032acb: Don't reap a run that is actively making progress. The stale reapers
8
+ (`reapIfStale`, `reapAllStaleRuns`, and the heartbeat-stale path of
9
+ `cleanupOldRuns`) keyed liveness solely on `heartbeat_at` (the 1.5s
10
+ process-liveness timer) and ignored `last_progress_at` (bumped whenever the
11
+ agent emits an event, including a long-running tool's periodic activity
12
+ heartbeats — e.g. image generation streaming activity every 8s). When the
13
+ heartbeat write lagged while a multi-minute tool was in flight, the run was
14
+ flipped to `errored`, and the producing isolate's SQL-abort check then
15
+ self-aborted the in-flight tool with "Run aborted" — looping on the
16
+ durable-background self-chaining path and interrupting once inline. The reapers
17
+ now use the most recent of `heartbeat_at` and `last_progress_at` (falling back to
18
+ `started_at`) as their liveness basis, so a demonstrably-progressing run is never
19
+ reaped mid-tool. Portable across SQLite and Postgres; can only make reaping more
20
+ conservative (a dead producer emits neither signal).
21
+ - 9032acb: Add a `trigger="label-icon"` option to `ShareButton` that renders a leading
22
+ share glyph alongside the "Share" label, so the trigger matches adjacent
23
+ icon+label buttons (e.g. an Upload button). The default `"label"` trigger stays
24
+ text-only and `"icon"` stays icon-only.
25
+ - 351ee93: Fix an infinite sign-in redirect loop under base-path deploys. When the
26
+ authenticated app shell (wrapped in `RequireSession`) was served at the sign-in
27
+ path — e.g. `/<app>/_agent-native/sign-in` on a path-prefixed deploy — the gate
28
+ redirected to the sign-in page from the sign-in page, nesting and re-encoding the
29
+ current URL as a fresh `?return=` on every hop (`…sign-in?return=%252F…sign-in%253Freturn…`).
30
+ `RequireSession` now refuses to redirect when already on the sign-in entry point
31
+ (new exported `isOnSignInPage` helper), and `safeReturnPath` collapses any
32
+ `return` that resolves back to `…/_agent-native/sign-in` to `/`.
33
+
34
+ ## 0.84.11
35
+
36
+ ### Patch Changes
37
+
38
+ - a2ce30e: Improve local SQLite collab write robustness for Design editor workflows.
39
+
40
+ ## 0.84.10
41
+
42
+ ### Patch Changes
43
+
44
+ - 720b8b0: Make agent chat recovery surface completed-tool timeouts more reliably and keep Design variant follow-ups bounded to the selected screen.
45
+
3
46
  ## 0.84.9
4
47
 
5
48
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.9",
3
+ "version": "0.84.12",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -428,6 +428,29 @@ function backgroundAwareStaleCutoffSql(): string {
428
428
  return `(CAST(? AS BIGINT) - CASE WHEN dispatch_mode LIKE 'background%' THEN ${BACKGROUND_RUN_STALE_MS} ELSE ${RUN_STALE_MS} END)`;
429
429
  }
430
430
 
431
+ /**
432
+ * Liveness basis for the stale reapers: the MOST RECENT of `heartbeat_at`
433
+ * ("process is up", bumped on a 1.5s timer) and `last_progress_at` ("real work
434
+ * is happening", bumped whenever the agent emits an event — including a
435
+ * long-running tool's periodic activity heartbeats, e.g. image generation every
436
+ * 8s), falling back to `started_at`.
437
+ *
438
+ * The reapers previously keyed liveness on `heartbeat_at` alone, so a run that
439
+ * was demonstrably progressing got reaped ('running' → 'errored') the moment the
440
+ * process-liveness write lagged (DB latency, a brief event-loop stall). The
441
+ * producing isolate's SQL-abort check then self-aborted the in-flight action
442
+ * with "Run aborted"; on the durable-background self-chaining path this re-drove
443
+ * the turn in a loop. Honoring progress means a run doing real work is never
444
+ * reaped mid-tool. It can only make reaping MORE conservative — a genuinely dead
445
+ * producer emits neither signal — so a truly-dead run is still reaped.
446
+ *
447
+ * Portable across SQLite and Postgres (CASE + COALESCE only; no GREATEST or
448
+ * scalar MAX, which differ between engines).
449
+ */
450
+ function livenessBasisSql(): string {
451
+ return `(CASE WHEN COALESCE(last_progress_at, started_at) > COALESCE(heartbeat_at, started_at) THEN COALESCE(last_progress_at, started_at) ELSE COALESCE(heartbeat_at, started_at) END)`;
452
+ }
453
+
431
454
  /**
432
455
  * Atomically claim a background-dispatched run for processing. The foreground
433
456
  * POST inserts the run row with `dispatch_mode = 'background'`; the FIRST
@@ -733,8 +756,8 @@ export async function reapIfStale(
733
756
  // override forces a flat window for callers that want one.
734
757
  const staleClause =
735
758
  typeof maxStaleMs === "number"
736
- ? `COALESCE(heartbeat_at, started_at) < ?`
737
- : `COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`;
759
+ ? `${livenessBasisSql()} < ?`
760
+ : `${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`;
738
761
  const staleArgs =
739
762
  typeof maxStaleMs === "number" ? [completedAt - maxStaleMs] : [completedAt];
740
763
  const { rowsAffected } = await client.execute({
@@ -1151,7 +1174,7 @@ export async function reapAllStaleRuns(): Promise<number> {
1151
1174
  const stale = await client.execute({
1152
1175
  sql: `SELECT id FROM agent_runs
1153
1176
  WHERE status = 'running'
1154
- AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
1177
+ AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1155
1178
  args: [now],
1156
1179
  });
1157
1180
  const completedAt = Date.now();
@@ -1162,7 +1185,7 @@ export async function reapAllStaleRuns(): Promise<number> {
1162
1185
  error_code = ?,
1163
1186
  error_detail = ?
1164
1187
  WHERE status = 'running'
1165
- AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
1188
+ AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1166
1189
  args: [
1167
1190
  completedAt,
1168
1191
  STALE_RUN_ERROR_EVENT.errorCode,
@@ -1207,7 +1230,7 @@ export async function cleanupOldRuns(
1207
1230
  sql: `SELECT id FROM agent_runs
1208
1231
  WHERE status = 'running'
1209
1232
  AND (
1210
- COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}
1233
+ ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}
1211
1234
  OR started_at < ?
1212
1235
  )`,
1213
1236
  args: [now, cutoff],
@@ -1236,7 +1259,7 @@ export async function cleanupOldRuns(
1236
1259
  error_code = ?,
1237
1260
  error_detail = ?
1238
1261
  WHERE status = 'running'
1239
- AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
1262
+ AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
1240
1263
  args: [
1241
1264
  completedAt,
1242
1265
  STALE_RUN_ERROR_EVENT.errorCode,
@@ -328,7 +328,8 @@ iteration, or a human-in-the-loop choice among design directions.
328
328
  full app per variant. Design saves every option as a normal screen on the
329
329
  overview board and renders an inline chat choice with one button per screen
330
330
  name. After the user picks, delete the unchosen variant screens and continue
331
- from the kept screen.
331
+ from the kept screen by first calling \`get-design-snapshot\` with that
332
+ screen's \`fileId\`, then making a bounded single-file pass.
332
333
  - If the chat choice buttons are not available in the host, ask the user to
333
334
  tell you the screen name they prefer. The variants are already real screens
334
335
  on the board, so do not ask them to paste HTML or copy a generated handoff
@@ -351,8 +352,10 @@ iteration, or a human-in-the-loop choice among design directions.
351
352
  and realistic controls over decorative mockups.
352
353
  5. After \`present-design-variants\`, wait for the user's pick before
353
354
  generating the next version. Keep the chosen screen, delete the other
354
- variant screens, then refine that direction with \`generate-design\` or
355
- \`edit-design\`.
355
+ variant screens, call \`get-design-snapshot\` with \`fileId\` for the kept
356
+ screen, then continue from that single file in a bounded pass. Prefer
357
+ \`edit-design\`; if \`generate-design\` is needed, update only the kept file
358
+ compactly and stop after the first successful save.
356
359
 
357
360
  ## Design Quality Bar
358
361
 
@@ -703,7 +703,26 @@ function hasContinuationProgress(content: ContentPart[]): boolean {
703
703
  );
704
704
  }
705
705
 
706
- function lastCompletedSideEffectTool(
706
+ const COMPLETED_TOOL_TIMEOUT_NAME_RE =
707
+ /^(add|apply|archive|capture|create|delete|deploy|duplicate|edit|generate|grant|insert|migrate|move|present|publish|remove|rename|reorder|revoke|save|send|set|sync|trash|update|write)(-|$)/;
708
+ const COMPLETED_TOOL_TIMEOUT_NAME_ALLOWLIST = new Set([
709
+ "connect-assets-mcp",
710
+ "import-design-tokens",
711
+ ]);
712
+
713
+ function isCompletedToolTimeoutCandidate(
714
+ part: Extract<ContentPart, { type: "tool-call" }>,
715
+ ): boolean {
716
+ if (part.completedSideEffect === false) return false;
717
+ if (part.completedSideEffect === true) return true;
718
+ const toolName = part.toolName.toLowerCase();
719
+ return (
720
+ COMPLETED_TOOL_TIMEOUT_NAME_ALLOWLIST.has(toolName) ||
721
+ COMPLETED_TOOL_TIMEOUT_NAME_RE.test(toolName)
722
+ );
723
+ }
724
+
725
+ function lastCompletedTimeoutCandidateTool(
707
726
  content: ContentPart[],
708
727
  ): Extract<ContentPart, { type: "tool-call" }> | undefined {
709
728
  for (let i = content.length - 1; i >= 0; i--) {
@@ -713,7 +732,7 @@ function lastCompletedSideEffectTool(
713
732
  part.activity !== true &&
714
733
  part.result !== undefined &&
715
734
  part.isError !== true &&
716
- part.completedSideEffect === true
735
+ isCompletedToolTimeoutCandidate(part)
717
736
  ) {
718
737
  return part;
719
738
  }
@@ -1866,7 +1885,7 @@ export function createAgentChatAdapter(
1866
1885
  // before tool_start; treating it as progress caused silent retry
1867
1886
  // loops when the LLM timed out while assembling a large tool input.
1868
1887
  const hasInFlightTool = hasInFlightToolCall(visibleContent);
1869
- const completedSideEffectTool = lastCompletedSideEffectTool(content);
1888
+ const completedTool = lastCompletedTimeoutCandidateTool(content);
1870
1889
  // Either real output or an actively-running tool counts as progress
1871
1890
  // for the stalled/empty caps.
1872
1891
  const madeProgress = madeContentProgress || hasInFlightTool;
@@ -1963,22 +1982,23 @@ export function createAgentChatAdapter(
1963
1982
  emptyTransientContinuationAttempts = 0;
1964
1983
  } else {
1965
1984
  totalTransientContinuationAttempts += 1;
1966
- // If a mutating action already completed, do not turn a missing
1967
- // closing sentence into a scary connection failure. Give the model
1968
- // one continuation opportunity (the completed tool itself counts
1969
- // as progress on the first timeout); if the follow-up produces no
1970
- // new content, stop locally with a clear "saved, final note timed
1971
- // out" message.
1985
+ // If a tool already completed, do not turn a missing closing
1986
+ // sentence into a scary connection failure. Give the model one
1987
+ // continuation opportunity (the completed tool itself counts as
1988
+ // progress on the first timeout); if the follow-up produces no new
1989
+ // content, stop locally with a clear completed-tool warning.
1972
1990
  if (
1973
- completedSideEffectTool &&
1991
+ signal.reason === "run_timeout" &&
1992
+ completedTool &&
1974
1993
  !hasInFlightToolCall(content) &&
1994
+ !currentPreparingToolName &&
1975
1995
  !madeContentProgress &&
1976
1996
  !hasInFlightTool
1977
1997
  ) {
1978
1998
  return {
1979
1999
  ok: false,
1980
2000
  resetVisibleContent: false,
1981
- completedToolName: completedSideEffectTool.toolName,
2001
+ completedToolName: completedTool.toolName,
1982
2002
  };
1983
2003
  }
1984
2004
  // Bail when the same write tool is stuck in-flight across too many
@@ -70,6 +70,24 @@ export function buildSignInReturnHref(): string {
70
70
  return `${base}?return=${encodeURIComponent(ret)}`;
71
71
  }
72
72
 
73
+ /**
74
+ * True when the browser is already sitting on the framework sign-in entry
75
+ * point. Redirecting to sign-in from here would append the current sign-in
76
+ * URL as a fresh `?return=`, re-encode it, and navigate again — an infinite
77
+ * loop of ever-growing `…sign-in?return=%252F…sign-in%253Freturn%253D…` URLs.
78
+ *
79
+ * This happens when the authenticated app shell (wrapped in `RequireSession`)
80
+ * is served at the sign-in path instead of the framework login HTML — e.g.
81
+ * under a base-path deploy where the SPA shell answers `/<app>/_agent-native/
82
+ * sign-in`. The sign-in page is the framework's job, not the gate's: never
83
+ * redirect to ourselves. Compared against `agentNativePath(...)` so it matches
84
+ * whether or not the app is mounted under a base path.
85
+ */
86
+ export function isOnSignInPage(): boolean {
87
+ if (typeof window === "undefined") return false;
88
+ return window.location.pathname === agentNativePath("/_agent-native/sign-in");
89
+ }
90
+
73
91
  export function RequireSession({
74
92
  children,
75
93
  fallback,
@@ -83,7 +101,8 @@ export function RequireSession({
83
101
  // flight is harmless but noisy.
84
102
  const redirectedRef = useRef(false);
85
103
 
86
- const mustRedirect = !bypass && !isLoading && !session && redirect;
104
+ const mustRedirect =
105
+ !bypass && !isLoading && !session && redirect && !isOnSignInPage();
87
106
 
88
107
  useEffect(() => {
89
108
  if (!mustRedirect) return;
@@ -46,8 +46,10 @@ export interface ShareButtonProps {
46
46
  /** @deprecated No longer affects rendering — trigger always says
47
47
  * "Share". Kept for callsite compatibility. */
48
48
  variant?: "compact" | "label";
49
- /** Optional trigger style. Defaults to the Google-Docs-style "Share" label. */
50
- trigger?: "label" | "icon";
49
+ /** Optional trigger style. Defaults to the Google-Docs-style "Share" label.
50
+ * Use "label-icon" to render a leading share glyph alongside the label so
51
+ * the trigger matches adjacent icon+label buttons; "icon" is icon-only. */
52
+ trigger?: "label" | "icon" | "label-icon";
51
53
  /** @deprecated Label triggers no longer render a visibility/share glyph. */
52
54
  hideTriggerIcon?: boolean;
53
55
  /** Optional className applied to the trigger button. */
@@ -355,7 +357,9 @@ export function ShareButton(props: ShareButtonProps) {
355
357
  };
356
358
 
357
359
  // The default trigger stays text-only; the icon trigger keeps the share glyph.
360
+ // "label-icon" renders the glyph alongside the label for icon+label parity.
358
361
  const iconOnly = props.trigger === "icon";
362
+ const showLabelIcon = props.trigger === "label-icon";
359
363
 
360
364
  return (
361
365
  <Popover open={open} onOpenChange={handleOpenChange}>
@@ -368,7 +372,9 @@ export function ShareButton(props: ShareButtonProps) {
368
372
  )}
369
373
  aria-label={iconOnly ? "Share" : undefined}
370
374
  >
371
- {iconOnly ? <IconShare3 size={16} strokeWidth={1.75} /> : null}
375
+ {iconOnly || showLabelIcon ? (
376
+ <IconShare3 size={16} strokeWidth={1.75} />
377
+ ) : null}
372
378
  {!iconOnly && <span>Share</span>}
373
379
  </button>
374
380
  </PopoverTrigger>
@@ -10,6 +10,41 @@ import { ensureTableExists, ensureColumnExists } from "../db/ddl-guard.js";
10
10
 
11
11
  let _initPromise: Promise<void> | undefined;
12
12
 
13
+ type DbExecClient = ReturnType<typeof getDbExec>;
14
+ type DbExecuteArg = Parameters<DbExecClient["execute"]>[0];
15
+ type DbExecuteResult = Awaited<ReturnType<DbExecClient["execute"]>>;
16
+
17
+ function isSqliteBusyError(error: unknown): boolean {
18
+ if (isPostgres()) return false;
19
+ const candidate = error as { code?: unknown; message?: unknown } | null;
20
+ return (
21
+ candidate?.code === "SQLITE_BUSY" ||
22
+ candidate?.code === "SQLITE_BUSY_RECOVERY" ||
23
+ (typeof candidate?.message === "string" &&
24
+ /database is locked|SQLITE_BUSY/i.test(candidate.message))
25
+ );
26
+ }
27
+
28
+ function sleep(ms: number): Promise<void> {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+
32
+ async function executeWithSqliteRetry(
33
+ client: DbExecClient,
34
+ statement: DbExecuteArg,
35
+ ): Promise<DbExecuteResult> {
36
+ const delays = [25, 75, 150, 300, 600];
37
+ for (let attempt = 0; ; attempt += 1) {
38
+ try {
39
+ return await client.execute(statement);
40
+ } catch (error) {
41
+ const delay = delays[attempt];
42
+ if (delay === undefined || !isSqliteBusyError(error)) throw error;
43
+ await sleep(delay);
44
+ }
45
+ }
46
+ }
47
+
13
48
  async function ensureTable(): Promise<void> {
14
49
  if (!_initPromise) {
15
50
  _initPromise = (async () => {
@@ -97,7 +132,7 @@ export async function trySaveYDocState(
97
132
  const b64 = uint8ArrayToBase64(state);
98
133
  const nowExpr = isPostgres() ? "NOW()::text" : "datetime('now')";
99
134
  if (expectedVersion === null) {
100
- const result = await client.execute({
135
+ const result = await executeWithSqliteRetry(client, {
101
136
  sql: isPostgres()
102
137
  ? `INSERT INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr}) ON CONFLICT (doc_id) DO NOTHING`
103
138
  : `INSERT OR IGNORE INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr})`,
@@ -106,7 +141,7 @@ export async function trySaveYDocState(
106
141
  return result.rowsAffected > 0;
107
142
  }
108
143
 
109
- const result = await client.execute({
144
+ const result = await executeWithSqliteRetry(client, {
110
145
  sql: `UPDATE _collab_docs SET yjs_state = ?, text_snapshot = ?, version = version + 1, updated_at = ${nowExpr} WHERE doc_id = ? AND version = ?`,
111
146
  args: [b64, textSnapshot, docId, expectedVersion],
112
147
  });
@@ -123,13 +158,13 @@ export async function saveYDocState(
123
158
  const client = getDbExec();
124
159
  const b64 = uint8ArrayToBase64(state);
125
160
  const nowExpr = isPostgres() ? "NOW()::text" : "datetime('now')";
126
- const updated = await client.execute({
161
+ const updated = await executeWithSqliteRetry(client, {
127
162
  sql: `UPDATE _collab_docs SET yjs_state = ?, text_snapshot = ?, version = version + 1, updated_at = ${nowExpr} WHERE doc_id = ?`,
128
163
  args: [b64, textSnapshot, docId],
129
164
  });
130
165
  if (updated.rowsAffected > 0) return;
131
166
 
132
- const inserted = await client.execute({
167
+ const inserted = await executeWithSqliteRetry(client, {
133
168
  sql: isPostgres()
134
169
  ? `INSERT INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr}) ON CONFLICT (doc_id) DO NOTHING`
135
170
  : `INSERT OR IGNORE INTO _collab_docs (doc_id, yjs_state, text_snapshot, version, updated_at) VALUES (?, ?, ?, 0, ${nowExpr})`,
@@ -137,7 +172,7 @@ export async function saveYDocState(
137
172
  });
138
173
  if (inserted.rowsAffected > 0) return;
139
174
 
140
- await client.execute({
175
+ await executeWithSqliteRetry(client, {
141
176
  sql: `UPDATE _collab_docs SET yjs_state = ?, text_snapshot = ?, version = version + 1, updated_at = ${nowExpr} WHERE doc_id = ?`,
142
177
  args: [b64, textSnapshot, docId],
143
178
  });
@@ -542,6 +542,12 @@ export function safeReturnPath(raw: string | null | undefined): string {
542
542
  try {
543
543
  const parsed = new URL(raw, "http://safe-base.invalid");
544
544
  if (parsed.origin !== "http://safe-base.invalid") return "/";
545
+ // Never return to the sign-in entry point itself. A `return` that resolves
546
+ // back to `…/_agent-native/sign-in` re-enters the redirect loop — each hop
547
+ // nests the prior sign-in URL as a fresh `?return=`. Collapse it to "/" so
548
+ // the post-sign-in 302 lands on the app, not another sign-in page. Matches
549
+ // with or without an app base-path prefix (`/<app>/_agent-native/sign-in`).
550
+ if (parsed.pathname.endsWith("/_agent-native/sign-in")) return "/";
545
551
  return parsed.pathname + parsed.search + parsed.hash;
546
552
  } catch {
547
553
  return "/";
@@ -10,16 +10,17 @@ LLMs — including Gemini Pro — degrade complex logos. They smear gradients, d
10
10
  ## How it works
11
11
 
12
12
  1. The library has a `canonicalLogoUrl` (set via `set-canonical-logo --libraryId --assetId`). The asset's role is `logo_reference`.
13
- 2. When `generate-image --includeLogo=true` runs, the prompt envelope adds:
13
+ 2. **Logo compositing is a preset option.** A generation preset carries `includeLogo` (stored in the preset `settings` and surfaced as a first-class field). When a generation resolves to a preset with `includeLogo: true`, `generate-image` composites the logo. A generate call's own `includeLogo` arg, when passed, overrides the preset for that run; when omitted, the preset's value wins.
14
+ 3. When logo compositing is on, the prompt envelope adds:
14
15
  > Leave a clean uncluttered area in the upper-right for the real brand logo; do not draw or approximate the logo yourself.
15
- 3. Gemini returns an image with empty space in that corner.
16
- 4. `compositeLogo()` from `server/lib/image-processing.ts` (Sharp) loads the canonical logo PNG / SVG, resizes it to ~16% of the image width with reasonable inset, and composites it onto the generated image.
17
- 5. Output: the image with the actual logo, pixel-perfect, vector-quality if the source is SVG.
16
+ 4. Gemini returns an image with empty space in that corner.
17
+ 5. `compositeLogo()` from `server/lib/image-processing.ts` (Sharp) loads the canonical logo PNG / SVG, resizes it to ~16% of the image width with reasonable inset, and composites it onto the generated image.
18
+ 6. Output: the image with the actual logo, pixel-perfect, vector-quality if the source is SVG.
18
19
 
19
20
  ## When to use it
20
21
 
21
- - The user explicitly toggled "Use logo" in the Generate popover.
22
- - The agent infers the user wants the logo (e.g. "make a hero with our brand logo").
22
+ - The user turned on "Composite canonical logo" when creating a generation preset (the preset then stamps the logo on every image made with it).
23
+ - The agent infers the user wants the logo for a one-off (e.g. "make a hero with our brand logo") — pass `includeLogo: true` on that single generate call to override the preset.
23
24
  - The image will appear in a customer-facing context where logo accuracy matters.
24
25
 
25
26
  ## When NOT to use it
@@ -47,10 +47,25 @@ prompt, aspectRatio }`.
47
47
  composer is empty. The image model is the only remaining composer-side
48
48
  default; the image-model picker writes `imageGenerationModel`, which image
49
49
  generation actions may use when `model` is omitted.
50
+ - When a `preset` is tagged, the server embeds that preset's aesthetics and
51
+ creative philosophy (brand style brief, prompt template, text/logo policy,
52
+ output format) into your message inside a `<tagged-generation-presets>` block.
53
+ Study and internalize that brief before you generate — let it drive
54
+ composition, mood, lighting, and subject — then pass the `presetId` to
55
+ `generate-image` / `generate-image-batch` so the saved format/model/tier/logo
56
+ apply automatically. Do not restate those as ad-hoc args.
50
57
  - For exact visible copy inside a generated image, pass `embeddedText` and
51
58
  optional `textPlacement` to `generate-image` or each `generate-image-batch`
52
59
  slot. Keep the general `prompt` for creative direction; the structured text
53
60
  fields are what allow the pipeline to render copy instead of suppressing it.
61
+ - Logo compositing is a preset option: a generation preset carries
62
+ `includeLogo`, and `generate-image` / `generate-image-batch` stamp the
63
+ library's pixel-perfect canonical logo onto the result when the resolved
64
+ preset has it on (no-op if the library has no canonical logo). Set it via
65
+ `create-generation-preset` / `update-generation-preset` (`includeLogo`). The
66
+ model never draws the logo itself. Pass `includeLogo` on a generate call only
67
+ to override the preset for that one run; otherwise omit it. See the
68
+ `logo-composite` skill.
54
69
  - `asset-variants` is the shared live generation tray state. New image
55
70
  candidates should appear there through `generate-image` or
56
71
  `generate-image-batch`; do not invent page-local progress surfaces.
@@ -155,6 +155,7 @@ export function serializeGenerationRun(row: any) {
155
155
  }
156
156
 
157
157
  export function serializeGenerationPreset(row: any): GenerationPresetSummary {
158
+ const settings = parseJson<Record<string, unknown>>(row.settings, {});
158
159
  return {
159
160
  id: row.id,
160
161
  libraryId: row.libraryId,
@@ -169,7 +170,8 @@ export function serializeGenerationPreset(row: any): GenerationPresetSummary {
169
170
  model: row.model,
170
171
  textPolicy: row.textPolicy ?? "",
171
172
  referencePolicy: row.referencePolicy ?? "auto",
172
- settings: parseJson<Record<string, unknown>>(row.settings, {}),
173
+ includeLogo: settings.includeLogo === true,
174
+ settings,
173
175
  sortOrder: Number(row.sortOrder ?? 0),
174
176
  createdAt: row.createdAt,
175
177
  updatedAt: row.updatedAt,
@@ -32,6 +32,12 @@ export default defineAction({
32
32
  referencePolicy: z
33
33
  .enum(GENERATION_PRESET_REFERENCE_POLICIES)
34
34
  .default("auto"),
35
+ includeLogo: z.coerce
36
+ .boolean()
37
+ .optional()
38
+ .describe(
39
+ "When true, images generated with this preset composite the library's canonical logo (no-op if the library has no canonical logo).",
40
+ ),
35
41
  settings: z.record(z.string(), z.unknown()).optional(),
36
42
  sortOrder: z.coerce.number().optional(),
37
43
  }),
@@ -62,7 +68,12 @@ export default defineAction({
62
68
  model: args.model,
63
69
  textPolicy: args.textPolicy,
64
70
  referencePolicy: args.referencePolicy,
65
- settings: stringifyJson(args.settings ?? {}),
71
+ settings: stringifyJson({
72
+ ...(args.settings ?? {}),
73
+ ...(args.includeLogo !== undefined
74
+ ? { includeLogo: args.includeLogo }
75
+ : {}),
76
+ }),
66
77
  sortOrder: args.sortOrder ?? 100,
67
78
  createdAt: now,
68
79
  updatedAt: now,
@@ -48,7 +48,11 @@ const schema = z.object({
48
48
  .describe("Number of image candidates to generate in the picker."),
49
49
  tier: z.enum(IMAGE_QUALITY_TIERS).optional(),
50
50
  styleStrength: z.enum(STYLE_STRENGTHS).default("balanced"),
51
- includeLogo: booleanParam.default(false),
51
+ includeLogo: booleanParam
52
+ .optional()
53
+ .describe(
54
+ "Override logo compositing for this run. When omitted, the selected preset's logo setting decides whether the library's canonical logo is composited.",
55
+ ),
52
56
  callerAppId: z
53
57
  .string()
54
58
  .optional()
@@ -86,7 +86,12 @@ export default defineAction({
86
86
  tier: z.enum(IMAGE_QUALITY_TIERS).optional(),
87
87
  intent: z.enum(GENERATION_INTENTS).default("generate"),
88
88
  styleStrength: z.enum(STYLE_STRENGTHS).default("balanced"),
89
- includeLogo: z.coerce.boolean().default(false),
89
+ includeLogo: z.coerce
90
+ .boolean()
91
+ .optional()
92
+ .describe(
93
+ "Composite the library's canonical logo onto every slot. When omitted, the selected preset's logo setting is used; pass an explicit value to override it. No-op if the library has no canonical logo.",
94
+ ),
90
95
  groundingMode: z.enum(["auto", "off", "google-search"]).default("auto"),
91
96
  source: z.enum(["chat", "ui", "a2a"]).default("chat"),
92
97
  callerAppId: z
@@ -103,7 +103,12 @@ export default defineAction({
103
103
  .describe(
104
104
  "Exact reference assets to use. When omitted, the server deterministically chooses a small relevant subset from the latest library references.",
105
105
  ),
106
- includeLogo: z.coerce.boolean().default(false),
106
+ includeLogo: z.coerce
107
+ .boolean()
108
+ .optional()
109
+ .describe(
110
+ "Composite the library's pixel-perfect canonical logo onto the finished image. When omitted, the selected preset's logo setting is used; pass an explicit value to override it. No-op if the library has no canonical logo.",
111
+ ),
107
112
  slotId: z.string().optional(),
108
113
  variantBatchId: z.string().optional(),
109
114
  variantScopeId: z
@@ -259,11 +264,13 @@ export default defineAction({
259
264
  preset?.imageSize ??
260
265
  collection?.defaultImageSize ??
261
266
  "2K") as (typeof IMAGE_SIZES)[number];
262
- const presetSettings = parseJson<{ tier?: ImageQualityTier }>(
263
- preset?.settings,
264
- {},
265
- );
267
+ const presetSettings = parseJson<{
268
+ tier?: ImageQualityTier;
269
+ includeLogo?: boolean;
270
+ }>(preset?.settings, {});
266
271
  const resolvedTier = args.tier ?? presetSettings.tier;
272
+ const resolvedIncludeLogo =
273
+ args.includeLogo ?? presetSettings.includeLogo ?? false;
267
274
  const category = (args.categories?.[0] ??
268
275
  preset?.category ??
269
276
  collection?.category) as ImageCategory | undefined;
@@ -320,7 +327,7 @@ export default defineAction({
320
327
  embeddedText: args.embeddedText,
321
328
  textPlacement: args.textPlacement,
322
329
  referenceCount: references.length,
323
- includeLogo: args.includeLogo,
330
+ includeLogo: resolvedIncludeLogo,
324
331
  aspectRatio: resolvedAspectRatio,
325
332
  imageSize: resolvedImageSize,
326
333
  category,
@@ -363,7 +370,7 @@ export default defineAction({
363
370
  aspectRatio: resolvedAspectRatio,
364
371
  imageSize: resolvedImageSize,
365
372
  groundingMode: args.groundingMode,
366
- includeLogo: args.includeLogo,
373
+ includeLogo: resolvedIncludeLogo,
367
374
  categories: resolvedCategories ?? [],
368
375
  collectionId: resolvedCollectionId ?? null,
369
376
  presetId: preset?.id ?? null,
@@ -386,7 +393,7 @@ export default defineAction({
386
393
  intent: args.intent,
387
394
  styleStrength: args.styleStrength,
388
395
  tier: resolvedTier,
389
- includeLogo: args.includeLogo,
396
+ includeLogo: resolvedIncludeLogo,
390
397
  categories: resolvedCategories ?? [],
391
398
  presetId: preset?.id,
392
399
  sessionId: session?.id,
@@ -463,7 +470,7 @@ export default defineAction({
463
470
  await deleteAppState("image-generation-setup").catch(() => {});
464
471
  let image = generated.image;
465
472
  let mimeType = generated.mimeType;
466
- if (args.includeLogo && library.canonicalLogoAssetId) {
473
+ if (resolvedIncludeLogo && library.canonicalLogoAssetId) {
467
474
  const [logo] = await db
468
475
  .select()
469
476
  .from(schema.assets)
@@ -539,7 +546,7 @@ export default defineAction({
539
546
  intent: args.intent,
540
547
  styleStrength: args.styleStrength,
541
548
  tier: resolvedTier,
542
- includeLogo: args.includeLogo,
549
+ includeLogo: resolvedIncludeLogo,
543
550
  presetId: preset?.id,
544
551
  sessionId: session?.id,
545
552
  generated: true,
@@ -584,7 +591,7 @@ export default defineAction({
584
591
  intent: args.intent,
585
592
  styleStrength: args.styleStrength,
586
593
  tier: resolvedTier,
587
- includeLogo: args.includeLogo,
594
+ includeLogo: resolvedIncludeLogo,
588
595
  categories: resolvedCategories ?? [],
589
596
  referenceSelection,
590
597
  settingsUsed,
@@ -72,8 +72,10 @@ const schema = z.object({
72
72
  .default("balanced")
73
73
  .describe("How strongly to follow the library style during generation."),
74
74
  includeLogo: booleanParam
75
- .default(false)
76
- .describe("Whether auto-generation should include the library logo."),
75
+ .optional()
76
+ .describe(
77
+ "Override logo compositing for auto-generation. When omitted, the selected preset's logo setting decides whether the library's canonical logo is composited.",
78
+ ),
77
79
  callerAppId: z
78
80
  .string()
79
81
  .optional()