@agent-native/core 0.131.9 → 0.132.1

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 (295) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +105 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
  4. package/corpus/core/docs/design/durable-agent-runs.md +15 -11
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/activity.ts +127 -10
  7. package/corpus/core/src/a2a/index.ts +3 -0
  8. package/corpus/core/src/action.ts +52 -11
  9. package/corpus/core/src/agent/durable-background.ts +38 -28
  10. package/corpus/core/src/agent/harness/background.ts +12 -2
  11. package/corpus/core/src/agent/production-agent.ts +210 -43
  12. package/corpus/core/src/agent/run-store.ts +45 -3
  13. package/corpus/core/src/application-state/handlers.ts +45 -0
  14. package/corpus/core/src/application-state/index.ts +3 -0
  15. package/corpus/core/src/application-state/store.ts +28 -11
  16. package/corpus/core/src/audit/redact.ts +104 -18
  17. package/corpus/core/src/client/AgentPanel.tsx +2 -7
  18. package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
  19. package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
  20. package/corpus/core/src/client/agent-chat.ts +4 -9
  21. package/corpus/core/src/client/application-state.ts +115 -12
  22. package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
  23. package/corpus/core/src/client/guided-questions.tsx +12 -23
  24. package/corpus/core/src/db/client.ts +148 -28
  25. package/corpus/core/src/db/create-get-db.ts +39 -6
  26. package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
  27. package/corpus/core/src/deploy/build.ts +26 -33
  28. package/corpus/core/src/feature-flags/store.ts +10 -5
  29. package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
  30. package/corpus/core/src/integrations/config-store.ts +13 -0
  31. package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
  32. package/corpus/core/src/mcp-client/routes.ts +33 -2
  33. package/corpus/core/src/onboarding/plugin.ts +31 -25
  34. package/corpus/core/src/org/accept-pending.ts +2 -0
  35. package/corpus/core/src/org/auto-join-domain.ts +2 -0
  36. package/corpus/core/src/org/context.ts +35 -10
  37. package/corpus/core/src/org/handlers.ts +6 -0
  38. package/corpus/core/src/org/request-org-cache.ts +71 -0
  39. package/corpus/core/src/resources/store.ts +10 -9
  40. package/corpus/core/src/scripts/runner.ts +6 -3
  41. package/corpus/core/src/secrets/storage.ts +97 -5
  42. package/corpus/core/src/server/action-change.ts +33 -0
  43. package/corpus/core/src/server/action-routes.ts +11 -9
  44. package/corpus/core/src/server/agent-teams.ts +10 -2
  45. package/corpus/core/src/server/better-auth-instance.ts +39 -12
  46. package/corpus/core/src/server/core-routes-plugin.ts +19 -2
  47. package/corpus/core/src/server/credential-provider.ts +57 -14
  48. package/corpus/core/src/server/http-response-telemetry.ts +1 -0
  49. package/corpus/core/src/server/poll.ts +95 -57
  50. package/corpus/core/src/server/voice-providers-status.ts +19 -4
  51. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
  52. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
  53. package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
  54. package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
  55. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
  56. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
  57. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
  58. package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
  59. package/corpus/templates/analytics/app/i18n-data.ts +40 -0
  60. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
  61. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
  62. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
  63. package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
  64. package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
  66. package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
  67. package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
  68. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
  69. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  70. package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
  71. package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
  72. package/corpus/templates/content/netlify.toml +3 -0
  73. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
  74. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
  75. package/corpus/templates/slides/actions/patch-deck.ts +25 -1
  76. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
  77. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
  78. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  79. package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
  80. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
  81. package/corpus/templates/slides/app/pages/Index.tsx +2 -2
  82. package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
  83. package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
  84. package/corpus/toolkit/CHANGELOG.md +8 -0
  85. package/corpus/toolkit/package.json +1 -1
  86. package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
  87. package/dist/a2a/activity.d.ts +22 -4
  88. package/dist/a2a/activity.d.ts.map +1 -1
  89. package/dist/a2a/activity.js +97 -11
  90. package/dist/a2a/activity.js.map +1 -1
  91. package/dist/a2a/index.d.ts +1 -1
  92. package/dist/a2a/index.d.ts.map +1 -1
  93. package/dist/a2a/index.js +1 -1
  94. package/dist/a2a/index.js.map +1 -1
  95. package/dist/action.d.ts.map +1 -1
  96. package/dist/action.js +41 -11
  97. package/dist/action.js.map +1 -1
  98. package/dist/agent/durable-background.d.ts +13 -8
  99. package/dist/agent/durable-background.d.ts.map +1 -1
  100. package/dist/agent/durable-background.js +36 -28
  101. package/dist/agent/durable-background.js.map +1 -1
  102. package/dist/agent/harness/background.d.ts.map +1 -1
  103. package/dist/agent/harness/background.js +12 -2
  104. package/dist/agent/harness/background.js.map +1 -1
  105. package/dist/agent/production-agent.d.ts.map +1 -1
  106. package/dist/agent/production-agent.js +195 -44
  107. package/dist/agent/production-agent.js.map +1 -1
  108. package/dist/agent/run-store.d.ts +2 -0
  109. package/dist/agent/run-store.d.ts.map +1 -1
  110. package/dist/agent/run-store.js +44 -3
  111. package/dist/agent/run-store.js.map +1 -1
  112. package/dist/application-state/handlers.d.ts +15 -0
  113. package/dist/application-state/handlers.d.ts.map +1 -1
  114. package/dist/application-state/handlers.js +42 -2
  115. package/dist/application-state/handlers.js.map +1 -1
  116. package/dist/application-state/index.d.ts +2 -2
  117. package/dist/application-state/index.d.ts.map +1 -1
  118. package/dist/application-state/index.js +2 -2
  119. package/dist/application-state/index.js.map +1 -1
  120. package/dist/application-state/store.d.ts +11 -0
  121. package/dist/application-state/store.d.ts.map +1 -1
  122. package/dist/application-state/store.js +23 -10
  123. package/dist/application-state/store.js.map +1 -1
  124. package/dist/audit/redact.d.ts +25 -2
  125. package/dist/audit/redact.d.ts.map +1 -1
  126. package/dist/audit/redact.js +85 -21
  127. package/dist/audit/redact.js.map +1 -1
  128. package/dist/client/AgentPanel.d.ts.map +1 -1
  129. package/dist/client/AgentPanel.js +2 -7
  130. package/dist/client/AgentPanel.js.map +1 -1
  131. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  132. package/dist/client/MultiTabAssistantChat.js +1 -1
  133. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  134. package/dist/client/RunStuckBanner.d.ts +11 -1
  135. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  136. package/dist/client/RunStuckBanner.js +20 -7
  137. package/dist/client/RunStuckBanner.js.map +1 -1
  138. package/dist/client/agent-chat.d.ts.map +1 -1
  139. package/dist/client/agent-chat.js +4 -6
  140. package/dist/client/agent-chat.js.map +1 -1
  141. package/dist/client/application-state.d.ts +11 -0
  142. package/dist/client/application-state.d.ts.map +1 -1
  143. package/dist/client/application-state.js +87 -9
  144. package/dist/client/application-state.js.map +1 -1
  145. package/dist/client/dynamic-suggestions.d.ts.map +1 -1
  146. package/dist/client/dynamic-suggestions.js +4 -13
  147. package/dist/client/dynamic-suggestions.js.map +1 -1
  148. package/dist/client/guided-questions.d.ts.map +1 -1
  149. package/dist/client/guided-questions.js +6 -22
  150. package/dist/client/guided-questions.js.map +1 -1
  151. package/dist/collab/routes.d.ts +1 -1
  152. package/dist/collab/struct-routes.d.ts +1 -1
  153. package/dist/db/client.d.ts +57 -10
  154. package/dist/db/client.d.ts.map +1 -1
  155. package/dist/db/client.js +122 -31
  156. package/dist/db/client.js.map +1 -1
  157. package/dist/db/create-get-db.d.ts.map +1 -1
  158. package/dist/db/create-get-db.js +31 -7
  159. package/dist/db/create-get-db.js.map +1 -1
  160. package/dist/db/runtime-diagnostics.d.ts +1 -0
  161. package/dist/db/runtime-diagnostics.d.ts.map +1 -1
  162. package/dist/db/runtime-diagnostics.js +31 -6
  163. package/dist/db/runtime-diagnostics.js.map +1 -1
  164. package/dist/deploy/build.d.ts +5 -20
  165. package/dist/deploy/build.d.ts.map +1 -1
  166. package/dist/deploy/build.js +18 -30
  167. package/dist/deploy/build.js.map +1 -1
  168. package/dist/feature-flags/store.d.ts.map +1 -1
  169. package/dist/feature-flags/store.js +10 -5
  170. package/dist/feature-flags/store.js.map +1 -1
  171. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  172. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  173. package/dist/integrations/a2a-continuations-store.js +12 -0
  174. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  175. package/dist/integrations/config-store.d.ts +1 -0
  176. package/dist/integrations/config-store.d.ts.map +1 -1
  177. package/dist/integrations/config-store.js +11 -0
  178. package/dist/integrations/config-store.js.map +1 -1
  179. package/dist/integrations/google-docs-poller.d.ts.map +1 -1
  180. package/dist/integrations/google-docs-poller.js +19 -2
  181. package/dist/integrations/google-docs-poller.js.map +1 -1
  182. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  183. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  184. package/dist/mcp-client/routes.d.ts.map +1 -1
  185. package/dist/mcp-client/routes.js +30 -2
  186. package/dist/mcp-client/routes.js.map +1 -1
  187. package/dist/notifications/routes.d.ts +3 -3
  188. package/dist/observability/routes.d.ts +1 -1
  189. package/dist/onboarding/plugin.d.ts.map +1 -1
  190. package/dist/onboarding/plugin.js +12 -8
  191. package/dist/onboarding/plugin.js.map +1 -1
  192. package/dist/org/accept-pending.d.ts.map +1 -1
  193. package/dist/org/accept-pending.js +2 -0
  194. package/dist/org/accept-pending.js.map +1 -1
  195. package/dist/org/auto-join-domain.d.ts.map +1 -1
  196. package/dist/org/auto-join-domain.js +2 -0
  197. package/dist/org/auto-join-domain.js.map +1 -1
  198. package/dist/org/context.d.ts.map +1 -1
  199. package/dist/org/context.js +29 -9
  200. package/dist/org/context.js.map +1 -1
  201. package/dist/org/handlers.d.ts.map +1 -1
  202. package/dist/org/handlers.js +5 -0
  203. package/dist/org/handlers.js.map +1 -1
  204. package/dist/org/request-org-cache.d.ts +16 -0
  205. package/dist/org/request-org-cache.d.ts.map +1 -0
  206. package/dist/org/request-org-cache.js +62 -0
  207. package/dist/org/request-org-cache.js.map +1 -0
  208. package/dist/resources/store.d.ts.map +1 -1
  209. package/dist/resources/store.js +11 -9
  210. package/dist/resources/store.js.map +1 -1
  211. package/dist/scripts/runner.d.ts.map +1 -1
  212. package/dist/scripts/runner.js +3 -3
  213. package/dist/scripts/runner.js.map +1 -1
  214. package/dist/secrets/storage.d.ts.map +1 -1
  215. package/dist/secrets/storage.js +82 -5
  216. package/dist/secrets/storage.js.map +1 -1
  217. package/dist/server/action-change.d.ts +20 -0
  218. package/dist/server/action-change.d.ts.map +1 -1
  219. package/dist/server/action-change.js +29 -0
  220. package/dist/server/action-change.js.map +1 -1
  221. package/dist/server/action-routes.d.ts.map +1 -1
  222. package/dist/server/action-routes.js +7 -8
  223. package/dist/server/action-routes.js.map +1 -1
  224. package/dist/server/agent-teams.d.ts.map +1 -1
  225. package/dist/server/agent-teams.js +10 -2
  226. package/dist/server/agent-teams.js.map +1 -1
  227. package/dist/server/better-auth-instance.d.ts.map +1 -1
  228. package/dist/server/better-auth-instance.js +31 -14
  229. package/dist/server/better-auth-instance.js.map +1 -1
  230. package/dist/server/core-routes-plugin.d.ts +1 -0
  231. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  232. package/dist/server/core-routes-plugin.js +18 -4
  233. package/dist/server/core-routes-plugin.js.map +1 -1
  234. package/dist/server/credential-provider.d.ts +16 -0
  235. package/dist/server/credential-provider.d.ts.map +1 -1
  236. package/dist/server/credential-provider.js +49 -13
  237. package/dist/server/credential-provider.js.map +1 -1
  238. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  239. package/dist/server/http-response-telemetry.js +1 -0
  240. package/dist/server/http-response-telemetry.js.map +1 -1
  241. package/dist/server/poll.d.ts +6 -0
  242. package/dist/server/poll.d.ts.map +1 -1
  243. package/dist/server/poll.js +84 -47
  244. package/dist/server/poll.js.map +1 -1
  245. package/dist/server/voice-providers-status.d.ts.map +1 -1
  246. package/dist/server/voice-providers-status.js +12 -3
  247. package/dist/server/voice-providers-status.js.map +1 -1
  248. package/docs/content/durable-background-runs.mdx +15 -6
  249. package/docs/design/durable-agent-runs.md +15 -11
  250. package/package.json +2 -2
  251. package/src/a2a/activity.ts +127 -10
  252. package/src/a2a/index.ts +3 -0
  253. package/src/action.ts +52 -11
  254. package/src/agent/durable-background.ts +38 -28
  255. package/src/agent/harness/background.ts +12 -2
  256. package/src/agent/production-agent.ts +210 -43
  257. package/src/agent/run-store.ts +45 -3
  258. package/src/application-state/handlers.ts +45 -0
  259. package/src/application-state/index.ts +3 -0
  260. package/src/application-state/store.ts +28 -11
  261. package/src/audit/redact.ts +104 -18
  262. package/src/client/AgentPanel.tsx +2 -7
  263. package/src/client/MultiTabAssistantChat.tsx +3 -0
  264. package/src/client/RunStuckBanner.tsx +29 -6
  265. package/src/client/agent-chat.ts +4 -9
  266. package/src/client/application-state.ts +115 -12
  267. package/src/client/dynamic-suggestions.ts +4 -12
  268. package/src/client/guided-questions.tsx +12 -23
  269. package/src/db/client.ts +148 -28
  270. package/src/db/create-get-db.ts +39 -6
  271. package/src/db/runtime-diagnostics.ts +41 -6
  272. package/src/deploy/build.ts +26 -33
  273. package/src/feature-flags/store.ts +10 -5
  274. package/src/integrations/a2a-continuations-store.ts +11 -0
  275. package/src/integrations/config-store.ts +13 -0
  276. package/src/integrations/google-docs-poller.ts +26 -2
  277. package/src/mcp-client/routes.ts +33 -2
  278. package/src/onboarding/plugin.ts +31 -25
  279. package/src/org/accept-pending.ts +2 -0
  280. package/src/org/auto-join-domain.ts +2 -0
  281. package/src/org/context.ts +35 -10
  282. package/src/org/handlers.ts +6 -0
  283. package/src/org/request-org-cache.ts +71 -0
  284. package/src/resources/store.ts +10 -9
  285. package/src/scripts/runner.ts +6 -3
  286. package/src/secrets/storage.ts +97 -5
  287. package/src/server/action-change.ts +33 -0
  288. package/src/server/action-routes.ts +11 -9
  289. package/src/server/agent-teams.ts +10 -2
  290. package/src/server/better-auth-instance.ts +39 -12
  291. package/src/server/core-routes-plugin.ts +19 -2
  292. package/src/server/credential-provider.ts +57 -14
  293. package/src/server/http-response-telemetry.ts +1 -0
  294. package/src/server/poll.ts +95 -57
  295. package/src/server/voice-providers-status.ts +19 -4
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
- import Ajv, { type ValidateFunction } from "ajv";
3
+ import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
4
4
  import {
5
5
  defineEventHandler,
6
6
  getHeader,
@@ -3596,6 +3596,9 @@ const rawToolInputAjv = new Ajv({
3596
3596
  coerceTypes: true,
3597
3597
  useDefaults: false,
3598
3598
  removeAdditional: false,
3599
+ // `parentSchema`/`data` on each error are what let us drop the branches of a
3600
+ // `oneOf` the caller never meant to match.
3601
+ verbose: true,
3599
3602
  });
3600
3603
 
3601
3604
  const rawToolInputValidatorCache = new WeakMap<object, ValidateFunction>();
@@ -3636,17 +3639,83 @@ function isStructurallyEmptyToolValue(value: unknown): boolean {
3636
3639
  );
3637
3640
  }
