@agent-native/core 0.90.3 → 0.90.5

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 (256) hide show
  1. package/bin/agent-native.js +25 -3
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +17 -0
  4. package/corpus/core/bin/agent-native.js +25 -3
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/durable-background.ts +18 -18
  7. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
  8. package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
  9. package/corpus/core/src/agent/engine/index.ts +1 -0
  10. package/corpus/core/src/agent/engine/registry.ts +63 -2
  11. package/corpus/core/src/agent/production-agent.ts +22 -11
  12. package/corpus/core/src/agent/run-manager.ts +17 -6
  13. package/corpus/core/src/agent/run-store.ts +16 -11
  14. package/corpus/core/src/cli/create.ts +35 -0
  15. package/corpus/core/src/cli/design-connect.ts +263 -1
  16. package/corpus/core/src/cli/index.ts +84 -7
  17. package/corpus/core/src/cli/workspacify.ts +34 -0
  18. package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
  19. package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
  20. package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
  21. package/corpus/core/src/client/history/VersionHistoryPanel.tsx +77 -47
  22. package/corpus/core/src/client/history/index.ts +3 -0
  23. package/corpus/core/src/client/history/use-history.ts +28 -0
  24. package/corpus/core/src/client/index.ts +8 -0
  25. package/corpus/core/src/client/review/index.ts +5 -0
  26. package/corpus/core/src/client/review/use-review.ts +40 -2
  27. package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
  28. package/corpus/core/src/history/actions/create-resource-version.ts +11 -8
  29. package/corpus/core/src/history/index.ts +0 -1
  30. package/corpus/core/src/history/registry.ts +24 -12
  31. package/corpus/core/src/history/store.ts +84 -53
  32. package/corpus/core/src/review/actions/consume-review-feedback.ts +15 -5
  33. package/corpus/core/src/review/actions/delete-review-comment.ts +15 -3
  34. package/corpus/core/src/review/actions/resolve-review-thread.ts +12 -5
  35. package/corpus/core/src/review/index.ts +0 -5
  36. package/corpus/core/src/review/registry.ts +24 -12
  37. package/corpus/core/src/review/store.ts +10 -7
  38. package/corpus/core/src/server/agent-chat-plugin.ts +6 -0
  39. package/corpus/core/src/server/credential-provider.ts +13 -3
  40. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +11 -11
  41. package/corpus/templates/assets/actions/generate-image.ts +64 -28
  42. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +1 -1
  43. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +91 -74
  44. package/corpus/templates/assets/server/lib/generation.ts +29 -4
  45. package/corpus/templates/assets/server/lib/image-processing.ts +131 -0
  46. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +24 -2
  47. package/corpus/templates/calendar/AGENTS.md +3 -0
  48. package/corpus/templates/calendar/actions/create-event.ts +8 -34
  49. package/corpus/templates/calendar/actions/event-action-helpers.ts +52 -0
  50. package/corpus/templates/calendar/actions/manage-event-draft.ts +7 -34
  51. package/corpus/templates/calendar/actions/update-event.ts +10 -23
  52. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +47 -0
  53. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +23 -0
  54. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +59 -2
  55. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +40 -1
  56. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +34 -1
  57. package/corpus/templates/calendar/app/hooks/use-events.ts +6 -0
  58. package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
  59. package/corpus/templates/calendar/app/i18n-data.ts +40 -0
  60. package/corpus/templates/calendar/changelog/2026-07-07-you-can-mark-event-guests-as-optional-while-inviting-them-or.md +6 -0
  61. package/corpus/templates/calendar/server/handlers/events.ts +1 -0
  62. package/corpus/templates/calendar/server/lib/google-calendar.ts +4 -0
  63. package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
  64. package/corpus/templates/calendar/shared/api.ts +2 -0
  65. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +29 -3
  66. package/corpus/templates/clips/actions/generate-workflow.ts +19 -8
  67. package/corpus/templates/clips/actions/get-clips-ai-prefs.ts +36 -0
  68. package/corpus/templates/clips/actions/lib/clips-ai-prefs.ts +28 -0
  69. package/corpus/templates/clips/actions/regenerate-chapters.ts +21 -7
  70. package/corpus/templates/clips/actions/regenerate-summary.ts +26 -8
  71. package/corpus/templates/clips/actions/regenerate-title.ts +44 -7
  72. package/corpus/templates/clips/actions/update-clips-ai-prefs.ts +50 -0
  73. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +7 -2
  74. package/corpus/templates/clips/app/hooks/use-auto-title.ts +25 -10
  75. package/corpus/templates/clips/app/i18n/ar-SA.ts +28 -6
  76. package/corpus/templates/clips/app/i18n/de-DE.ts +28 -6
  77. package/corpus/templates/clips/app/i18n/en-US.ts +29 -6
  78. package/corpus/templates/clips/app/i18n/es-ES.ts +29 -6
  79. package/corpus/templates/clips/app/i18n/fr-FR.ts +30 -6
  80. package/corpus/templates/clips/app/i18n/hi-IN.ts +26 -6
  81. package/corpus/templates/clips/app/i18n/ja-JP.ts +27 -6
  82. package/corpus/templates/clips/app/i18n/ko-KR.ts +28 -6
  83. package/corpus/templates/clips/app/i18n/pt-BR.ts +28 -6
  84. package/corpus/templates/clips/app/i18n/zh-CN.ts +26 -6
  85. package/corpus/templates/clips/app/i18n/zh-TW.ts +25 -6
  86. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +73 -0
  87. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -0
  88. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -2
  89. package/corpus/templates/clips/changelog/2026-07-07-ai-tools-can-include-the-full-video-not-just-the-transcript-.md +6 -0
  90. package/corpus/templates/clips/changelog/2026-07-07-clips-desktop-now-surfaces-meetings-and-dictate-directly-in-.md +6 -0
  91. package/corpus/templates/clips/changelog/2026-07-07-meeting-notes-now-explain-exactly-how-to-start-granola-style.md +6 -0
  92. package/corpus/templates/clips/changelog/2026-07-07-meeting-start-alerts-now-include-separate-start-notes-and-jo.md +6 -0
  93. package/corpus/templates/clips/changelog/2026-07-07-recordings-start-more-predictably-and-newly-saved-clips-refr.md +6 -0
  94. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  95. package/corpus/templates/clips/desktop/src/app.tsx +435 -105
  96. package/corpus/templates/clips/desktop/src/lib/recorder.ts +27 -21
  97. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +23 -22
  98. package/corpus/templates/clips/desktop/src/styles.css +167 -2
  99. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  100. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -7
  101. package/corpus/templates/clips/desktop/src-tauri/src/tray_meetings.rs +5 -4
  102. package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.put.ts +16 -8
  103. package/corpus/templates/clips/shared/clips-ai-prefs.ts +81 -0
  104. package/corpus/templates/content/AGENTS.md +1 -0
  105. package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
  106. package/corpus/templates/content/actions/_database-utils.ts +2 -1
  107. package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
  108. package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
  109. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
  110. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
  111. package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
  112. package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
  113. package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
  114. package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
  115. package/corpus/templates/content/app/i18n-data.ts +31 -0
  116. package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
  117. package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
  118. package/corpus/templates/content/parity/matrix.md +24 -24
  119. package/corpus/templates/content/parity/matrix.ts +2 -0
  120. package/corpus/templates/content/shared/api.ts +59 -4
  121. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +16 -1
  122. package/corpus/templates/design/actions/add-localhost-screens.ts +1 -0
  123. package/corpus/templates/design/actions/connect-localhost.ts +26 -3
  124. package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
  125. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +362 -12
  126. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +89 -14
  127. package/corpus/templates/design/app/components/design/bridge-security.ts +7 -1
  128. package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -1
  129. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
  130. package/corpus/templates/design/app/i18n-data.ts +41 -0
  131. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
  132. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
  133. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-uses-live-app-iframes-for-click-to-edit-pr.md +6 -0
  134. package/dist/agent/durable-background.d.ts +12 -12
  135. package/dist/agent/durable-background.js +18 -18
  136. package/dist/agent/durable-background.js.map +1 -1
  137. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  138. package/dist/agent/engine/ai-sdk-engine.js +8 -2
  139. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  140. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  141. package/dist/agent/engine/anthropic-engine.js +11 -1
  142. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  143. package/dist/agent/engine/index.d.ts +1 -1
  144. package/dist/agent/engine/index.d.ts.map +1 -1
  145. package/dist/agent/engine/index.js +1 -1
  146. package/dist/agent/engine/index.js.map +1 -1
  147. package/dist/agent/engine/registry.d.ts +14 -0
  148. package/dist/agent/engine/registry.d.ts.map +1 -1
  149. package/dist/agent/engine/registry.js +56 -2
  150. package/dist/agent/engine/registry.js.map +1 -1
  151. package/dist/agent/production-agent.d.ts +2 -2
  152. package/dist/agent/production-agent.d.ts.map +1 -1
  153. package/dist/agent/production-agent.js +18 -11
  154. package/dist/agent/production-agent.js.map +1 -1
  155. package/dist/agent/run-manager.d.ts +7 -1
  156. package/dist/agent/run-manager.d.ts.map +1 -1
  157. package/dist/agent/run-manager.js +6 -1
  158. package/dist/agent/run-manager.js.map +1 -1
  159. package/dist/agent/run-store.d.ts +1 -1
  160. package/dist/agent/run-store.d.ts.map +1 -1
  161. package/dist/agent/run-store.js +15 -10
  162. package/dist/agent/run-store.js.map +1 -1
  163. package/dist/cli/create.d.ts.map +1 -1
  164. package/dist/cli/create.js +31 -0
  165. package/dist/cli/create.js.map +1 -1
  166. package/dist/cli/design-connect.d.ts.map +1 -1
  167. package/dist/cli/design-connect.js +197 -1
  168. package/dist/cli/design-connect.js.map +1 -1
  169. package/dist/cli/index.js +76 -9
  170. package/dist/cli/index.js.map +1 -1
  171. package/dist/cli/workspacify.d.ts.map +1 -1
  172. package/dist/cli/workspacify.js +30 -0
  173. package/dist/cli/workspacify.js.map +1 -1
  174. package/dist/client/KeepTabOpenNotice.d.ts +9 -10
  175. package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
  176. package/dist/client/KeepTabOpenNotice.js +8 -4
  177. package/dist/client/KeepTabOpenNotice.js.map +1 -1
  178. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  179. package/dist/client/RunStuckBanner.js +14 -13
  180. package/dist/client/RunStuckBanner.js.map +1 -1
  181. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  182. package/dist/client/agent-chat-adapter.js +25 -17
  183. package/dist/client/agent-chat-adapter.js.map +1 -1
  184. package/dist/client/history/VersionHistoryPanel.d.ts +1 -1
  185. package/dist/client/history/VersionHistoryPanel.d.ts.map +1 -1
  186. package/dist/client/history/VersionHistoryPanel.js +12 -5
  187. package/dist/client/history/VersionHistoryPanel.js.map +1 -1
  188. package/dist/client/history/index.d.ts +1 -1
  189. package/dist/client/history/index.d.ts.map +1 -1
  190. package/dist/client/history/index.js +1 -1
  191. package/dist/client/history/index.js.map +1 -1
  192. package/dist/client/history/use-history.d.ts +12 -0
  193. package/dist/client/history/use-history.d.ts.map +1 -1
  194. package/dist/client/history/use-history.js +6 -0
  195. package/dist/client/history/use-history.js.map +1 -1
  196. package/dist/client/index.d.ts +2 -2
  197. package/dist/client/index.d.ts.map +1 -1
  198. package/dist/client/index.js +2 -2
  199. package/dist/client/index.js.map +1 -1
  200. package/dist/client/review/index.d.ts +1 -1
  201. package/dist/client/review/index.d.ts.map +1 -1
  202. package/dist/client/review/index.js +1 -1
  203. package/dist/client/review/index.js.map +1 -1
  204. package/dist/client/review/use-review.d.ts +23 -0
  205. package/dist/client/review/use-review.d.ts.map +1 -1
  206. package/dist/client/review/use-review.js +8 -0
  207. package/dist/client/review/use-review.js.map +1 -1
  208. package/dist/client/use-run-stuck-detection.d.ts +1 -1
  209. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  210. package/dist/client/use-run-stuck-detection.js +6 -4
  211. package/dist/client/use-run-stuck-detection.js.map +1 -1
  212. package/dist/collab/struct-routes.d.ts +1 -1
  213. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  214. package/dist/history/actions/create-resource-version.js +7 -4
  215. package/dist/history/actions/create-resource-version.js.map +1 -1
  216. package/dist/history/index.d.ts +1 -1
  217. package/dist/history/index.d.ts.map +1 -1
  218. package/dist/history/index.js +1 -1
  219. package/dist/history/index.js.map +1 -1
  220. package/dist/history/registry.d.ts.map +1 -1
  221. package/dist/history/registry.js +14 -12
  222. package/dist/history/registry.js.map +1 -1
  223. package/dist/history/store.d.ts.map +1 -1
  224. package/dist/history/store.js +74 -49
  225. package/dist/history/store.js.map +1 -1
  226. package/dist/notifications/routes.d.ts +1 -1
  227. package/dist/resources/handlers.d.ts +3 -3
  228. package/dist/review/actions/consume-review-feedback.d.ts +1 -0
  229. package/dist/review/actions/consume-review-feedback.js +8 -2
  230. package/dist/review/actions/consume-review-feedback.js.map +1 -1
  231. package/dist/review/actions/delete-review-comment.d.ts +2 -1
  232. package/dist/review/actions/delete-review-comment.js +11 -3
  233. package/dist/review/actions/delete-review-comment.js.map +1 -1
  234. package/dist/review/actions/resolve-review-thread.d.ts +2 -1
  235. package/dist/review/actions/resolve-review-thread.js +5 -2
  236. package/dist/review/actions/resolve-review-thread.js.map +1 -1
  237. package/dist/review/index.d.ts +1 -1
  238. package/dist/review/index.d.ts.map +1 -1
  239. package/dist/review/index.js +1 -1
  240. package/dist/review/index.js.map +1 -1
  241. package/dist/review/registry.d.ts.map +1 -1
  242. package/dist/review/registry.js +14 -12
  243. package/dist/review/registry.js.map +1 -1
  244. package/dist/review/store.d.ts +3 -3
  245. package/dist/review/store.d.ts.map +1 -1
  246. package/dist/review/store.js +7 -4
  247. package/dist/review/store.js.map +1 -1
  248. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  249. package/dist/server/agent-chat-plugin.js +5 -0
  250. package/dist/server/agent-chat-plugin.js.map +1 -1
  251. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  252. package/dist/server/credential-provider.d.ts +2 -0
  253. package/dist/server/credential-provider.d.ts.map +1 -1
  254. package/dist/server/credential-provider.js +13 -3
  255. package/dist/server/credential-provider.js.map +1 -1
  256. package/package.json +1 -1
