@agent-native/core 0.128.4 → 0.129.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 (343) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +84 -0
  3. package/corpus/core/docs/content/template-design-collaboration-and-full-apps.mdx +3 -1
  4. package/corpus/core/package.json +6 -3
  5. package/corpus/core/src/a2a/client.ts +18 -4
  6. package/corpus/core/src/a2a/server.ts +19 -3
  7. package/corpus/core/src/a2a/types.ts +7 -0
  8. package/corpus/core/src/action.ts +43 -16
  9. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +14 -10
  10. package/corpus/core/src/agent/production-agent.ts +41 -3
  11. package/corpus/core/src/agent/thread-data-builder.ts +5 -0
  12. package/corpus/core/src/chat-threads/migrations.ts +53 -0
  13. package/corpus/core/src/cli/create.ts +4 -0
  14. package/corpus/core/src/cli/skills-content/design-visual-edit-skill.ts +36 -16
  15. package/corpus/core/src/cli/skills.ts +16 -0
  16. package/corpus/core/src/client/AgentPanel.tsx +9 -5
  17. package/corpus/core/src/client/AssistantChat.tsx +138 -46
  18. package/corpus/core/src/client/MultiTabAssistantChat.tsx +16 -10
  19. package/corpus/core/src/client/agent-chat-adapter.ts +21 -3
  20. package/corpus/core/src/client/analytics.ts +159 -102
  21. package/corpus/core/src/client/chat/message-components.tsx +12 -5
  22. package/corpus/core/src/client/chat/runtime.ts +2 -1
  23. package/corpus/core/src/client/chat/tool-call-display.tsx +25 -2
  24. package/corpus/core/src/client/chat-model-groups.ts +3 -0
  25. package/corpus/core/src/client/client-status-requests.ts +161 -0
  26. package/corpus/core/src/client/settings/VoiceTranscriptionSection.tsx +2 -2
  27. package/corpus/core/src/client/sse-event-processor.ts +15 -3
  28. package/corpus/core/src/client/use-agent-engine-configured.ts +54 -50
  29. package/corpus/core/src/client/use-chat-models.ts +16 -11
  30. package/corpus/core/src/client/use-chat-threads.ts +32 -9
  31. package/corpus/core/src/client/use-session.ts +4 -5
  32. package/corpus/core/src/db/client.ts +83 -8
  33. package/corpus/core/src/db/ensure-additive-columns.ts +31 -20
  34. package/corpus/core/src/db/migrations.ts +22 -2
  35. package/corpus/core/src/mcp/approval-store.ts +99 -0
  36. package/corpus/core/src/mcp/build-server.ts +579 -303
  37. package/corpus/core/src/mcp/embed-app.ts +1 -1
  38. package/corpus/core/src/mcp/oauth-client-metadata.ts +380 -0
  39. package/corpus/core/src/mcp/oauth-route.ts +57 -52
  40. package/corpus/core/src/mcp/screen-memory-stdio.ts +400 -378
  41. package/corpus/core/src/mcp/server.ts +29 -145
  42. package/corpus/core/src/mcp/stdio.ts +72 -80
  43. package/corpus/core/src/mcp-client/manager.ts +6 -10
  44. package/corpus/core/src/mcp-client/oauth-client.ts +127 -36
  45. package/corpus/core/src/mcp-client/oauth-routes.ts +16 -6
  46. package/corpus/core/src/mcp-client/routes.ts +6 -5
  47. package/corpus/core/src/server/agent-capabilities.ts +20 -0
  48. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +27 -0
  49. package/corpus/core/src/server/agent-chat-plugin.ts +18 -0
  50. package/corpus/core/src/server/transcribe-voice.ts +14 -5
  51. package/corpus/core/src/templates/default/.agents/skills/security/SKILL.md +13 -2
  52. package/corpus/core/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
  53. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
  54. package/corpus/core/src/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
  55. package/corpus/templates/analytics/.agents/skills/security/SKILL.md +13 -2
  56. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +1 -10
  57. package/corpus/templates/analytics/app/global.css +0 -31
  58. package/corpus/templates/analytics/app/lib/sql-query.ts +46 -20
  59. package/corpus/templates/analytics/changelog/2026-07-28-analytics-dashboard-loading-states-now-use-a-simpler-standar.md +6 -0
  60. package/corpus/templates/analytics/changelog/2026-07-28-analytics-dashboards-now-load-first-party-charts-reliably-in.md +6 -0
  61. package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +42 -16
  62. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +4 -2
  63. package/corpus/templates/analytics/shared/sql-query-limits.ts +1 -0
  64. package/corpus/templates/assets/.agents/skills/security/SKILL.md +13 -2
  65. package/corpus/templates/brain/.agents/skills/security/SKILL.md +13 -2
  66. package/corpus/templates/calendar/.agents/skills/security/SKILL.md +13 -2
  67. package/corpus/templates/chat/.agents/skills/security/SKILL.md +13 -2
  68. package/corpus/templates/chat/README.md +0 -2
  69. package/corpus/templates/clips/.agents/skills/security/SKILL.md +13 -2
  70. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -6
  71. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -2
  72. package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -1
  73. package/corpus/templates/clips/changelog/2026-07-28-copied-share-links-no-longer-leave-an-unnecessary-confirmati.md +6 -0
  74. package/corpus/templates/clips/changelog/2026-07-28-playback-skip-controls-no-longer-show-overlapping-numbers.md +6 -0
  75. package/corpus/templates/clips/changelog/2026-07-28-public-clips-share-buttons-now-use-a-clean-text-only-label.md +6 -0
  76. package/corpus/templates/clips/desktop/src/app.tsx +14 -17
  77. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +3 -1
  78. package/corpus/templates/clips/desktop/src/styles.css +13 -10
  79. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +2 -2
  80. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +36 -0
  81. package/corpus/templates/content/.agents/skills/security/SKILL.md +13 -2
  82. package/corpus/templates/content/actions/_builder-cms-read-client.ts +155 -34
  83. package/corpus/templates/content/actions/_builder-docs-client.ts +151 -30
  84. package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +5 -2
  85. package/corpus/templates/content/app/root.tsx +35 -14
  86. package/corpus/templates/content/changelog/2026-07-28-builder-backed-files-media-fields-now-accept-direct-file-url.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-28-builder-content-reads-now-negotiate-the-newest-mcp-protocol-.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-28-public-content-pages-load-faster-before-the-optional-ask-sid.md +6 -0
  89. package/corpus/templates/design/.agents/skills/security/SKILL.md +13 -2
  90. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +36 -16
  91. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +42 -3
  92. package/corpus/templates/design/actions/open-visual-edit.ts +2 -1
  93. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  94. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -15
  95. package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +314 -0
  96. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +88 -3
  97. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +1 -1
  98. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +7 -6
  99. package/corpus/templates/design/app/i18n-data.ts +56 -11
  100. package/corpus/templates/design/app/i18n-responsive-interact.ts +183 -0
  101. package/corpus/templates/design/app/lib/agent-chat.ts +60 -0
  102. package/corpus/templates/design/app/pages/design-editor/responsive-interact.ts +89 -0
  103. package/corpus/templates/design/changelog/2026-07-28-design-edits-can-be-applied-through-the-host-coding-agent-fr.md +6 -0
  104. package/corpus/templates/design/changelog/2026-07-28-interact-mode-can-preview-designs-in-responsive-device-sizes.md +6 -0
  105. package/corpus/templates/design/changelog/2026-07-28-interact-mode-now-opens-a-full-screen-responsive-preview-wit.md +6 -0
  106. package/corpus/templates/design/changelog/2026-07-28-show-or-hide-design-editing-chrome-with-cmd-on-apple-devices.md +2 -2
  107. package/corpus/templates/design/changelog/2026-07-28-visual-edit-now-always-shows-the-live-running-app-in-the-can.md +6 -0
  108. package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +13 -2
  109. package/corpus/templates/forms/.agents/skills/security/SKILL.md +13 -2
  110. package/corpus/templates/macros/.agents/skills/security/SKILL.md +13 -2
  111. package/corpus/templates/mail/.agents/skills/security/SKILL.md +13 -2
  112. package/corpus/templates/plan/.agents/skills/security/SKILL.md +13 -2
  113. package/corpus/templates/slides/.agents/skills/security/SKILL.md +13 -2
  114. package/corpus/templates/tasks/.agents/skills/security/SKILL.md +13 -2
  115. package/corpus/toolkit/CHANGELOG.md +13 -0
  116. package/corpus/toolkit/package.json +1 -1
  117. package/corpus/toolkit/src/chat-history.css +1 -2
  118. package/corpus/toolkit/src/composer/TiptapComposer.tsx +45 -0
  119. package/dist/a2a/client.d.ts +7 -0
  120. package/dist/a2a/client.d.ts.map +1 -1
  121. package/dist/a2a/client.js +8 -3
  122. package/dist/a2a/client.js.map +1 -1
  123. package/dist/a2a/server.d.ts.map +1 -1
  124. package/dist/a2a/server.js +18 -3
  125. package/dist/a2a/server.js.map +1 -1
  126. package/dist/a2a/types.d.ts +7 -0
  127. package/dist/a2a/types.d.ts.map +1 -1
  128. package/dist/a2a/types.js.map +1 -1
  129. package/dist/action.d.ts +15 -1
  130. package/dist/action.d.ts.map +1 -1
  131. package/dist/action.js +39 -16
  132. package/dist/action.js.map +1 -1
  133. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  134. package/dist/agent/engine/ai-sdk-engine.js +14 -10
  135. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  136. package/dist/agent/production-agent.d.ts.map +1 -1
  137. package/dist/agent/production-agent.js +35 -7
  138. package/dist/agent/production-agent.js.map +1 -1
  139. package/dist/agent/thread-data-builder.d.ts +2 -0
  140. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  141. package/dist/agent/thread-data-builder.js +3 -0
  142. package/dist/agent/thread-data-builder.js.map +1 -1
  143. package/dist/chat-threads/migrations.d.ts +21 -0
  144. package/dist/chat-threads/migrations.d.ts.map +1 -0
  145. package/dist/chat-threads/migrations.js +45 -0
  146. package/dist/chat-threads/migrations.js.map +1 -0
  147. package/dist/cli/create.d.ts.map +1 -1
  148. package/dist/cli/create.js +4 -0
  149. package/dist/cli/create.js.map +1 -1
  150. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  151. package/dist/cli/skills-content/design-visual-edit-skill.d.ts.map +1 -1
  152. package/dist/cli/skills-content/design-visual-edit-skill.js +36 -16
  153. package/dist/cli/skills-content/design-visual-edit-skill.js.map +1 -1
  154. package/dist/cli/skills.d.ts.map +1 -1
  155. package/dist/cli/skills.js +15 -0
  156. package/dist/cli/skills.js.map +1 -1
  157. package/dist/client/AgentPanel.d.ts.map +1 -1
  158. package/dist/client/AgentPanel.js +6 -5
  159. package/dist/client/AgentPanel.js.map +1 -1
  160. package/dist/client/AssistantChat.d.ts +37 -0
  161. package/dist/client/AssistantChat.d.ts.map +1 -1
  162. package/dist/client/AssistantChat.js +41 -8
  163. package/dist/client/AssistantChat.js.map +1 -1
  164. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  165. package/dist/client/MultiTabAssistantChat.js +11 -10
  166. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  167. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  168. package/dist/client/agent-chat-adapter.js +19 -3
  169. package/dist/client/agent-chat-adapter.js.map +1 -1
  170. package/dist/client/analytics.d.ts.map +1 -1
  171. package/dist/client/analytics.js +150 -101
  172. package/dist/client/analytics.js.map +1 -1
  173. package/dist/client/chat/message-components.d.ts +2 -1
  174. package/dist/client/chat/message-components.d.ts.map +1 -1
  175. package/dist/client/chat/message-components.js +11 -4
  176. package/dist/client/chat/message-components.js.map +1 -1
  177. package/dist/client/chat/runtime.d.ts.map +1 -1
  178. package/dist/client/chat/runtime.js +2 -1
  179. package/dist/client/chat/runtime.js.map +1 -1
  180. package/dist/client/chat/tool-call-display.d.ts +4 -1
  181. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  182. package/dist/client/chat/tool-call-display.js +14 -8
  183. package/dist/client/chat/tool-call-display.js.map +1 -1
  184. package/dist/client/chat-model-groups.d.ts.map +1 -1
  185. package/dist/client/chat-model-groups.js +3 -0
  186. package/dist/client/chat-model-groups.js.map +1 -1
  187. package/dist/client/client-status-requests.d.ts +14 -0
  188. package/dist/client/client-status-requests.d.ts.map +1 -0
  189. package/dist/client/client-status-requests.js +126 -0
  190. package/dist/client/client-status-requests.js.map +1 -0
  191. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  192. package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
  193. package/dist/client/sse-event-processor.d.ts +7 -0
  194. package/dist/client/sse-event-processor.d.ts.map +1 -1
  195. package/dist/client/sse-event-processor.js +8 -3
  196. package/dist/client/sse-event-processor.js.map +1 -1
  197. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  198. package/dist/client/use-agent-engine-configured.js +29 -43
  199. package/dist/client/use-agent-engine-configured.js.map +1 -1
  200. package/dist/client/use-chat-models.d.ts.map +1 -1
  201. package/dist/client/use-chat-models.js +11 -11
  202. package/dist/client/use-chat-models.js.map +1 -1
  203. package/dist/client/use-chat-threads.d.ts.map +1 -1
  204. package/dist/client/use-chat-threads.js +23 -7
  205. package/dist/client/use-chat-threads.js.map +1 -1
  206. package/dist/client/use-session.d.ts.map +1 -1
  207. package/dist/client/use-session.js +4 -4
  208. package/dist/client/use-session.js.map +1 -1
  209. package/dist/collab/awareness.d.ts +2 -2
  210. package/dist/collab/awareness.d.ts.map +1 -1
  211. package/dist/collab/struct-routes.d.ts +1 -1
  212. package/dist/db/client.d.ts.map +1 -1
  213. package/dist/db/client.js +48 -4
  214. package/dist/db/client.js.map +1 -1
  215. package/dist/db/ensure-additive-columns.d.ts.map +1 -1
  216. package/dist/db/ensure-additive-columns.js +32 -21
  217. package/dist/db/ensure-additive-columns.js.map +1 -1
  218. package/dist/db/migrations.d.ts +9 -0
  219. package/dist/db/migrations.d.ts.map +1 -1
  220. package/dist/db/migrations.js +14 -3
  221. package/dist/db/migrations.js.map +1 -1
  222. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  223. package/dist/mcp/approval-store.d.ts +16 -0
  224. package/dist/mcp/approval-store.d.ts.map +1 -0
  225. package/dist/mcp/approval-store.js +85 -0
  226. package/dist/mcp/approval-store.js.map +1 -0
  227. package/dist/mcp/build-server.d.ts +1 -34
  228. package/dist/mcp/build-server.d.ts.map +1 -1
  229. package/dist/mcp/build-server.js +220 -23
  230. package/dist/mcp/build-server.js.map +1 -1
  231. package/dist/mcp/embed-app.js +1 -1
  232. package/dist/mcp/embed-app.js.map +1 -1
  233. package/dist/mcp/oauth-client-metadata.d.ts +15 -0
  234. package/dist/mcp/oauth-client-metadata.d.ts.map +1 -0
  235. package/dist/mcp/oauth-client-metadata.js +297 -0
  236. package/dist/mcp/oauth-client-metadata.js.map +1 -0
  237. package/dist/mcp/oauth-route.d.ts.map +1 -1
  238. package/dist/mcp/oauth-route.js +39 -53
  239. package/dist/mcp/oauth-route.js.map +1 -1
  240. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  241. package/dist/mcp/screen-memory-stdio.js +304 -296
  242. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  243. package/dist/mcp/server.d.ts +12 -25
  244. package/dist/mcp/server.d.ts.map +1 -1
  245. package/dist/mcp/server.js +24 -133
  246. package/dist/mcp/server.js.map +1 -1
  247. package/dist/mcp/stdio.d.ts.map +1 -1
  248. package/dist/mcp/stdio.js +36 -35
  249. package/dist/mcp/stdio.js.map +1 -1
  250. package/dist/mcp-client/manager.d.ts.map +1 -1
  251. package/dist/mcp-client/manager.js +6 -6
  252. package/dist/mcp-client/manager.js.map +1 -1
  253. package/dist/mcp-client/oauth-client.d.ts +13 -17
  254. package/dist/mcp-client/oauth-client.d.ts.map +1 -1
  255. package/dist/mcp-client/oauth-client.js +74 -16
  256. package/dist/mcp-client/oauth-client.js.map +1 -1
  257. package/dist/mcp-client/oauth-routes.d.ts +2 -2
  258. package/dist/mcp-client/oauth-routes.d.ts.map +1 -1
  259. package/dist/mcp-client/oauth-routes.js +16 -6
  260. package/dist/mcp-client/oauth-routes.js.map +1 -1
  261. package/dist/mcp-client/routes.js +5 -5
  262. package/dist/mcp-client/routes.js.map +1 -1
  263. package/dist/notifications/routes.d.ts +1 -1
  264. package/dist/observability/routes.d.ts +3 -3
  265. package/dist/progress/routes.d.ts +1 -1
  266. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  267. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  268. package/dist/resources/handlers.d.ts +1 -1
  269. package/dist/secrets/routes.d.ts +9 -9
  270. package/dist/server/agent-capabilities.d.ts.map +1 -1
  271. package/dist/server/agent-capabilities.js +21 -0
  272. package/dist/server/agent-capabilities.js.map +1 -1
  273. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -0
  274. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  275. package/dist/server/agent-chat/action-filters-a2a.js +16 -0
  276. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  277. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  278. package/dist/server/agent-chat-plugin.js +11 -1
  279. package/dist/server/agent-chat-plugin.js.map +1 -1
  280. package/dist/server/realtime-token.d.ts +1 -1
  281. package/dist/server/transcribe-voice.d.ts +1 -1
  282. package/dist/server/transcribe-voice.js +12 -6
  283. package/dist/server/transcribe-voice.js.map +1 -1
  284. package/dist/templates/chat/.agents/skills/security/SKILL.md +13 -2
  285. package/dist/templates/chat/README.md +0 -2
  286. package/dist/templates/default/.agents/skills/security/SKILL.md +13 -2
  287. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
  288. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
  289. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
  290. package/docs/content/template-design-collaboration-and-full-apps.mdx +3 -1
  291. package/package.json +7 -4
  292. package/src/a2a/client.ts +18 -4
  293. package/src/a2a/server.ts +19 -3
  294. package/src/a2a/types.ts +7 -0
  295. package/src/action.ts +43 -16
  296. package/src/agent/engine/ai-sdk-engine.ts +14 -10
  297. package/src/agent/production-agent.ts +41 -3
  298. package/src/agent/thread-data-builder.ts +5 -0
  299. package/src/chat-threads/migrations.ts +53 -0
  300. package/src/cli/create.ts +4 -0
  301. package/src/cli/skills-content/design-visual-edit-skill.ts +36 -16
  302. package/src/cli/skills.ts +16 -0
  303. package/src/client/AgentPanel.tsx +9 -5
  304. package/src/client/AssistantChat.tsx +138 -46
  305. package/src/client/MultiTabAssistantChat.tsx +16 -10
  306. package/src/client/agent-chat-adapter.ts +21 -3
  307. package/src/client/analytics.ts +159 -102
  308. package/src/client/chat/message-components.tsx +12 -5
  309. package/src/client/chat/runtime.ts +2 -1
  310. package/src/client/chat/tool-call-display.tsx +25 -2
  311. package/src/client/chat-model-groups.ts +3 -0
  312. package/src/client/client-status-requests.ts +161 -0
  313. package/src/client/settings/VoiceTranscriptionSection.tsx +2 -2
  314. package/src/client/sse-event-processor.ts +15 -3
  315. package/src/client/use-agent-engine-configured.ts +54 -50
  316. package/src/client/use-chat-models.ts +16 -11
  317. package/src/client/use-chat-threads.ts +32 -9
  318. package/src/client/use-session.ts +4 -5
  319. package/src/db/client.ts +83 -8
  320. package/src/db/ensure-additive-columns.ts +31 -20
  321. package/src/db/migrations.ts +22 -2
  322. package/src/mcp/approval-store.ts +99 -0
  323. package/src/mcp/build-server.ts +579 -303
  324. package/src/mcp/embed-app.ts +1 -1
  325. package/src/mcp/oauth-client-metadata.ts +380 -0
  326. package/src/mcp/oauth-route.ts +57 -52
  327. package/src/mcp/screen-memory-stdio.ts +400 -378
  328. package/src/mcp/server.ts +29 -145
  329. package/src/mcp/stdio.ts +72 -80
  330. package/src/mcp-client/manager.ts +6 -10
  331. package/src/mcp-client/oauth-client.ts +127 -36
  332. package/src/mcp-client/oauth-routes.ts +16 -6
  333. package/src/mcp-client/routes.ts +6 -5
  334. package/src/server/agent-capabilities.ts +20 -0
  335. package/src/server/agent-chat/action-filters-a2a.ts +27 -0
  336. package/src/server/agent-chat-plugin.ts +18 -0
  337. package/src/server/transcribe-voice.ts +14 -5
  338. package/src/templates/chat/.agents/skills/security/SKILL.md +13 -2
  339. package/src/templates/chat/README.md +0 -2
  340. package/src/templates/default/.agents/skills/security/SKILL.md +13 -2
  341. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
  342. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
  343. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