3638
3641
 
3642
+ function compileToolValueValidator(schema: object): ValidateFunction | null {
3643
+ const cached = optionalPlaceholderValidatorCache.get(schema);
3644
+ if (cached) return cached;
3645
+ try {
3646
+ const validator = optionalPlaceholderAjv.compile(schema);
3647
+ optionalPlaceholderValidatorCache.set(schema, validator);
3648
+ return validator;
3649
+ } catch {
3650
+ return null;
3651
+ }
3652
+ }
3653
+
3639
3654
  function schemaAcceptsToolValue(schema: object, value: unknown): boolean {
3640
- let validator = optionalPlaceholderValidatorCache.get(schema);
3641
- if (!validator) {
3642
- try {
3643
- validator = optionalPlaceholderAjv.compile(schema);
3644
- optionalPlaceholderValidatorCache.set(schema, validator);
3645
- } catch {
3646
- return false;
3647
- }
3655
+ const validator = compileToolValueValidator(schema);
3656
+ return validator ? Boolean(validator(value)) : false;
3657
+ }
3658
+
3659
+ /**
3660
+ * True only when the sub-schema compiled AND rejected the value. A sub-schema
3661
+ * that cannot be compiled standalone — a `$ref` into the root `$defs`, say — is
3662
+ * unknown, not invalid; counting it as invalid would let one unreadable
3663
+ * sub-schema delete the caller's intentional empty values everywhere else in
3664
+ * the same object.
3665
+ */
3666
+ function schemaRejectsToolValue(schema: object, value: unknown): boolean {
3667
+ const validator = compileToolValueValidator(schema);
3668
+ return validator ? !validator(value) : false;
3669
+ }
3670
+
3671
+ /** The `const`/single-value-`enum` a discriminated-union branch pins a key to. */
3672
+ function schemaDiscriminatorValue(
3673
+ schema: AgentNativeJsonSchema | undefined,
3674
+ ): unknown {
3675
+ if (!schema) return undefined;
3676
+ if (schema.const !== undefined) return schema.const;
3677
+ if (Array.isArray(schema.enum) && schema.enum.length === 1) {
3678
+ return schema.enum[0];
3648
3679
  }
3649
- return Boolean(validator(value));
3680
+ return undefined;
3681
+ }
3682
+
3683
+ /**
3684
+ * Index of the `oneOf`/`anyOf` branch whose discriminator constants all match
3685
+ * `value`, or -1 when the union is not discriminated or nothing matches.
3686
+ */
3687
+ function discriminatedBranchIndex(
3688
+ branches: readonly AgentNativeJsonSchema[],
3689
+ value: unknown,
3690
+ ): number {
3691
+ if (!value || typeof value !== "object" || Array.isArray(value)) return -1;
3692
+ const record = value as Record<string, unknown>;
3693
+ return branches.findIndex((branch) => {
3694
+ const properties = branch.properties;
3695
+ if (!properties) return false;
3696
+ const discriminators = Object.entries(properties).filter(
3697
+ ([, spec]) => schemaDiscriminatorValue(spec) !== undefined,
3698
+ );
3699
+ if (discriminators.length === 0) return false;
3700
+ return discriminators.every(
3701
+ ([key, spec]) => record[key] === schemaDiscriminatorValue(spec),
3702
+ );
3703
+ });
3704
+ }
3705
+
3706
+ /**
3707
+ * The object schema that actually governs `value` — the union branch its
3708
+ * discriminator selects, or the schema itself. Undefined when a union cannot be
3709
+ * resolved, since guessing a branch would strip the wrong keys.
3710
+ */
3711
+ function resolveObjectSchemaBranch(
3712
+ schema: RawJsonSchema,
3713
+ value: unknown,
3714
+ ): RawJsonSchema | undefined {
3715
+ const branches = schema.oneOf ?? schema.anyOf;
3716
+ if (!branches?.length) return schema;
3717
+ const index = discriminatedBranchIndex(branches, value);
3718
+ return index >= 0 ? branches[index] : undefined;
3650
3719
  }
3651
3720
 
3652
3721
  /**
@@ -3654,35 +3723,82 @@ function schemaAcceptsToolValue(schema: object, value: unknown): boolean {
3654
3723
  * instead of omitting it. One schema-invalid empty value proves that pattern;
3655
3724
  * only then strip the other optional empty/default sentinels from the call.
3656
3725
  * Calls whose empty values are all schema-valid keep their intentional clears.
3726
+ *
3727
+ * The proof is evaluated per object, and the walk descends through array items
3728
+ * and discriminated-union branches: gateways pre-fill the leaf object they are
3729
+ * building (`operations[0].fields`), which a top-level-only sweep never reaches.
3657
3730
  */
3658
- function normalizeOptionalToolPlaceholders(
3731
+ function stripOptionalToolPlaceholders(
3659
3732
  schema: RawJsonSchema | undefined,
3660
- input: unknown,
3661
- ): { input: unknown; changed: boolean } {
3662
- if (!schema?.properties || !input || typeof input !== "object") {
3663
- return { input, changed: false };
3733
+ value: unknown,
3734
+ ): { value: unknown; changed: boolean } {
3735
+ if (!schema) return { value, changed: false };
3736
+
3737
+ if (Array.isArray(value)) {
3738
+ const itemSchema = schema.items;
3739
+ if (!itemSchema) return { value, changed: false };
3740
+ let changed = false;
3741
+ const items = value.map((item) => {
3742
+ const result = stripOptionalToolPlaceholders(itemSchema, item);
3743
+ if (result.changed) changed = true;
3744
+ return result.value;
3745
+ });
3746
+ return changed
3747
+ ? { value: items, changed: true }
3748
+ : { value, changed: false };
3664
3749
  }
3665
- if (Array.isArray(input)) return { input, changed: false };
3750
+
3751
+ if (!value || typeof value !== "object") return { value, changed: false };
3752
+
3753
+ const objectSchema = resolveObjectSchemaBranch(schema, value);
3754
+ const properties = objectSchema?.properties;
3755
+ if (!properties) return { value, changed: false };
3666
3756
 
3667
3757
  const required = new Set(
3668
- Array.isArray(schema.required) ? schema.required : [],
3758
+ Array.isArray(objectSchema.required) ? objectSchema.required : [],
3669
3759
  );
3760
+ const record = value as Record<string, unknown>;
3670
3761
  const placeholders: string[] = [];
3671
3762
  let hasSchemaInvalidPlaceholder = false;
3672
- for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
3673
- if (required.has(key) || !isStructurallyEmptyToolValue(value)) continue;
3674
- const propertySchema = schema.properties[key];
3763
+ let normalized: Record<string, unknown> | null = null;
3764
+
3765
+ for (const [key, entry] of Object.entries(record)) {
3766
+ const propertySchema = properties[key];
3675
3767
  if (!propertySchema || typeof propertySchema !== "object") continue;
3676
- placeholders.push(key);
3677
- if (!schemaAcceptsToolValue(propertySchema, value)) {
3678
- hasSchemaInvalidPlaceholder = true;
3768
+ if (!required.has(key) && isStructurallyEmptyToolValue(entry)) {
3769
+ placeholders.push(key);
3770
+ if (schemaRejectsToolValue(propertySchema, entry)) {
3771
+ hasSchemaInvalidPlaceholder = true;
3772
+ }
3773
+ continue;
3679
3774
  }
3775
+ const nested = stripOptionalToolPlaceholders(propertySchema, entry);
3776
+ if (nested.changed) {
3777
+ normalized ??= { ...record };
3778
+ normalized[key] = nested.value;
3779
+ }
3780
+ }
3781
+
3782
+ if (hasSchemaInvalidPlaceholder) {
3783
+ normalized ??= { ...record };
3784
+ for (const key of placeholders) delete normalized[key];
3680
3785
  }
3681
- if (!hasSchemaInvalidPlaceholder) return { input, changed: false };
3682
3786
 
3683
- const normalized = { ...(input as Record<string, unknown>) };
3684
- for (const key of placeholders) delete normalized[key];
3685
- return { input: normalized, changed: true };
3787
+ return normalized
3788
+ ? { value: normalized, changed: true }
3789
+ : { value, changed: false };
3790
+ }
3791
+
3792
+ function normalizeOptionalToolPlaceholders(
3793
+ schema: RawJsonSchema | undefined,
3794
+ input: unknown,
3795
+ ): { input: unknown; changed: boolean } {
3796
+ if (!schema?.properties || !input || typeof input !== "object") {
3797
+ return { input, changed: false };
3798
+ }
3799
+ if (Array.isArray(input)) return { input, changed: false };
3800
+ const result = stripOptionalToolPlaceholders(schema, input);
3801
+ return { input: result.value, changed: result.changed };
3686
3802
  }
3687
3803
 
3688
3804
  /**
@@ -3754,6 +3870,54 @@ function shouldValidateRawToolParameters(entry: ActionEntry): boolean {
3754
3870
  return !maybeSchema?.["~standard"] && Boolean(entry.tool.parameters);
3755
3871
  }
3756
3872
 
3873
+ /**
3874
+ * With `allErrors`, a failing union reports every branch, so a five-branch
3875
+ * union answers "your `patch-deck-fields` op has a bad enum" with eight
3876
+ * complaints about `slideId`/`orderedIds` from the four branches the caller
3877
+ * never meant. The model reads the loudest, wrong advice and re-sends the same
3878
+ * arguments until the identical-error breaker ends the turn. When the
3879
+ * discriminator names a branch, report only that branch's errors.
3880
+ *
3881
+ * Both union keywords are load-bearing for the same Zod schema: Zod v4's own
3882
+ * `toJSONSchema` emits `oneOf` for a discriminated union, while the manual
3883
+ * fallback converter in `action.ts` emits `anyOf`.
3884
+ *
3885
+ * Scoped by `instancePath` as well as `schemaPath`: every element of an array
3886
+ * shares one `items` schema, so `operations[0]` and `operations[1]` produce
3887
+ * branch errors under the same `schemaPath` and only the instance path says
3888
+ * which element they belong to.
3889
+ */
3890
+ function isWithinInstancePath(candidate: string, root: string): boolean {
3891
+ return candidate === root || candidate.startsWith(`${root}/`);
3892
+ }
3893
+
3894
+ function narrowUnionBranchErrors(
3895
+ errors: ErrorObject[] | null | undefined,
3896
+ ): ErrorObject[] | null | undefined {
3897
+ if (!errors?.length) return errors;
3898
+ let kept = errors;
3899
+ for (const error of errors) {
3900
+ const keyword = error.keyword;
3901
+ if (keyword !== "oneOf" && keyword !== "anyOf") continue;
3902
+ const branches = (error.parentSchema as RawJsonSchema | undefined)?.[
3903
+ keyword
3904
+ ];
3905
+ if (!branches?.length) continue;
3906
+ const index = discriminatedBranchIndex(branches, error.data);
3907
+ if (index < 0) continue;
3908
+ const branchPrefix = `${error.schemaPath}/`;
3909
+ kept = kept.filter((candidate) => {
3910
+ if (candidate === error) return false;
3911
+ if (!isWithinInstancePath(candidate.instancePath, error.instancePath)) {
3912
+ return true;
3913
+ }
3914
+ if (!candidate.schemaPath.startsWith(branchPrefix)) return true;
3915
+ return candidate.schemaPath.startsWith(`${branchPrefix}${index}/`);
3916
+ });
3917
+ }
3918
+ return kept.length ? kept : errors;
3919
+ }
3920
+
3757
3921
  function validateRawToolInput(
3758
3922
  entry: ActionEntry,
3759
3923
  input: unknown,
@@ -3768,7 +3932,7 @@ function validateRawToolInput(
3768
3932
  return `tool schema is invalid: ${sanitizeToolErrorValue(err)}`;
3769
3933
  }
3770
3934
  if (validator(input === undefined ? {} : input)) return null;
3771
- return rawToolInputAjv.errorsText(validator.errors, {
3935
+ return rawToolInputAjv.errorsText(narrowUnionBranchErrors(validator.errors), {
3772
3936
  separator: "; ",
3773
3937
  dataVar: "input",
3774
3938
  });
@@ -5673,23 +5837,26 @@ export async function runAgentLoop(opts: {
5673
5837
  result = `${result}\n\n${formatAgentWarningsForToolResult(agentWarnings)}`;
5674
5838
  }
5675
5839
 
5676
- // Auto-refresh the UI after a successful mutating tool call. Any action
5677
- // that isn't explicitly read-only is assumed to mutate. The client's
5678
- // useDbSync listener sees a change event with source:"action" and
5679
- // invalidates ["action"] queries so list-* / get-* refetch. This makes
5680
- // refresh after agent writes reliable without the model needing to
5681
- // remember to call `refresh-screen` itself.
5682
- if (!isError && actionEntry.readOnly !== true) {
5840
+ // Auto-refresh the UI after a successful mutating tool call. Any call
5841
+ // that isn't read-only by its own per-call Plan-mode effect, else the
5842
+ // action's readOnly flag is assumed to mutate. The client's useDbSync
5843
+ // listener sees a change event with source:"action" and invalidates
5844
+ // ["action"] queries so list-* / get-* refetch. This makes refresh after
5845
+ // agent writes reliable without the model needing to remember to call
5846
+ // `refresh-screen` itself.
5847
+ if (!isError) {
5683
5848
  try {
5684
- const { notifyActionChange } =
5849
+ const { actionCallIsReadOnly, notifyActionChange } =
5685
5850
  await import("../server/action-change.js");
5686
- const owner = opts.ownerEmail ?? getRequestUserEmail() ?? undefined;
5687
- const orgId = opts.orgId ?? getRequestOrgId() ?? undefined;
5688
- await notifyActionChange({
5689
- actionName: toolCall.name,
5690
- ...(owner ? { owner } : {}),
5691
- ...(orgId ? { orgId } : {}),
5692
- });
5851
+ if (!actionCallIsReadOnly(actionEntry, toolCall.input, false)) {
5852
+ const owner = opts.ownerEmail ?? getRequestUserEmail() ?? undefined;
5853
+ const orgId = opts.orgId ?? getRequestOrgId() ?? undefined;
5854
+ await notifyActionChange({
5855
+ actionName: toolCall.name,
5856
+ ...(owner ? { owner } : {}),
5857
+ ...(orgId ? { orgId } : {}),
5858
+ });
5859
+ }
5693
5860
  } catch {
5694
5861
  // poll module may be unavailable in non-server contexts — ignore
5695
5862
  }
@@ -163,6 +163,46 @@ export const UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS = 5 * 60_000;
163
163
  */
164
164
  export const UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS = 20_000;
165
165
 
166
+ /**
167
+ * How long a negative `status='running'` probe stays trusted.
168
+ *
169
+ * MUST stay well below the tightest staleness window any sweep enforces
170
+ * (`RUN_STALE_MS`, 15s): the probe sees running rows of EVERY age, so a
171
+ * negative result proves there were zero running rows that instant, and a row
172
+ * inserted since — by this process or another one — cannot yet be old enough
173
+ * for a sweep to act on it. Widen this past a staleness window and a genuinely
174
+ * dead run becomes invisible for a whole extra tick.
175
+ */
176
+ const NO_RUNNING_RUNS_CACHE_MS = 5_000;
177
+
178
+ let _noRunningRunsUntil = 0;
179
+
180
+ /** Test seam — the probe cache is module state, so suites must clear it. */
181
+ export function __resetNoRunningRunsProbeForTests(): void {
182
+ _noRunningRunsUntil = 0;
183
+ }
184
+
185
+ /**
186
+ * `status = 'running'` is a strict superset of every sweep predicate in this
187
+ * file, so one probe answers all of them. The fast sweep runs `reapAllStaleRuns`
188
+ * and `listUnclaimedBackgroundRunRows` back to back every 20s; on an idle app
189
+ * both scan for rows that do not exist, which on a remote database is two
190
+ * round trips per tick per app, forever. The probe collapses that pair to one.
191
+ */
192
+ async function hasRunningRuns(): Promise<boolean> {
193
+ if (Date.now() < _noRunningRunsUntil) return false;
194
+ const { rows } = await getDbExec().execute({
195
+ sql: `SELECT id FROM agent_runs WHERE status = 'running' LIMIT 1`,
196
+ args: [],
197
+ });
198
+ if ((rows?.length ?? 0) > 0) {
199
+ _noRunningRunsUntil = 0;
200
+ return true;
201
+ }
202
+ _noRunningRunsUntil = Date.now() + NO_RUNNING_RUNS_CACHE_MS;
203
+ return false;
204
+ }
205
+
166
206
  /**
167
207
  * FIX 3 (durable-background incident) per-turn run-count ceiling for
168
208
  * stale-run recovery — mirrors `chainServerDrivenContinuation`'s own ledger
@@ -920,6 +960,7 @@ export async function listUnclaimedBackgroundRunRows(): Promise<
920
960
  UnclaimedBackgroundRunRow[]
921
961
  > {
922
962
  await ensureRunTables();
963
+ if (!(await hasRunningRuns())) return [];
923
964
  const client = getDbExec();
924
965
  const { rows } = await client.execute({
925
966
  // CAST keeps the ms-epoch param 64-bit on Postgres (see
@@ -2538,6 +2579,7 @@ export async function getCurrentTurnEventsForThread(
2538
2579
  */
2539
2580
  export async function reapAllStaleRuns(): Promise<number> {
2540
2581
  await ensureRunTables();
2582
+ if (!(await hasRunningRuns())) return 0;
2541
2583
  const client = getDbExec();
2542
2584
  const now = Date.now();
2543
2585
  // Background-dispatched runs use the wider window; everything else 15s. The
@@ -2570,9 +2612,9 @@ export async function reapAllStaleRuns(): Promise<number> {
2570
2612
  // `reapSingleStaleRun` re-applies the identical staleness clause per row
2571
2613
  // (same as the bulk UPDATE previously did), so a row whose heartbeat
2572
2614
  // landed between the SELECT above and this loop is still correctly
2573
- // excluded. This function only runs once, at process startup (see
2574
- // agent-chat-plugin.ts) the extra per-row round trips are
2575
- // inconsequential.
2615
+ // excluded. The per-row round trips only ever run for rows the SELECT above
2616
+ // already found stale; on an idle app `hasRunningRuns` returns before any of
2617
+ // this, so the 20s fast sweep (agent-chat-plugin.ts) costs one probe.
2576
2618
  let reapedCount = 0;
2577
2619
  for (const row of stale.rows) {
2578
2620
  const id = (row as { id?: unknown }).id;
@@ -3,6 +3,7 @@ import {
3
3
  defineEventHandler,
4
4
  getRouterParam,
5
5
  getHeader,
6
+ getQuery,
6
7
  setResponseStatus,
7
8
  type H3Event,
8
9
  } from "h3";
@@ -11,6 +12,7 @@ import { getSession } from "../server/auth.js";
11
12
  import { readBody } from "../server/h3-helpers.js";
12
13
  import {
13
14
  appStateGet,
15
+ appStateGetManyEntries,
14
16
  appStatePut,
15
17
  appStateDelete,
16
18
  appStateList,
@@ -45,6 +47,49 @@ export const getState = defineEventHandler(async (event: H3Event) => {
45
47
  return value ?? null;
46
48
  });
47
49
 
50
+ /** Upper bound on one batched read, so a crafted URL can't fan out unbounded. */
51
+ export const MAX_APP_STATE_BATCH_KEYS = 100;
52
+
53
+ /**
54
+ * `GET /_agent-native/application-state?keys=a,b,c` — read many keys for the
55
+ * caller's session in one round trip.
56
+ *
57
+ * `values` carries only the keys that have a stored row; every other requested
58
+ * key is listed in `missing`. A key whose stored value is `null` therefore
59
+ * appears in `values` with a `null` value and NOT in `missing`, which is what
60
+ * keeps "no row" distinguishable from "row holding an empty value".
61
+ */
62
+ export const getStateMany = defineEventHandler(async (event: H3Event) => {
63
+ const sessionId = await getSessionId(event);
64
+ const raw = getQuery(event).keys;
65
+ const requested = (Array.isArray(raw) ? raw : [raw])
66
+ .flatMap((entry) => String(entry ?? "").split(","))
67
+ .map((key) => safeKey(key.trim()))
68
+ .filter((key) => key.length > 0);
69
+ const keys = [...new Set(requested)];
70
+
71
+ if (keys.length === 0) {
72
+ throw createError({
73
+ statusCode: 400,
74
+ statusMessage: "Query parameter `keys` is required",
75
+ });
76
+ }
77
+ if (keys.length > MAX_APP_STATE_BATCH_KEYS) {
78
+ throw createError({
79
+ statusCode: 400,
80
+ statusMessage: `At most ${MAX_APP_STATE_BATCH_KEYS} keys may be read at once`,
81
+ });
82
+ }
83
+
84
+ const entries = await appStateGetManyEntries(sessionId, keys);
85
+ const values: Record<string, unknown> = {};
86
+ for (const entry of entries) values[entry.key] = entry.value;
87
+ return {
88
+ values,
89
+ missing: keys.filter((key) => !(key in values)),
90
+ };
91
+ });
92
+
48
93
  export const putState = defineEventHandler(async (event: H3Event) => {
49
94
  const sessionId = await getSessionId(event);
50
95
  const key = safeKey(String(getRouterParam(event, "key")));
@@ -2,6 +2,7 @@
2
2
  export {
3
3
  appStateGet,
4
4
  appStateGetMany,
5
+ appStateGetManyEntries,
5
6
  appStatePut,
6
7
  appStateDelete,
7
8
  appStateCompareAndSet,
@@ -22,6 +23,8 @@ export {
22
23
  // H3 route handlers (for templates)
23
24
  export {
24
25
  getState,
26
+ getStateMany,
27
+ MAX_APP_STATE_BATCH_KEYS,
25
28
  putState,
26
29
  deleteState,
27
30
  listComposeDrafts,
@@ -112,19 +112,18 @@ export async function appStateGet(
112
112
  }
113
113
 
114
114
  /**
115
- * Read several application-state keys for one session in a single SQL query.
116
- * Missing keys are returned as `null` so callers can preserve the requested
117
- * shape without issuing one fallback query per key.
115
+ * Read several application-state keys for one session in a single SQL query,
116
+ * returning ONLY the rows that exist. A key absent from the result has no row;
117
+ * a key present with a `null` value has a row that stores `null`. Callers that
118
+ * must not conflate the two (the batched HTTP read) depend on that difference,
119
+ * so this deliberately does not pad missing keys.
118
120
  */
119
- export async function appStateGetMany(
121
+ export async function appStateGetManyEntries(
120
122
  sessionId: string,
121
123
  keys: readonly string[],
122
- ): Promise<Record<string, Record<string, unknown> | null>> {
124
+ ): Promise<Array<{ key: string; value: Record<string, unknown> | null }>> {
123
125
  const uniqueKeys = [...new Set(keys)];
124
- const values: Record<string, Record<string, unknown> | null> = {};
125
- for (const key of uniqueKeys) values[key] = null;
126
- if (uniqueKeys.length === 0) return values;
127
-
126
+ if (uniqueKeys.length === 0) return [];
128
127
  await ensureTable();
129
128
  const client = getDbExec();
130
129
  const placeholders = uniqueKeys.map(() => "?").join(", ");
@@ -132,8 +131,26 @@ export async function appStateGetMany(
132
131
  sql: `SELECT key, value FROM application_state WHERE session_id = ? AND key IN (${placeholders})`,
133
132
  args: [sessionId, ...uniqueKeys],
134
133
  });
135
- for (const row of rows) {
136
- values[row.key as string] = JSON.parse(row.value as string);
134
+ return rows.map((row) => ({
135
+ key: row.key as string,
136
+ value: JSON.parse(row.value as string),
137
+ }));
138
+ }
139
+
140
+ /**
141
+ * Read several application-state keys for one session in a single SQL query.
142
+ * Missing keys are returned as `null` so callers can preserve the requested
143
+ * shape without issuing one fallback query per key.
144
+ */
145
+ export async function appStateGetMany(
146
+ sessionId: string,
147
+ keys: readonly string[],
148
+ ): Promise<Record<string, Record<string, unknown> | null>> {
149
+ const values: Record<string, Record<string, unknown> | null> = {};
150
+ for (const key of new Set(keys)) values[key] = null;
151
+
152
+ for (const entry of await appStateGetManyEntries(sessionId, keys)) {
153
+ values[entry.key] = entry.value;
137
154
  }
138
155
  return values;
139
156
  }