@@ -1,4 +1,9 @@
1
- import { getDbExec, intType, isPostgres } from "../db/client.js";
1
+ import {
2
+ getDbExec,
3
+ intType,
4
+ isPostgres,
5
+ isUniqueViolation,
6
+ } from "../db/client.js";
2
7
  import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
3
8
  import type { Visibility } from "../sharing/schema.js";
4
9
  import type {
@@ -9,6 +14,8 @@ import type {
9
14
 
10
15
  let historyTableInitPromise: Promise<void> | undefined;
11
16
 
17
+ const VERSION_INSERT_MAX_ATTEMPTS = 8;
18
+
12
19
  export interface InsertResourceVersionInput {
13
20
  resourceType: string;
14
21
  resourceId: string;
@@ -53,7 +60,7 @@ export async function ensureResourceVersionsTable(): Promise<void> {
53
60
  metadata_json TEXT
54
61
  )`;
55
62
  const indexes = [
56
- `CREATE INDEX IF NOT EXISTS idx_agent_resource_versions_resource
63
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_resource_versions_resource_number
57
64
  ON agent_resource_versions (resource_type, resource_id, version_number)`,
58
65
  `CREATE INDEX IF NOT EXISTS idx_agent_resource_versions_owner
59
66
  ON agent_resource_versions (owner_email, created_at)`,
@@ -64,7 +71,7 @@ export async function ensureResourceVersionsTable(): Promise<void> {
64
71
  if (isPostgres()) {
65
72
  await ensureTableExists("agent_resource_versions", createSql);
66
73
  await ensureIndexExists(
67
- "idx_agent_resource_versions_resource",
74
+ "idx_agent_resource_versions_resource_number",
68
75
  indexes[0],
69
76
  );
70
77
  await ensureIndexExists(
@@ -89,41 +96,55 @@ export async function insertResourceVersion(
89
96
  ): Promise<ResourceVersion> {
90
97
  await ensureResourceVersionsTable();
91
98
  const client = getDbExec();
92
- const maxRows = await client.execute({
93
- sql: `SELECT MAX(version_number) as max_version
99
+ const visibility =
100
+ input.visibility === "org" || input.visibility === "public"
101
+ ? input.visibility
102
+ : "private";
103
+ const actorKind = input.actorKind ?? "human";
104
+ const createdBy = input.createdBy ?? null;
105
+ const ownerEmail = input.ownerEmail ?? input.createdBy ?? null;
106
+ const orgId = input.orgId ?? null;
107
+ const title = input.title ?? null;
108
+ const summary = input.summary ?? null;
109
+ const metadata = input.metadata ?? null;
110
+ const snapshotJson = JSON.stringify(input.snapshot);
111
+ const metadataJson = stringifyOptionalJson(metadata);
112
+
113
+ let lastError: unknown;
114
+ for (let attempt = 0; attempt < VERSION_INSERT_MAX_ATTEMPTS; attempt++) {
115
+ const maxRows = await client.execute({
116
+ sql: `SELECT MAX(version_number) as max_version
94
117
  FROM agent_resource_versions
95
118
  WHERE resource_type = ? AND resource_id = ?`,
96
- args: [input.resourceType, input.resourceId],
97
- });
98
- const versionNumber =
99
- Number(
100
- (maxRows.rows?.[0] as Record<string, unknown> | undefined)?.max_version ??
101
- 0,
102
- ) + 1;
103
- const id = createVersionId();
104
- const createdAt = new Date().toISOString();
105
- const version: ResourceVersion = {
106
- id,
107
- resourceType: input.resourceType,
108
- resourceId: input.resourceId,
109
- versionNumber,
110
- createdAt,
111
- createdBy: input.createdBy ?? null,
112
- actorKind: input.actorKind ?? "human",
113
- ownerEmail: input.ownerEmail ?? input.createdBy ?? null,
114
- orgId: input.orgId ?? null,
115
- visibility:
116
- input.visibility === "org" || input.visibility === "public"
117
- ? input.visibility
118
- : "private",
119
- title: input.title ?? null,
120
- summary: input.summary ?? null,
121
- snapshot: input.snapshot,
122
- metadata: input.metadata ?? null,
123
- };
119
+ args: [input.resourceType, input.resourceId],
120
+ });
121
+ const versionNumber =
122
+ Number(
123
+ (maxRows.rows?.[0] as Record<string, unknown> | undefined)
124
+ ?.max_version ?? 0,
125
+ ) + 1;
126
+ const id = createVersionId();
127
+ const createdAt = new Date().toISOString();
128
+ const version: ResourceVersion = {
129
+ id,
130
+ resourceType: input.resourceType,
131
+ resourceId: input.resourceId,
132
+ versionNumber,
133
+ createdAt,
134
+ createdBy,
135
+ actorKind,
136
+ ownerEmail,
137
+ orgId,
138
+ visibility,
139
+ title,
140
+ summary,
141
+ snapshot: input.snapshot,
142
+ metadata,
143
+ };
124
144
 
125
- await client.execute({
126
- sql: `INSERT INTO agent_resource_versions (
145
+ try {
146
+ await client.execute({
147
+ sql: `INSERT INTO agent_resource_versions (
127
148
  id,
128
149
  resource_type,
129
150
  resource_id,
@@ -139,25 +160,35 @@ export async function insertResourceVersion(
139
160
  snapshot_json,
140
161
  metadata_json
141
162
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
142
- args: [
143
- version.id,
144
- version.resourceType,
145
- version.resourceId,
146
- version.versionNumber,
147
- version.createdAt,
148
- version.createdBy,
149
- version.actorKind,
150
- version.ownerEmail,
151
- version.orgId,
152
- version.visibility,
153
- version.title,
154
- version.summary,
155
- JSON.stringify(version.snapshot),
156
- stringifyOptionalJson(version.metadata),
157
- ],
158
- });
163
+ args: [
164
+ version.id,
165
+ version.resourceType,
166
+ version.resourceId,
167
+ version.versionNumber,
168
+ version.createdAt,
169
+ version.createdBy,
170
+ version.actorKind,
171
+ version.ownerEmail,
172
+ version.orgId,
173
+ version.visibility,
174
+ version.title,
175
+ version.summary,
176
+ snapshotJson,
177
+ metadataJson,
178
+ ],
179
+ });
180
+ return version;
181
+ } catch (error) {
182
+ lastError = error;
183
+ if (!isUniqueViolation(error)) {
184
+ throw error;
185
+ }
186
+ }
187
+ }
159
188
 