@@ -0,0 +1,161 @@
1
+ import { agentNativePath } from "./api-path.js";
2
+
3
+ export type ClientStatusResult<T> =
4
+ | { state: "available"; value: T }
5
+ | { state: "unavailable"; status?: number };
6
+
7
+ type CacheEntry = {
8
+ expiresAt: number;
9
+ result: ClientStatusResult<unknown>;
10
+ };
11
+
12
+ const RESULT_TTL_MS = 500;
13
+ const REQUEST_TIMEOUT_MS = 15_000;
14
+ const cache = new Map<string, CacheEntry>();
15
+ const requests = new Map<string, Promise<ClientStatusResult<unknown>>>();
16
+ const requestControllers = new Map<string, AbortController>();
17
+ const requestGenerations = new Map<string, number>();
18
+ let invalidationListenersInstalled = false;
19
+ let generation = 0;
20
+
21
+ function expireClientStatusCache(): void {
22
+ generation += 1;
23
+ cache.clear();
24
+ }
25
+
26
+ function installInvalidationListeners(): void {
27
+ if (
28
+ invalidationListenersInstalled ||
29
+ typeof window === "undefined" ||
30
+ typeof document === "undefined"
31
+ ) {
32
+ return;
33
+ }
34
+ invalidationListenersInstalled = true;
35
+
36
+ if (typeof window.addEventListener === "function") {
37
+ window.addEventListener("focus", expireClientStatusCache);
38
+ window.addEventListener(
39
+ "agent-engine:configured-changed",
40
+ invalidateClientStatusRequests,
41
+ );
42
+ }
43
+ if (typeof document.addEventListener === "function") {
44
+ document.addEventListener("visibilitychange", () => {
45
+ if (document.visibilityState === "visible") {
46
+ expireClientStatusCache();
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ async function fetchClientStatus<T>(
53
+ path: string,
54
+ ): Promise<ClientStatusResult<T>> {
55
+ installInvalidationListeners();
56
+ const url = agentNativePath(path);
57
+ const cached = cache.get(url);
58
+ if (cached && cached.expiresAt > Date.now()) {
59
+ return cached.result as ClientStatusResult<T>;
60
+ }
61
+ cache.delete(url);
62
+
63
+ const pending = requests.get(url);
64
+ if (pending) return pending as Promise<ClientStatusResult<T>>;
65
+
66
+ const requestGeneration = generation;
67
+ const requestUrlGeneration = requestGenerations.get(url) ?? 0;
68
+ const controller =
69
+ typeof AbortController === "undefined" ? null : new AbortController();
70
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
71
+ const timeout = new Promise<ClientStatusResult<unknown>>((resolve) => {
72
+ timeoutId = setTimeout(() => {
73
+ controller?.abort();
74
+ resolve({ state: "unavailable" });
75
+ }, REQUEST_TIMEOUT_MS);
76
+ });
77
+ const transport = fetch(url, {
78
+ cache: "no-store",
79
+ credentials: "same-origin",
80
+ ...(controller ? { signal: controller.signal } : {}),
81
+ })
82
+ .then(async (response): Promise<ClientStatusResult<unknown>> => {
83
+ if (!response.ok) {
84
+ return { state: "unavailable", status: response.status };
85
+ }
86
+ try {
87
+ return { state: "available", value: await response.json() };
88
+ } catch {
89
+ return { state: "unavailable", status: response.status };
90
+ }
91
+ })
92
+ .catch((): ClientStatusResult<unknown> => ({ state: "unavailable" }));
93
+ const request = Promise.race([transport, timeout])
94
+ .then((result) => {
95
+ if (
96
+ generation === requestGeneration &&
97
+ (requestGenerations.get(url) ?? 0) === requestUrlGeneration &&
98
+ result.state === "available"
99
+ ) {
100
+ cache.set(url, {
101
+ expiresAt: Date.now() + RESULT_TTL_MS,
102
+ result,
103
+ });
104
+ }
105
+ return result;
106
+ })
107
+ .finally(() => {
108
+ if (timeoutId !== undefined) clearTimeout(timeoutId);
109
+ if (requests.get(url) === request) requests.delete(url);
110
+ if (requestControllers.get(url) === controller) {
111
+ requestControllers.delete(url);
112
+ }
113
+ });
114
+
115
+ requests.set(url, request);
116
+ if (controller) requestControllers.set(url, controller);
117
+ return request as Promise<ClientStatusResult<T>>;
118
+ }
119
+
120
+ export function invalidateClientStatusRequest(path: string): void {
121
+ const url = agentNativePath(path);
122
+ requestGenerations.set(url, (requestGenerations.get(url) ?? 0) + 1);
123
+ cache.delete(url);
124
+ requestControllers.get(url)?.abort();
125
+ requestControllers.delete(url);
126
+ requests.delete(url);
127
+ }
128
+
129
+ export function invalidateClientStatusRequests(): void {
130
+ generation += 1;
131
+ cache.clear();
132
+ for (const controller of requestControllers.values()) {
133
+ controller.abort();
134
+ }
135
+ requestControllers.clear();
136
+ requests.clear();
137
+ }
138
+
139
+ export function fetchAgentEngineStatus<T = unknown>(): Promise<
140
+ ClientStatusResult<T>
141
+ > {
142
+ return fetchClientStatus<T>("/_agent-native/agent-engine/status");
143
+ }
144
+
145
+ export function fetchEnvironmentStatus<T = unknown>(): Promise<
146
+ ClientStatusResult<T>
147
+ > {
148
+ return fetchClientStatus<T>("/_agent-native/env-status");
149
+ }
150
+
151
+ export function fetchBuilderStatus<T = unknown>(): Promise<
152
+ ClientStatusResult<T>
153
+ > {
154
+ return fetchClientStatus<T>("/_agent-native/builder/status");
155
+ }
156
+
157
+ export function fetchAuthSessionStatus<T = unknown>(): Promise<
158
+ ClientStatusResult<T>
159
+ > {
160
+ return fetchClientStatus<T>("/_agent-native/auth/session");
161
+ }
@@ -627,8 +627,8 @@ export function VoiceTranscriptionSection() {
627
627
  id="openai"
628
628
  selected={transcriptionMode === "batch" && provider === "openai"}
629
629
  onSelect={() => chooseBatchProvider("openai")}
630
- title="OpenAI Whisper"
631
- subtitle="Batch Whisper provider. Requires an OpenAI API key."
630
+ title="OpenAI Transcribe"
631
+ subtitle="Batch transcription provider. Requires an OpenAI API key."
632
632
  rightSlot={
633
633
  openAiConfigured === null ? null : openAiConfigured ? (
634
634
  <span className="flex items-center gap-1 text-[10px] text-green-500">
@@ -34,6 +34,13 @@ export type ContentPart =
34
34
  args: Record<string, string>;
35
35
  result?: string;
36
36
  isError?: boolean;
37
+ /**
38
+ * Set when the stream ended while this tool was still in flight. We know
39
+ * it started and NOT whether its side effect landed, so it is deliberately
40
+ * separate from `isError` — an email that WAS delivered must never render
41
+ * as a failure just because the transport dropped before `tool_done`.
42
+ */
43
+ outcome?: "unknown";
37
44
  completedSideEffect?: boolean;
38
45
  mcpApp?: AgentMcpAppPayload;
39
46
  chatUI?: ActionChatUIConfig;
@@ -155,7 +162,10 @@ export function settleInterruptedToolCalls(
155
162
  part.activity === true
156
163
  ? (options?.activityResult ?? INTERRUPTED_ACTIVITY_RESULT)
157
164
  : result;
158
- part.isError = true;
165
+ // Interrupted is not failed: the side effect may well have landed. Never
166
+ // set `isError` here — that is reserved for a result the server told us
167
+ // failed.
168
+ part.outcome = "unknown";
159
169
  changed = true;
160
170
  }
161
171
  }
@@ -1029,7 +1039,8 @@ function completedToolNamesAfterLastAssistantText(
1029
1039
  part.type === "tool-call" &&
1030
1040
  part.activity !== true &&
1031
1041
  part.result !== undefined &&
1032
- part.isError !== true
1042
+ part.isError !== true &&
1043
+ part.outcome !== "unknown"
1033
1044
  ) {
1034
1045
  names.add(part.toolName);
1035
1046
  }
@@ -1053,7 +1064,8 @@ function hasCompletedCustomUi(content: ContentPart[]): boolean {
1053
1064
  part?.type !== "tool-call" ||
1054
1065
  part.activity === true ||
1055
1066
  part.result === undefined ||
1056
- part.isError === true
1067
+ part.isError === true ||
1068
+ part.outcome === "unknown"
1057
1069
  ) {
1058
1070
  continue;
1059
1071
  }
@@ -1,7 +1,13 @@
1
1
  import { useEffect, useState } from "react";
2
2
 
3
3
  import { PROVIDER_ENV_VARS } from "../agent/engine/provider-env-vars.js";
4
- import { agentNativePath } from "./api-path.js";
4
+ import {
5
+ fetchAgentEngineStatus,
6
+ fetchBuilderStatus,
7
+ fetchEnvironmentStatus,
8
+ invalidateClientStatusRequest,
9
+ type ClientStatusResult,
10
+ } from "./client-status-requests.js";
5
11
 
6
12
  const PROVIDER_ENV_VAR_SET = new Set(PROVIDER_ENV_VARS);
7
13
 
@@ -39,43 +45,32 @@ export interface UseAgentEngineConfiguredOptions {
39
45
  threadId?: string | null;
40
46
  }
41
47
 
42
- // Abort ceiling for a hung request, NOT a deadline the probes race against.
43
- // `/agent-engine/status` resolves session, org context, a scoped secret and a
44
- // settings row — several DB round-trips that measured ~4.7s on a warm local
45
- // dev server, so any budget short enough to "feel fast" just manufactures a
46
- // fake answer. Losing this ceiling is treated as "don't know", never "missing".
47
- const DEFAULT_STATUS_CHECK_TIMEOUT_MS = 15000;
48
48
  const RETRY_BASE_MS = 2000;
49
49
  const RETRY_MAX_MS = 30000;
50
50
 
51
- async function fetchStatusJson(
51
+ async function waitForStatus<T>(
52
+ request: Promise<ClientStatusResult<T>>,
52
53
  path: string,
53
- timeoutMs: number,
54
- ): Promise<unknown | null> {
55
- const controller =
56
- typeof AbortController !== "undefined" ? new AbortController() : null;
54
+ timeoutMs: number | undefined,
55
+ ): Promise<ClientStatusResult<T>> {
56
+ if (timeoutMs === undefined) return request;
57
+
57
58
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
58
- const timeout = new Promise<null>((resolve) => {
59
+ const timeout = new Promise<ClientStatusResult<T>>((resolve) => {
59
60
  timeoutId = setTimeout(() => {
60
- controller?.abort();
61
- resolve(null);
61
+ // A request that loses this race may never settle. Evict and abort the
62
+ // shared probe so the scheduled retry starts a genuinely new request.
63
+ invalidateClientStatusRequest(path);
64
+ resolve({ state: "unavailable" });
62
65
  }, timeoutMs);
63
66
  });
64
-
65
- // Never serve a stale status from the HTTP cache: this is re-fetched right
66
- // after a provider connects, and a cached "missing" would keep the composer
67
- // gate and error banner pinned even though a provider is now configured.
68
- const request = fetch(agentNativePath(path), {
69
- cache: "no-store",
70
- ...(controller ? { signal: controller.signal } : {}),
71
- })
72
- .then((r) => (r.ok ? r.json() : null))
73
- .catch(() => null)
74
- .finally(() => {
75
- if (timeoutId !== undefined) clearTimeout(timeoutId);
76
- });
77
-
78
- return Promise.race([request, timeout]);
67
+ try {
68
+ return await Promise.race([request, timeout]);
69
+ } finally {
70
+ if (timeoutId !== undefined) {
71
+ clearTimeout(timeoutId);
72
+ }
73
+ }
79
74
  }
80
75
 
81
76
  function hasConfiguredFlag(value: unknown): value is { configured: boolean } {
@@ -117,22 +112,38 @@ export async function fetchAgentEngineConfiguredState(
117
112
  const timeoutMs =
118
113
  typeof options?.timeoutMs === "number" && options.timeoutMs > 0
119
114
  ? options.timeoutMs
120
- : DEFAULT_STATUS_CHECK_TIMEOUT_MS;
121
- const [envKeys, builderStatus, engineStatus] = await Promise.all([
122
- fetchStatusJson("/_agent-native/env-status", timeoutMs),
123
- fetchStatusJson("/_agent-native/builder/status", timeoutMs),
124
- fetchStatusJson("/_agent-native/agent-engine/status", timeoutMs),
125
- ]);
126
-
127
- // All three failed — surface a retryable unavailable state instead of
128
- // leaving the composer in an infinite checking loop.
129
- if (envKeys == null && builderStatus == null && engineStatus == null) {
130
- return "unavailable";
115
+ : undefined;
116
+ const engineResult = await waitForStatus(
117
+ fetchAgentEngineStatus(),
118
+ "/_agent-native/agent-engine/status",
119
+ timeoutMs,
120
+ );
121
+ if (
122
+ engineResult.state === "available" &&
123
+ hasConfiguredFlag(engineResult.value)
124
+ ) {
125
+ return engineResult.value.configured ? "configured" : "missing";
131
126
  }
132
127
 
128
+ // Older hosts may not expose the canonical route. Only then pay for the two
129
+ // legacy probes; current hosts answer readiness with one request.
130
+ const [envResult, builderResult] = await Promise.all([
131
+ waitForStatus(
132
+ fetchEnvironmentStatus(),
133
+ "/_agent-native/env-status",
134
+ timeoutMs,
135
+ ),
136
+ waitForStatus(
137
+ fetchBuilderStatus(),
138
+ "/_agent-native/builder/status",
139
+ timeoutMs,
140
+ ),
141
+ ]);
142
+ const envKeys = envResult.state === "available" ? envResult.value : undefined;
143
+ const builderStatus =
144
+ builderResult.state === "available" ? builderResult.value : undefined;
133
145
  const envKeysKnown = Array.isArray(envKeys);
134
146
  const builderStatusKnown = hasConfiguredFlag(builderStatus);
135
- const engineStatusKnown = hasConfiguredFlag(engineStatus);
136
147
  const keys = envKeysKnown
137
148
  ? (envKeys as Array<{
138
149
  key: string;
@@ -142,16 +153,9 @@ export async function fetchAgentEngineConfiguredState(
142
153
  const llmKeys = keys.filter((k) => PROVIDER_ENV_VAR_SET.has(k.key));
143
154
  const anyConfigured =
144
155
  llmKeys.some((k) => k.configured) ||
145
- (builderStatusKnown && builderStatus.configured) ||
146
- (engineStatusKnown && engineStatus.configured);
156
+ (builderStatusKnown && builderStatus.configured);
147
157
  if (anyConfigured) return "configured";
148
158
 
149
- // The engine status route is the canonical readiness check: it resolves
150
- // Builder, scoped BYOK secrets, deployment credentials, and custom engines.
151
- // Once it has answered `configured: false`, a slow legacy env/Builder status
152
- // request must not leave the composer permissively stuck in `unknown`.
153
- if (engineStatusKnown) return "missing";
154
-
155
159
  // Compatibility fallback for older hosts without the canonical route.
156
160
  return envKeysKnown && builderStatusKnown ? "missing" : "unavailable";
157
161
  }
@@ -7,11 +7,14 @@ import {
7
7
  resolveReasoningEffortSelection,
8
8
  type ReasoningEffort,
9
9
  } from "../shared/reasoning-effort.js";
10
- import { agentNativePath } from "./api-path.js";
11
10
  import {
12
11
  buildChatModelGroups,
13
12
  type EngineModelGroup,
14
13
  } from "./chat-model-groups.js";
14
+ import {
15
+ fetchBuilderStatus,
16
+ fetchEnvironmentStatus,
17
+ } from "./client-status-requests.js";
15
18
  import { callAction } from "./use-action.js";
16
19
 
17
20
  export type { EngineModelGroup } from "./chat-model-groups.js";
@@ -192,14 +195,10 @@ export function useChatModels({
192
195
  callAction("manage-agent-engine" as any, { action: "list" } as any).catch(
193
196
  () => null,
194
197
  ),
195
- fetch(agentNativePath("/_agent-native/env-status"))
196
- .then((r) => (r.ok ? r.json() : []))
197
- .catch(() => []),
198
- fetch(agentNativePath("/_agent-native/builder/status"))
199
- .then((r) => (r.ok ? r.json() : null))
200
- .catch(() => null),
198
+ fetchEnvironmentStatus<Array<{ key: string; configured: boolean }>>(),
199
+ fetchBuilderStatus<{ configured?: boolean }>(),
201
200
  ])
202
- .then(([enginesData, envKeys, builderStatus]) => {
201
+ .then(([enginesData, envResult, builderResult]) => {
203
202
  if (!enginesData?.engines) {
204
203
  // Without a catalog the picker keeps an unvalidated DEFAULT_MODEL,
205
204
  // which is indistinguishable from a real selection unless we say so.
@@ -208,10 +207,16 @@ export function useChatModels({
208
207
  );
209
208
  return;
210
209
  }
210
+ if (
211
+ envResult.state !== "available" ||
212
+ builderResult.state !== "available"
213
+ ) {
214
+ return;
215
+ }
216
+ const envKeys = envResult.value;
217
+ const builderStatus = builderResult.value;
211
218
  const configuredKeys = new Set(
212
- (envKeys as Array<{ key: string; configured: boolean }>)
213
- .filter((k) => k.configured)
214
- .map((k) => k.key),
219
+ envKeys.filter((k) => k.configured).map((k) => k.key),
215
220
  );
216
221
  const builderConnected = builderStatus?.configured === true;
217
222
  const currentEngineName: string | undefined =
@@ -78,6 +78,35 @@ const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
78
78
  const THREADS_UPDATED_EVENT = "agent-chat:threads-updated";
79
79
  const THREADS_PAGE_SIZE = 50;
80
80
 
81
+ const sharedThreadListRequests = new Map<
82
+ string,
83
+ Promise<ChatThreadSummary[] | undefined>
84
+ >();
85
+
86
+ async function fetchSharedThreadListPage(
87
+ apiUrl: string,
88
+ offset: number,
89
+ ): Promise<ChatThreadSummary[] | undefined> {
90
+ const requestKey = `${apiUrl}\n${offset}`;
91
+ const pending = sharedThreadListRequests.get(requestKey);
92
+ if (pending) return pending;
93
+
94
+ const request = fetch(
95
+ offset > 0 ? `${apiUrl}/threads?offset=${offset}` : `${apiUrl}/threads`,
96
+ )
97
+ .then(async (res) => {
98
+ if (!res.ok) return undefined;
99
+ const data = await res.json();
100
+ const threads = (data.threads ?? []) as ChatThreadSummary[];
101
+ return threads;
102
+ })
103
+ .finally(() => {
104
+ sharedThreadListRequests.delete(requestKey);
105
+ });
106
+ sharedThreadListRequests.set(requestKey, request);
107
+ return request;
108
+ }
109
+
81
110
  function emitThreadsUpdated() {
82
111
  if (typeof window === "undefined") return;
83
112
  window.dispatchEvent(new CustomEvent(THREADS_UPDATED_EVENT));
@@ -441,20 +470,14 @@ export function useChatThreads(
441
470
  async (options?: { append?: boolean }) => {
442
471
  try {
443
472
  const offset = options?.append ? nextThreadsOffsetRef.current : 0;
444
- const res = await fetch(
445
- offset > 0
446
- ? `${apiUrl}/threads?offset=${offset}`
447
- : `${apiUrl}/threads`,
448
- );
449
- if (!res.ok) {
473
+ const loaded = await fetchSharedThreadListPage(apiUrl, offset);
474
+ if (!loaded) {
450
475
  if (!options?.append) {
451
476
  setThreadsLoadError("Could not load chat history.");
452
477
  }
453
478
  return;
454
479
  }
455
- const data = await res.json();
456
480
  setThreadsLoadError(null);
457
- const loaded = (data.threads ?? []) as ChatThreadSummary[];
458
481
  if (!options?.append) {
459
482
  nextThreadsOffsetRef.current = loaded.length;
460
483
  } else {
@@ -526,7 +549,7 @@ export function useChatThreads(
526
549
  }
527
550
  return [...optimisticOnly, ...merged];
528
551
  });
529
- return data.threads as ChatThreadSummary[];
552
+ return loaded;
530
553
  } catch {
531
554
  if (!options?.append) {
532
555
  setThreadsLoadError("Could not load chat history.");
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
2
2
 
3
3
  import type { AuthSession } from "../server/auth.js";
4
4
  import { setSentryUser, trackSessionStatus } from "./analytics.js";
5
- import { agentNativePath } from "./api-path.js";
5
+ import { fetchAuthSessionStatus } from "./client-status-requests.js";
6
6
 
7
7
  export type { AuthSession };
8
8
 
@@ -51,10 +51,9 @@ function fetchSharedSession(): Promise<AuthSession | null | undefined> {
51
51
 
52
52
  sessionRequest = (async () => {
53
53
  try {
54
- const res = await fetch(agentNativePath("/_agent-native/auth/session"));
55
- if (!res.ok) return undefined;
56
-
57
- const data = await res.json();
54
+ const result = await fetchAuthSessionStatus();
55
+ if (result.state === "unavailable") return undefined;
56
+ const data = result.value as AuthSession & { error?: unknown };
58
57
  const session = data.error ? null : (data as AuthSession);
59
58
  cachedSession = session;
60
59
  cachedSessionAt = Date.now();
package/src/db/client.ts CHANGED
@@ -638,6 +638,27 @@ function sqlAndArgs(sql: DbExecStatement): {
638
638
  : { rawSql: sql.sql, args: sql.args || [] };
639
639
  }
640
640
 
641
+ const POSTGRES_STATEMENT_TIMEOUT_HEADROOM_MS = 250;
642
+ const POSTGRES_MAX_INT = 2_147_483_647;
643
+
644
+ function hasExplicitDbTimeout(statement: DbExecStatement): boolean {
645
+ if (typeof statement === "string") return false;
646
+ const timeoutMs = Number(statement.timeoutMs);
647
+ return Number.isFinite(timeoutMs) && timeoutMs > 0;
648
+ }
649
+
650
+ function postgresStatementTimeoutMs(clientTimeoutMs: number): number {
651
+ const safeTimeoutMs = Math.max(
652
+ 1,
653
+ Math.min(POSTGRES_MAX_INT, Math.floor(clientTimeoutMs)),
654
+ );
655
+ const headroomMs = Math.min(
656
+ POSTGRES_STATEMENT_TIMEOUT_HEADROOM_MS,
657
+ Math.max(1, Math.floor(safeTimeoutMs * 0.1)),
658
+ );
659
+ return Math.max(1, safeTimeoutMs - headroomMs);
660
+ }
661
+
641
662
  export function dbExecQueryBudget(statement: DbExecStatement): {
642
663
  timeoutMs: number;
643
664
  maxAttempts: number;
@@ -1229,6 +1250,36 @@ async function createDbExecInternal(
1229
1250
  rowsAffected: result.rowCount ?? 0,
1230
1251
  };
1231
1252
  }
1253
+ async function queryNeonClientWithStatementTimeout(
1254
+ client: any,
1255
+ sql: Parameters<DbExec["execute"]>[0],
1256
+ remainingMs: () => number,
1257
+ markClientForDiscard: () => void,
1258
+ ) {
1259
+ const statementTimeoutMs = postgresStatementTimeoutMs(remainingMs());
1260
+ await queryNeonClient(
1261
+ client,
1262
+ `SET statement_timeout = ${statementTimeoutMs}`,
1263
+ remainingMs(),
1264
+ );
1265
+ try {
1266
+ return await queryNeonClient(client, sql, remainingMs());
1267
+ } finally {
1268
+ try {
1269
+ await queryNeonClient(
1270
+ client,
1271
+ "RESET statement_timeout",
1272
+ remainingMs(),
1273
+ );
1274
+ } catch (err) {
1275
+ markClientForDiscard();
1276
+ console.warn(
1277
+ "[db/neon] statement timeout reset failed; discarding connection:",
1278
+ err instanceof Error ? err.message : err,
1279
+ );
1280
+ }
1281
+ }
1282
+ }
1232
1283
  return {
1233
1284
  async execute(sql) {
1234
1285
  const { timeoutMs, maxAttempts } = dbExecQueryBudget(sql);
@@ -1276,17 +1327,25 @@ async function createDbExecInternal(
1276
1327
  released = true;
1277
1328
  client.release(err);
1278
1329
  };
1330
+ let discardClient = false;
1279
1331
 
1280
1332
  try {
1281
- const result = await queryNeonClient(
1282
- client,
1283
- sql,
1284
- remainingAttemptMs(),
1285
- );
1286
- releaseClient();
1333
+ const result = hasExplicitDbTimeout(sql)
1334
+ ? await queryNeonClientWithStatementTimeout(
1335
+ client,
1336
+ sql,
1337
+ remainingAttemptMs,
1338
+ () => {
1339
+ discardClient = true;
1340
+ },
1341
+ )
1342
+ : await queryNeonClient(client, sql, remainingAttemptMs());
1343
+ releaseClient(discardClient ? true : undefined);
1287
1344
  return result;
1288
1345
  } catch (err) {
1289
- releaseClient(isConnectionError(err) ? true : undefined);
1346
+ releaseClient(
1347
+ discardClient || isConnectionError(err) ? true : undefined,
1348
+ );
1290
1349
  throw err;
1291
1350
  }
1292
1351
  }, maxAttempts);
@@ -1317,7 +1376,23 @@ async function createDbExecInternal(
1317
1376
  client.release(err);
1318
1377
  };
1319
1378
  const tx: DbExec = {
1320
- execute: (sql) => queryNeonClient(client, sql),
1379
+ execute: async (sql) => {
1380
+ if (!hasExplicitDbTimeout(sql)) {
1381
+ return queryNeonClient(client, sql);
1382
+ }
1383
+ const { timeoutMs } = dbExecQueryBudget(sql);
1384
+ const startedAt = Date.now();
1385
+ const remainingMs = () =>
1386
+ Math.max(1, timeoutMs - (Date.now() - startedAt));
1387
+ const statementTimeoutMs =
1388
+ postgresStatementTimeoutMs(remainingMs());
1389
+ await queryNeonClient(
1390
+ client,
1391
+ `SET LOCAL statement_timeout = ${statementTimeoutMs}`,
1392
+ remainingMs(),
1393
+ );
1394
+ return queryNeonClient(client, sql, remainingMs());
1395
+ },
1321
1396
  };
1322
1397
  try {
1323
1398
  await queryNeonClient(client, "BEGIN");