160
- return version;
189
+ throw lastError instanceof Error
190
+ ? lastError
191
+ : new Error("Failed to allocate a unique resource version number");
161
192
  }
162
193
 
163
194
  export async function queryResourceVersions(
@@ -23,11 +23,21 @@ export default defineAction({
23
23
  actionCtx,
24
24
  "editor",
25
25
  );
26
- await consumeReviewFeedback(args.commentIds, undefined, {
27
- resourceType: args.resourceType,
28
- resourceId: args.resourceId,
29
- });
30
- return { consumedCommentIds: args.commentIds };
26
+ const updatedCount = await consumeReviewFeedback(
27
+ args.commentIds,
28
+ undefined,
29
+ {
30
+ resourceType: args.resourceType,
31
+ resourceId: args.resourceId,
32
+ },
33
+ );
34
+ if (updatedCount < 1) {
35
+ throw new Error("No matching review comments to consume");
36
+ }
37
+ return {
38
+ consumedCommentIds: args.commentIds,
39
+ updatedCount,
40
+ };
31
41
  },
32
42
  audit: {
33
43
  target: (args) => ({
@@ -39,12 +39,19 @@ export default defineAction({
39
39
  }
40
40
  const isAuthor =
41
41
  Boolean(actionCtx?.userEmail) &&
42
- comment.authorEmail === actionCtx?.userEmail;
42
+ normalizeEmail(comment.authorEmail) ===
43
+ normalizeEmail(actionCtx?.userEmail);
43
44
  if (!isAuthor && (!access || !roleSatisfies(access.role, "editor"))) {
44
45
  throw new ForbiddenError("Not allowed to delete this review comment");
45
46
  }
46
- await deleteReviewComment(comment.id, actionCtx?.userEmail ?? null);
47
- return { commentId: comment.id, deleted: true };
47
+ const updatedCount = await deleteReviewComment(
48
+ comment.id,
49
+ actionCtx?.userEmail ?? null,
50
+ );
51
+ if (updatedCount < 1) {
52
+ throw new Error("Review comment not found");
53
+ }
54
+ return { commentId: comment.id, deleted: true as const, updatedCount };
48
55
  },
49
56
  audit: {
50
57
  target: (args) => ({
@@ -53,3 +60,8 @@ export default defineAction({
53
60
  }),
54
61
  },
55
62
  });
63
+
64
+ function normalizeEmail(email: string | null | undefined): string | null {
65
+ const normalized = email?.trim().toLowerCase();
66
+ return normalized || null;
67
+ }
@@ -45,11 +45,18 @@ export default defineAction({
45
45
  if (!threadId) {
46
46
  throw new Error("Provide threadId or commentId");
47
47
  }
48
- await resolveReviewThread(threadId, actionCtx?.userEmail ?? null, {
49
- resourceType: args.resourceType,
50
- resourceId: args.resourceId,
51
- });
52
- return { threadId, resolved: true };
48
+ const updatedCount = await resolveReviewThread(
49
+ threadId,
50
+ actionCtx?.userEmail ?? null,
51
+ {
52
+ resourceType: args.resourceType,
53
+ resourceId: args.resourceId,
54
+ },
55
+ );
56
+ if (updatedCount < 1) {
57
+ throw new Error("Review thread not found");
58
+ }
59
+ return { threadId, resolved: true as const, updatedCount };
53
60
  },
54
61
  audit: {
55
62
  target: (args) => ({
@@ -24,13 +24,8 @@ export {
24
24
  } from "./registry.js";
25
25
  export {
26
26
  __resetReviewInitForTests,
27
- consumeReviewFeedback,
28
- deleteReviewComment,
29
27
  ensureReviewTables,
30
28
  getReviewCommentById,
31
29
  getReviewStatus,
32
- insertReviewComment,
33
30
  queryReviewComments,
34
- resolveReviewThread,
35
- upsertReviewStatus,
36
31
  } from "./store.js";
@@ -1,4 +1,8 @@
1
- import { ForbiddenError, resolveAccess } from "../sharing/access.js";
1
+ import {
2
+ ForbiddenError,
3
+ resolveAccess,
4
+ type AccessContext,
5
+ } from "../sharing/access.js";
2
6
  import { getShareableResource } from "../sharing/registry.js";
3
7
  import { roleSatisfies, type Visibility } from "../sharing/schema.js";
4
8
  import type {
@@ -31,6 +35,16 @@ export function listReviewableResources(): ReviewableResourceRegistration[] {
31
35
  return Array.from(registrations.values());
32
36
  }
33
37
 
38
+ function accessContextFrom(
39
+ ctx?: ReviewResourceContext,
40
+ ): AccessContext | undefined {
41
+ if (!ctx) return undefined;
42
+ return {
43
+ userEmail: ctx.userEmail ?? undefined,
44
+ orgId: ctx.orgId ?? undefined,
45
+ };
46
+ }
47
+
34
48
  export async function resolveReviewableResourceAccess(
35
49
  resourceType: string,
36
50
  resourceId: string,
@@ -42,7 +56,11 @@ export async function resolveReviewableResourceAccess(
42
56
  }
43
57
 
44
58
  if (getShareableResource(resourceType)) {
45
- const access = await resolveAccess(resourceType, resourceId);
59
+ const access = await resolveAccess(
60
+ resourceType,
61
+ resourceId,
62
+ accessContextFrom(ctx),
63
+ );
46
64
  if (!access) {
47
65
  return null;
48
66
  }
@@ -55,16 +73,10 @@ export async function resolveReviewableResourceAccess(
55
73
  };
56
74
  }
57
75
 
58
- const userEmail = ctx?.userEmail ?? null;
59
- if (!userEmail) {
60
- return null;
61
- }
62
- return {
63
- role: "owner",
64
- ownerEmail: userEmail,
65
- orgId: ctx?.orgId ?? null,
66
- visibility: "private",
67
- };
76
+ // Fail closed: unregistered types, and registered types without an access
77
+ // resolver that aren't shareable, never invent ownership. bypassScope on
78
+ // review queries is only safe after a real resource ACL.
79
+ return null;
68
80
  }
69
81
 
70
82
  export async function assertReviewableResourceAccess(
@@ -310,13 +310,13 @@ export async function resolveReviewThread(
310
310
  threadId: string,
311
311
  resolvedBy?: string | null,
312
312
  resource?: { resourceType: string; resourceId: string },
313
- ): Promise<void> {
313
+ ): Promise<number> {
314
314
  await ensureReviewTables();
315
315
  const now = new Date().toISOString();
316
316
  const resourceClause = resource
317
317
  ? "AND resource_type = ? AND resource_id = ?"
318
318
  : "";
319
- await getDbExec().execute({
319
+ const result = await getDbExec().execute({
320
320
  sql: `UPDATE agent_review_comments
321
321
  SET status = 'resolved',
322
322
  resolved_by = ?,
@@ -331,15 +331,16 @@ export async function resolveReviewThread(
331
331
  ...(resource ? [resource.resourceType, resource.resourceId] : []),
332
332
  ],
333
333
  });
334
+ return result.rowsAffected ?? 0;
334
335
  }
335
336
 
336
337
  export async function deleteReviewComment(
337
338
  id: string,
338
339
  deletedBy?: string | null,
339
- ): Promise<void> {
340
+ ): Promise<number> {
340
341
  await ensureReviewTables();
341
342
  const now = new Date().toISOString();
342
- await getDbExec().execute({
343
+ const result = await getDbExec().execute({
343
344
  sql: `UPDATE agent_review_comments
344
345
  SET status = 'deleted',
345
346
  deleted_by = ?,
@@ -348,22 +349,23 @@ export async function deleteReviewComment(
348
349
  WHERE id = ?`,
349
350
  args: [deletedBy ?? null, now, now, id],
350
351
  });
352
+ return result.rowsAffected ?? 0;
351
353
  }
352
354
 
353
355
  export async function consumeReviewFeedback(
354
356
  ids: string[],
355
357
  consumedAt = new Date().toISOString(),
356
358
  resource?: { resourceType: string; resourceId: string },
357
- ): Promise<void> {
359
+ ): Promise<number> {
358
360
  if (!ids.length) {
359
- return;
361
+ return 0;
360
362
  }
361
363
  await ensureReviewTables();
362
364
  const placeholders = ids.map(() => "?").join(", ");
363
365
  const resourceClause = resource
364
366
  ? "AND resource_type = ? AND resource_id = ?"
365
367
  : "";
366
- await getDbExec().execute({
368
+ const result = await getDbExec().execute({
367
369
  sql: `UPDATE agent_review_comments
368
370
  SET consumed_at = ?,
369
371
  updated_at = ?
@@ -375,6 +377,7 @@ export async function consumeReviewFeedback(
375
377
  ...(resource ? [resource.resourceType, resource.resourceId] : []),
376
378
  ],
377
379
  });
380
+ return result.rowsAffected ?? 0;
378
381
  }
379
382
 
380
383
  export async function upsertReviewStatus(
@@ -6623,6 +6623,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6623
6623
  scope: "user",
6624
6624
  scopeId: ownerEmail,
6625
6625
  });
6626
+ const { clearProviderCredentialAuthFailure } =
6627
+ await import("./credential-provider.js");
6628
+ await clearProviderCredentialAuthFailure({
6629
+ key: secretKey,
6630
+ value: trimmedKey,
6631
+ });
6626
6632
  } catch (err) {
6627
6633
  console.error(
6628
6634
  "[agent-chat] save-key persistence failed:",
@@ -721,6 +721,8 @@ export async function clearBuilderCredentialAuthFailure(creds: {
721
721
  }
722
722
 
723
723
  const PROVIDER_AUTH_FAILURE_SETTING_PREFIX = "provider-auth-failure:";
724
+ /** Stale failure markers expire so a transient 401 cannot permanently block deploy keys. */
725
+ export const PROVIDER_AUTH_FAILURE_TTL_MS = 15 * 60 * 1000;
724
726
 
725
727
  export interface ProviderCredentialAuthFailure {
726
728
  fingerprint: string;
@@ -760,10 +762,18 @@ export async function getProviderCredentialAuthFailure(opts: {
760
762
  const fingerprint = providerCredentialFingerprint(key, opts.value);
761
763
  if (!fingerprint) return null;
762
764
  try {
763
- const { getSetting } = await import("../settings/store.js");
764
- const row = await getSetting(providerAuthFailureSettingKey(fingerprint));
765
+ const settings = await import("../settings/store.js");
766
+ const settingKey = providerAuthFailureSettingKey(fingerprint);
767
+ const row = await settings.getSetting(settingKey);
765
768
  if (!row) return null;
766
769
  if (row.fingerprint !== fingerprint) return null;
770
+ const at = typeof row.at === "number" ? row.at : Date.now();
771
+ if (Date.now() - at > PROVIDER_AUTH_FAILURE_TTL_MS) {
772
+ if (typeof settings.deleteSetting === "function") {
773
+ await settings.deleteSetting(settingKey).catch(() => {});
774
+ }
775
+ return null;
776
+ }
767
777
  return {
768
778
  fingerprint,
769
779
  key:
@@ -776,7 +786,7 @@ export async function getProviderCredentialAuthFailure(opts: {
776
786
  : "The model provider rejected the saved API key.",
777
787
  status: typeof row.status === "number" ? row.status : undefined,
778
788
  code: typeof row.code === "string" ? row.code : undefined,
779
- at: typeof row.at === "number" ? row.at : Date.now(),
789
+ at,
780
790
  ownerEmail:
781
791
  typeof row.ownerEmail === "string" ? row.ownerEmail : undefined,
782
792
  orgId: typeof row.orgId === "string" ? row.orgId : undefined,
@@ -25,8 +25,11 @@ export async function assertPresetSkeletonAssetsValid(input: {
25
25
  skeleton.mask?.type === "asset" && typeof skeleton.mask.assetId === "string"
26
26
  ? skeleton.mask.assetId
27
27
  : "";
28
- if (!backgroundAssetId || !maskAssetId) return;
28
+ if (!backgroundAssetId) return;
29
29
 
30
+ const assetIds = maskAssetId
31
+ ? [backgroundAssetId, maskAssetId]
32
+ : [backgroundAssetId];
30
33
  const rows = await input.db
31
34
  .select({
32
35
  id: schema.assets.id,
@@ -35,18 +38,15 @@ export async function assertPresetSkeletonAssetsValid(input: {
35
38
  height: schema.assets.height,
36
39
  })
37
40
  .from(schema.assets)
38
- .where(inArray(schema.assets.id, [backgroundAssetId, maskAssetId]));
41
+ .where(inArray(schema.assets.id, assetIds));
39
42
  const background = rows.find((asset: any) => asset.id === backgroundAssetId);
43
+ if (!background || background.libraryId !== input.libraryId) {
44
+ throw new Error("Skeleton image must belong to this asset library.");
45
+ }
46
+ if (!maskAssetId) return;
40
47
  const mask = rows.find((asset: any) => asset.id === maskAssetId);
41
- if (
42
- !background ||
43
- !mask ||
44
- background.libraryId !== input.libraryId ||
45
- mask.libraryId !== input.libraryId
46
- ) {
47
- throw new Error(
48
- "Skeleton image and mask must belong to this asset library.",
49
- );
48
+ if (!mask || mask.libraryId !== input.libraryId) {
49
+ throw new Error("Skeleton mask must belong to this asset library.");
50
50
  }
51
51
  if (
52
52
  typeof background.width !== "number" ||
@@ -30,9 +30,11 @@ import {
30
30
  compositeLogo,
31
31
  maskFromManualMaskAlpha,
32
32
  maskFromPlateAlpha,
33
+ prepareGptImage2SkeletonInpaintImages,
33
34
  } from "../server/lib/image-processing.js";
34
35
  import { nowIso, parseJson, stringifyJson } from "../server/lib/json.js";
35
36
  import {
37
+ aspectRatioValue,
36
38
  clampCutoutAspectRatio,
37
39
  normalizePresetSkeletonSpec,
38
40
  skeletonUsesCanonicalLogo,
@@ -350,13 +352,21 @@ export default defineAction({
350
352
  isSkeletonCutout &&
351
353
  Boolean(skeletonAssets?.backgroundAsset) &&
352
354
  resolvedModel === "gpt-image-2";
355
+ const skeletonInpaint = useSkeletonInpaint
356
+ ? await inpaintReferencesForSkeleton(skeletonAssets)
357
+ : null;
353
358
  const resolvedAspectRatio =
354
- isSkeletonCutout && !useSkeletonInpaint
355
- ? clampCutoutAspectRatio({
356
- aspectRatio: outputAspectRatio,
357
- supported: supportedAspectRatiosForModel("gpt-image-1"),
358
- })
359
- : outputAspectRatio;
359
+ useSkeletonInpaint && skeletonInpaint
360
+ ? closestSupportedAspectRatioForSize(
361
+ skeletonInpaint.size,
362
+ supportedAspectRatiosForModel("gpt-image-2"),
363
+ )
364
+ : isSkeletonCutout
365
+ ? clampCutoutAspectRatio({
366
+ aspectRatio: outputAspectRatio,
367
+ supported: supportedAspectRatiosForModel("gpt-image-1"),
368
+ })
369
+ : outputAspectRatio;
360
370
  if (
361
371
  isSkeletonCutout &&
362
372
  !useSkeletonInpaint &&
@@ -386,7 +396,7 @@ export default defineAction({
386
396
  : "";
387
397
  let references: ReferenceForGeneration[];
388
398
  if (useSkeletonInpaint) {
389
- references = await inpaintReferencesForSkeleton(skeletonAssets);
399
+ references = skeletonInpaint?.references ?? [];
390
400
  } else {
391
401
  const backgroundPlateReference =
392
402
  backgroundPlateReferenceForSkeleton(skeletonAssets);
@@ -839,7 +849,10 @@ function backgroundPlateReferenceForSkeleton(
839
849
 
840
850
  async function inpaintReferencesForSkeleton(
841
851
  skeletonAssets: LoadedSkeletonAssets | null,
842
- ): Promise<ReferenceForGeneration[]> {
852
+ ): Promise<{
853
+ references: ReferenceForGeneration[];
854
+ size: { width: number; height: number };
855
+ }> {
843
856
  if (
844
857
  !skeletonAssets?.backgroundAsset ||
845
858
  !skeletonAssets.backgroundAssetId ||
@@ -853,26 +866,49 @@ async function inpaintReferencesForSkeleton(
853
866
  plate: skeletonAssets.backgroundAsset,
854
867
  })
855
868
  : await maskFromPlateAlpha(skeletonAssets.backgroundAsset);
856
- return [
857
- {
858
- id: skeletonAssets.backgroundAssetId,
859
- role: "edit_target",
860
- category: "skeleton",
861
- mimeType: skeletonAssets.backgroundMimeType,
862
- data: skeletonAssets.backgroundAsset.toString("base64"),
863
- selectionReason: "explicit",
864
- },
865
- {
866
- id:
867
- skeletonAssets.maskAssetId ??
868
- `${skeletonAssets.backgroundAssetId}:mask`,
869
- role: "mask",
870
- category: "skeleton",
871
- mimeType: "image/png",
872
- data: mask.toString("base64"),
873
- selectionReason: "explicit",
874
- },
875
- ];
869
+ const prepared = await prepareGptImage2SkeletonInpaintImages({
870
+ plate: skeletonAssets.backgroundAsset,
871
+ mask,
872
+ });
873
+ return {
874
+ size: prepared.size,
875
+ references: [
876
+ {
877
+ id: skeletonAssets.backgroundAssetId,
878
+ role: "edit_target",
879
+ category: "skeleton",
880
+ mimeType: prepared.resized
881
+ ? "image/png"
882
+ : skeletonAssets.backgroundMimeType,
883
+ data: prepared.plate.toString("base64"),
884
+ selectionReason: "explicit",
885
+ },
886
+ {
887
+ id:
888
+ skeletonAssets.maskAssetId ??
889
+ `${skeletonAssets.backgroundAssetId}:mask`,
890
+ role: "mask",
891
+ category: "skeleton",
892
+ mimeType: "image/png",
893
+ data: prepared.mask.toString("base64"),
894
+ selectionReason: "explicit",
895
+ },
896
+ ],
897
+ };
898
+ }
899
+
900
+ function closestSupportedAspectRatioForSize(
901
+ size: { width: number; height: number },
902
+ supported: readonly AspectRatio[],
903
+ ): AspectRatio {
904
+ const target = size.width / size.height;
905
+ return (
906
+ [...supported].sort(
907
+ (left, right) =>
908
+ Math.abs(aspectRatioValue(left) - target) -
909
+ Math.abs(aspectRatioValue(right) - target),
910
+ )[0] ?? "1:1"
911
+ );
876
912
  }
877
913
 
878
914
  type LoadedSkeletonAssets = {
@@ -44,7 +44,7 @@ function variantStateKey(threadId: string | null) {
44
44
  // STALE_IMAGE_RUN_MS in refresh-generation-run, so a slow-but-healthy run (e.g.
45
45
  // gpt-image-2) is not flagged "interrupted" and flipped to an error slot before
46
46
  // its finished image arrives.
47
- const STALE_PENDING_RUN_MS = 6 * 60 * 1000;
47
+ const STALE_PENDING_RUN_MS = 10 * 60 * 1000;
48
48
 
49
49
  function slotTime(slot: AssetVariantState["slots"][number]): number {
50
50
  const raw = slot.createdAt ?? slot.updatedAt ?? "";