@agent-native/core 0.131.9 → 0.132.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 (292) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +99 -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/agent/durable-background.ts +38 -28
  9. package/corpus/core/src/agent/harness/background.ts +12 -2
  10. package/corpus/core/src/agent/production-agent.ts +18 -15
  11. package/corpus/core/src/agent/run-store.ts +45 -3
  12. package/corpus/core/src/application-state/handlers.ts +45 -0
  13. package/corpus/core/src/application-state/index.ts +3 -0
  14. package/corpus/core/src/application-state/store.ts +28 -11
  15. package/corpus/core/src/audit/redact.ts +104 -18
  16. package/corpus/core/src/client/AgentPanel.tsx +2 -7
  17. package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
  18. package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
  19. package/corpus/core/src/client/agent-chat.ts +4 -9
  20. package/corpus/core/src/client/application-state.ts +115 -12
  21. package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
  22. package/corpus/core/src/client/guided-questions.tsx +12 -23
  23. package/corpus/core/src/db/client.ts +148 -28
  24. package/corpus/core/src/db/create-get-db.ts +39 -6
  25. package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
  26. package/corpus/core/src/deploy/build.ts +26 -33
  27. package/corpus/core/src/feature-flags/store.ts +10 -5
  28. package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
  29. package/corpus/core/src/integrations/config-store.ts +13 -0
  30. package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
  31. package/corpus/core/src/mcp-client/routes.ts +33 -2
  32. package/corpus/core/src/onboarding/plugin.ts +31 -25
  33. package/corpus/core/src/org/accept-pending.ts +2 -0
  34. package/corpus/core/src/org/auto-join-domain.ts +2 -0
  35. package/corpus/core/src/org/context.ts +35 -10
  36. package/corpus/core/src/org/handlers.ts +6 -0
  37. package/corpus/core/src/org/request-org-cache.ts +71 -0
  38. package/corpus/core/src/resources/store.ts +10 -9
  39. package/corpus/core/src/scripts/runner.ts +6 -3
  40. package/corpus/core/src/secrets/storage.ts +97 -5
  41. package/corpus/core/src/server/action-change.ts +33 -0
  42. package/corpus/core/src/server/action-routes.ts +11 -9
  43. package/corpus/core/src/server/agent-teams.ts +10 -2
  44. package/corpus/core/src/server/better-auth-instance.ts +39 -12
  45. package/corpus/core/src/server/core-routes-plugin.ts +19 -2
  46. package/corpus/core/src/server/credential-provider.ts +57 -14
  47. package/corpus/core/src/server/http-response-telemetry.ts +1 -0
  48. package/corpus/core/src/server/poll.ts +95 -57
  49. package/corpus/core/src/server/voice-providers-status.ts +19 -4
  50. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
  51. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
  52. package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
  53. package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
  54. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
  55. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
  56. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
  57. package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
  58. package/corpus/templates/analytics/app/i18n-data.ts +40 -0
  59. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
  60. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
  61. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
  62. package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
  63. package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
  65. package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
  66. package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
  67. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
  68. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  69. package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
  70. package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
  71. package/corpus/templates/content/netlify.toml +3 -0
  72. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
  73. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
  74. package/corpus/templates/slides/actions/patch-deck.ts +25 -1
  75. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
  76. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
  77. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
  78. package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
  79. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
  80. package/corpus/templates/slides/app/pages/Index.tsx +2 -2
  81. package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
  82. package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
  83. package/corpus/toolkit/CHANGELOG.md +8 -0
  84. package/corpus/toolkit/package.json +1 -1
  85. package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
  86. package/dist/a2a/activity.d.ts +22 -4
  87. package/dist/a2a/activity.d.ts.map +1 -1
  88. package/dist/a2a/activity.js +97 -11
  89. package/dist/a2a/activity.js.map +1 -1
  90. package/dist/a2a/index.d.ts +1 -1
  91. package/dist/a2a/index.d.ts.map +1 -1
  92. package/dist/a2a/index.js +1 -1
  93. package/dist/a2a/index.js.map +1 -1
  94. package/dist/agent/durable-background.d.ts +13 -8
  95. package/dist/agent/durable-background.d.ts.map +1 -1
  96. package/dist/agent/durable-background.js +36 -28
  97. package/dist/agent/durable-background.js.map +1 -1
  98. package/dist/agent/harness/background.d.ts.map +1 -1
  99. package/dist/agent/harness/background.js +12 -2
  100. package/dist/agent/harness/background.js.map +1 -1
  101. package/dist/agent/production-agent.d.ts.map +1 -1
  102. package/dist/agent/production-agent.js +18 -15
  103. package/dist/agent/production-agent.js.map +1 -1
  104. package/dist/agent/run-store.d.ts +2 -0
  105. package/dist/agent/run-store.d.ts.map +1 -1
  106. package/dist/agent/run-store.js +44 -3
  107. package/dist/agent/run-store.js.map +1 -1
  108. package/dist/application-state/handlers.d.ts +15 -0
  109. package/dist/application-state/handlers.d.ts.map +1 -1
  110. package/dist/application-state/handlers.js +42 -2
  111. package/dist/application-state/handlers.js.map +1 -1
  112. package/dist/application-state/index.d.ts +2 -2
  113. package/dist/application-state/index.d.ts.map +1 -1
  114. package/dist/application-state/index.js +2 -2
  115. package/dist/application-state/index.js.map +1 -1
  116. package/dist/application-state/store.d.ts +11 -0
  117. package/dist/application-state/store.d.ts.map +1 -1
  118. package/dist/application-state/store.js +23 -10
  119. package/dist/application-state/store.js.map +1 -1
  120. package/dist/audit/redact.d.ts +25 -2
  121. package/dist/audit/redact.d.ts.map +1 -1
  122. package/dist/audit/redact.js +85 -21
  123. package/dist/audit/redact.js.map +1 -1
  124. package/dist/client/AgentPanel.d.ts.map +1 -1
  125. package/dist/client/AgentPanel.js +2 -7
  126. package/dist/client/AgentPanel.js.map +1 -1
  127. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  128. package/dist/client/MultiTabAssistantChat.js +1 -1
  129. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  130. package/dist/client/RunStuckBanner.d.ts +11 -1
  131. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  132. package/dist/client/RunStuckBanner.js +20 -7
  133. package/dist/client/RunStuckBanner.js.map +1 -1
  134. package/dist/client/agent-chat.d.ts.map +1 -1
  135. package/dist/client/agent-chat.js +4 -6
  136. package/dist/client/agent-chat.js.map +1 -1
  137. package/dist/client/application-state.d.ts +11 -0
  138. package/dist/client/application-state.d.ts.map +1 -1
  139. package/dist/client/application-state.js +87 -9
  140. package/dist/client/application-state.js.map +1 -1
  141. package/dist/client/dynamic-suggestions.d.ts.map +1 -1
  142. package/dist/client/dynamic-suggestions.js +4 -13
  143. package/dist/client/dynamic-suggestions.js.map +1 -1
  144. package/dist/client/guided-questions.d.ts.map +1 -1
  145. package/dist/client/guided-questions.js +6 -22
  146. package/dist/client/guided-questions.js.map +1 -1
  147. package/dist/collab/awareness.d.ts +2 -2
  148. package/dist/collab/awareness.d.ts.map +1 -1
  149. package/dist/db/client.d.ts +57 -10
  150. package/dist/db/client.d.ts.map +1 -1
  151. package/dist/db/client.js +122 -31
  152. package/dist/db/client.js.map +1 -1
  153. package/dist/db/create-get-db.d.ts.map +1 -1
  154. package/dist/db/create-get-db.js +31 -7
  155. package/dist/db/create-get-db.js.map +1 -1
  156. package/dist/db/runtime-diagnostics.d.ts +1 -0
  157. package/dist/db/runtime-diagnostics.d.ts.map +1 -1
  158. package/dist/db/runtime-diagnostics.js +31 -6
  159. package/dist/db/runtime-diagnostics.js.map +1 -1
  160. package/dist/deploy/build.d.ts +5 -20
  161. package/dist/deploy/build.d.ts.map +1 -1
  162. package/dist/deploy/build.js +18 -30
  163. package/dist/deploy/build.js.map +1 -1
  164. package/dist/feature-flags/store.d.ts.map +1 -1
  165. package/dist/feature-flags/store.js +10 -5
  166. package/dist/feature-flags/store.js.map +1 -1
  167. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  168. package/dist/integrations/a2a-continuations-store.js +12 -0
  169. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  170. package/dist/integrations/config-store.d.ts +1 -0
  171. package/dist/integrations/config-store.d.ts.map +1 -1
  172. package/dist/integrations/config-store.js +11 -0
  173. package/dist/integrations/config-store.js.map +1 -1
  174. package/dist/integrations/google-docs-poller.d.ts.map +1 -1
  175. package/dist/integrations/google-docs-poller.js +19 -2
  176. package/dist/integrations/google-docs-poller.js.map +1 -1
  177. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  178. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  179. package/dist/mcp-client/routes.d.ts.map +1 -1
  180. package/dist/mcp-client/routes.js +30 -2
  181. package/dist/mcp-client/routes.js.map +1 -1
  182. package/dist/notifications/routes.d.ts +6 -6
  183. package/dist/observability/routes.d.ts +3 -3
  184. package/dist/onboarding/plugin.d.ts.map +1 -1
  185. package/dist/onboarding/plugin.js +12 -8
  186. package/dist/onboarding/plugin.js.map +1 -1
  187. package/dist/org/accept-pending.d.ts.map +1 -1
  188. package/dist/org/accept-pending.js +2 -0
  189. package/dist/org/accept-pending.js.map +1 -1
  190. package/dist/org/auto-join-domain.d.ts.map +1 -1
  191. package/dist/org/auto-join-domain.js +2 -0
  192. package/dist/org/auto-join-domain.js.map +1 -1
  193. package/dist/org/context.d.ts.map +1 -1
  194. package/dist/org/context.js +29 -9
  195. package/dist/org/context.js.map +1 -1
  196. package/dist/org/handlers.d.ts.map +1 -1
  197. package/dist/org/handlers.js +5 -0
  198. package/dist/org/handlers.js.map +1 -1
  199. package/dist/org/request-org-cache.d.ts +16 -0
  200. package/dist/org/request-org-cache.d.ts.map +1 -0
  201. package/dist/org/request-org-cache.js +62 -0
  202. package/dist/org/request-org-cache.js.map +1 -0
  203. package/dist/progress/routes.d.ts +1 -1
  204. package/dist/resources/handlers.d.ts +1 -1
  205. package/dist/resources/store.d.ts.map +1 -1
  206. package/dist/resources/store.js +11 -9
  207. package/dist/resources/store.js.map +1 -1
  208. package/dist/scripts/runner.d.ts.map +1 -1
  209. package/dist/scripts/runner.js +3 -3
  210. package/dist/scripts/runner.js.map +1 -1
  211. package/dist/secrets/storage.d.ts.map +1 -1
  212. package/dist/secrets/storage.js +82 -5
  213. package/dist/secrets/storage.js.map +1 -1
  214. package/dist/server/action-change.d.ts +20 -0
  215. package/dist/server/action-change.d.ts.map +1 -1
  216. package/dist/server/action-change.js +29 -0
  217. package/dist/server/action-change.js.map +1 -1
  218. package/dist/server/action-routes.d.ts.map +1 -1
  219. package/dist/server/action-routes.js +7 -8
  220. package/dist/server/action-routes.js.map +1 -1
  221. package/dist/server/agent-teams.d.ts.map +1 -1
  222. package/dist/server/agent-teams.js +10 -2
  223. package/dist/server/agent-teams.js.map +1 -1
  224. package/dist/server/better-auth-instance.d.ts.map +1 -1
  225. package/dist/server/better-auth-instance.js +31 -14
  226. package/dist/server/better-auth-instance.js.map +1 -1
  227. package/dist/server/core-routes-plugin.d.ts +1 -0
  228. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  229. package/dist/server/core-routes-plugin.js +18 -4
  230. package/dist/server/core-routes-plugin.js.map +1 -1
  231. package/dist/server/credential-provider.d.ts +16 -0
  232. package/dist/server/credential-provider.d.ts.map +1 -1
  233. package/dist/server/credential-provider.js +49 -13
  234. package/dist/server/credential-provider.js.map +1 -1
  235. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  236. package/dist/server/http-response-telemetry.js +1 -0
  237. package/dist/server/http-response-telemetry.js.map +1 -1
  238. package/dist/server/poll.d.ts +6 -0
  239. package/dist/server/poll.d.ts.map +1 -1
  240. package/dist/server/poll.js +84 -47
  241. package/dist/server/poll.js.map +1 -1
  242. package/dist/server/transcribe-voice.d.ts +1 -1
  243. package/dist/server/voice-providers-status.d.ts.map +1 -1
  244. package/dist/server/voice-providers-status.js +12 -3
  245. package/dist/server/voice-providers-status.js.map +1 -1
  246. package/docs/content/durable-background-runs.mdx +15 -6
  247. package/docs/design/durable-agent-runs.md +15 -11
  248. package/package.json +2 -2
  249. package/src/a2a/activity.ts +127 -10
  250. package/src/a2a/index.ts +3 -0
  251. package/src/agent/durable-background.ts +38 -28
  252. package/src/agent/harness/background.ts +12 -2
  253. package/src/agent/production-agent.ts +18 -15
  254. package/src/agent/run-store.ts +45 -3
  255. package/src/application-state/handlers.ts +45 -0
  256. package/src/application-state/index.ts +3 -0
  257. package/src/application-state/store.ts +28 -11
  258. package/src/audit/redact.ts +104 -18
  259. package/src/client/AgentPanel.tsx +2 -7
  260. package/src/client/MultiTabAssistantChat.tsx +3 -0
  261. package/src/client/RunStuckBanner.tsx +29 -6
  262. package/src/client/agent-chat.ts +4 -9
  263. package/src/client/application-state.ts +115 -12
  264. package/src/client/dynamic-suggestions.ts +4 -12
  265. package/src/client/guided-questions.tsx +12 -23
  266. package/src/db/client.ts +148 -28
  267. package/src/db/create-get-db.ts +39 -6
  268. package/src/db/runtime-diagnostics.ts +41 -6
  269. package/src/deploy/build.ts +26 -33
  270. package/src/feature-flags/store.ts +10 -5
  271. package/src/integrations/a2a-continuations-store.ts +11 -0
  272. package/src/integrations/config-store.ts +13 -0
  273. package/src/integrations/google-docs-poller.ts +26 -2
  274. package/src/mcp-client/routes.ts +33 -2
  275. package/src/onboarding/plugin.ts +31 -25
  276. package/src/org/accept-pending.ts +2 -0
  277. package/src/org/auto-join-domain.ts +2 -0
  278. package/src/org/context.ts +35 -10
  279. package/src/org/handlers.ts +6 -0
  280. package/src/org/request-org-cache.ts +71 -0
  281. package/src/resources/store.ts +10 -9
  282. package/src/scripts/runner.ts +6 -3
  283. package/src/secrets/storage.ts +97 -5
  284. package/src/server/action-change.ts +33 -0
  285. package/src/server/action-routes.ts +11 -9
  286. package/src/server/agent-teams.ts +10 -2
  287. package/src/server/better-auth-instance.ts +39 -12
  288. package/src/server/core-routes-plugin.ts +19 -2
  289. package/src/server/credential-provider.ts +57 -14
  290. package/src/server/http-response-telemetry.ts +1 -0
  291. package/src/server/poll.ts +95 -57
  292. package/src/server/voice-providers-status.ts +19 -4
@@ -17,7 +17,10 @@ import { pathToFileURL } from "url";
17
17
 
18
18
  import type { ActionEntry } from "../agent/production-agent.js";
19
19
  import { closeDbExec } from "../db/client.js";
20
- import { notifyActionChange } from "../server/action-change.js";
20
+ import {
21
+ actionCallIsReadOnly,
22
+ notifyActionChange,
23
+ } from "../server/action-change.js";
21
24
  import {
22
25
  runWithRequestContext,
23
26
  getRequestOrgId,
@@ -442,7 +445,7 @@ async function dispatchAction(
442
445
  ) {
443
446
  const parsed = parseActionArgs(args, { coerceBooleans: true });
444
447
  const result = await handler.run(parsed, cliActionCtx(actionName));
445
- if (handler.readOnly !== true) {
448
+ if (!actionCallIsReadOnly(handler, parsed, false)) {
446
449
  await notifyActionChange({ actionName }).catch(() => {});
447
450
  }
448
451
  if (result) assertCliHandoffLaunched(printActionResult(result));
@@ -476,7 +479,7 @@ async function dispatchAction(
476
479
  parsed as Record<string, string>,
477
480
  cliActionCtx(actionName),
478
481
  );
479
- if (packageAction.readOnly !== true) {
482
+ if (!actionCallIsReadOnly(packageAction, parsed, false)) {
480
483
  await notifyActionChange({ actionName }).catch(() => {});
481
484
  }
482
485
  if (result) assertCliHandoffLaunched(printActionResult(result));
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
16
16
 
17
17
  import { getDbExec, isPostgres } from "../db/client.js";
18
18
  import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
19
+ import { getRequestContext } from "../server/request-context.js";
19
20
  import {
20
21
  encryptSecretValue as encryptLegacyValue,
21
22
  encryptSharedSecretValue as encryptValue,
@@ -217,6 +218,8 @@ export async function writeAppSecret(args: WriteSecretArgs): Promise<string> {
217
218
  now,
218
219
  ];
219
220
 
221
+ invalidateRequestSecret(args);
222
+
220
223
  if (isPostgres()) {
221
224
  const { rows } = await client.execute({
222
225
  sql: `${upsertSql} RETURNING id`,
@@ -339,6 +342,52 @@ async function populateSharedAppSecret(
339
342
  }
340
343
  }
341
344
 
345
+ // ---------------------------------------------------------------------------
346
+ // Per-request read memo
347
+ // ---------------------------------------------------------------------------
348
+
349
+ /**
350
+ * Per-request memo of secret reads, keyed on the active AsyncLocalStorage
351
+ * RequestContext (WeakMap → freed with the request). Mirrors the settings
352
+ * store's cache, including its staleness rule: a write in THIS request is
353
+ * written through, while other in-flight requests keep their snapshot for
354
+ * their own (short) lifetime.
355
+ *
356
+ * The map key is `scope|scopeId|key`, which is the secret's full identity —
357
+ * `user:<email>`, `org:<id>`, `solo:<email>`, `workspace` — so a hit can never
358
+ * answer one caller with another caller's secret. Do NOT reduce the key to
359
+ * just `key`.
360
+ *
361
+ * This exists because one credential resolution is a WATERFALL: user scope,
362
+ * then org, then workspace, then solo. Callers re-resolve the same credential
363
+ * several times per request (engine detection, config resolution, usability
364
+ * checks), and against a remote database each probe is a network round trip.
365
+ */
366
+ const _requestSecretsCache = new WeakMap<
367
+ object,
368
+ Map<string, ReadSecretResult | null>
369
+ >();
370
+
371
+ function requestSecretsCache(): Map<string, ReadSecretResult | null> | null {
372
+ const ctx = getRequestContext();
373
+ if (!ctx || typeof ctx !== "object") return null;
374
+ let cache = _requestSecretsCache.get(ctx);
375
+ if (!cache) {
376
+ cache = new Map();
377
+ _requestSecretsCache.set(ctx, cache);
378
+ }
379
+ return cache;
380
+ }
381
+
382
+ function secretCacheKey(ref: SecretRef): string {
383
+ return `${ref.scope}|${ref.scopeId}|${ref.key}`;
384
+ }
385
+
386
+ /** Drop this request's memo for a secret whose stored value just changed. */
387
+ function invalidateRequestSecret(ref: SecretRef): void {
388
+ requestSecretsCache()?.delete(secretCacheKey(ref));
389
+ }
390
+
342
391
  type AppSecretsReadQuery = { sql: string; args: unknown[] };
343
392
 
344
393
  /**
@@ -379,6 +428,17 @@ async function executeAppSecretsRead(query: AppSecretsReadQuery) {
379
428
  */
380
429
  export async function readAppSecret(
381
430
  ref: SecretRef,
431
+ ): Promise<ReadSecretResult | null> {
432
+ const cache = requestSecretsCache();
433
+ const cacheKey = secretCacheKey(ref);
434
+ if (cache?.has(cacheKey)) return cache.get(cacheKey) ?? null;
435
+ const result = await readAppSecretUncached(ref);
436
+ cache?.set(cacheKey, result);
437
+ return result;
438
+ }
439
+
440
+ async function readAppSecretUncached(
441
+ ref: SecretRef,
382
442
  ): Promise<ReadSecretResult | null> {
383
443
  const { key, scope, scopeId } = ref;
384
444
  const { rows } = await executeAppSecretsRead({
@@ -417,15 +477,41 @@ export async function readAppSecrets(args: {
417
477
  scope: SecretScope;
418
478
  scopeId: string;
419
479
  }): Promise<Map<string, ReadSecretResult>> {
420
- const keys = [...new Set(args.keys.filter(Boolean))];
421
- if (keys.length === 0) return new Map();
480
+ const requested = [...new Set(args.keys.filter(Boolean))];
481
+ if (requested.length === 0) return new Map();
482
+
483
+ const cache = requestSecretsCache();
484
+ const results = new Map<string, ReadSecretResult>();
485
+ const keys: string[] = [];
486
+ for (const key of requested) {
487
+ const cacheKey = secretCacheKey({
488
+ key,
489
+ scope: args.scope,
490
+ scopeId: args.scopeId,
491
+ });
492
+ if (cache?.has(cacheKey)) {
493
+ const cached = cache.get(cacheKey);
494
+ if (cached) results.set(key, cached);
495
+ continue;
496
+ }
497
+ keys.push(key);
498
+ }
499
+ if (keys.length === 0) return results;
422
500
 
423
501
  const placeholders = keys.map(() => "?").join(", ");
424
502
  const { rows } = await executeAppSecretsRead({
425
503
  sql: `SELECT key, encrypted_value, shared_encrypted_value, updated_at, id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key IN (${placeholders})`,
426
504
  args: [args.scope, args.scopeId, ...keys],
427
505
  });
428
- const results = new Map<string, ReadSecretResult>();
506
+ // The statement covered every uncached key in this scope, so a key missing
507
+ // from `rows` is genuinely absent — memo it as such rather than leaving a
508
+ // single-key read to go ask again.
509
+ for (const key of keys) {
510
+ cache?.set(
511
+ secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }),
512
+ null,
513
+ );
514
+ }
429
515
  for (const row of rows) {
430
516
  const key = String(row.key ?? "");
431
517
  if (!key) continue;
@@ -442,11 +528,16 @@ export async function readAppSecrets(args: {
442
528
  decrypted.sharedCiphertextToReplace ?? null,
443
529
  );
444
530
  }
445
- results.set(key, {
531
+ const result = {
446
532
  value: decrypted.value,
447
533
  last4: last4(decrypted.value),
448
534
  updatedAt: Number(row.updated_at ?? 0),
449
- });
535
+ };
536
+ results.set(key, result);
537
+ cache?.set(
538
+ secretCacheKey({ key, scope: args.scope, scopeId: args.scopeId }),
539
+ result,
540
+ );
450
541
  } catch {
451
542
  // Match readAppSecret: corrupted or stale ciphertext behaves as missing.
452
543
  }
@@ -589,6 +680,7 @@ function parseAllowlist(raw: string | null): string[] | null {
589
680
 
590
681
  export async function deleteAppSecret(ref: SecretRef): Promise<boolean> {
591
682
  await ensureTable();
683
+ invalidateRequestSecret(ref);
592
684
  const { key, scope, scopeId } = ref;
593
685
  const client = getDbExec();
594
686
  const { rowsAffected } = await client.execute({
@@ -4,10 +4,43 @@ import {
4
4
  actionChangeMarkerValue,
5
5
  type ActionChangeTarget,
6
6
  } from "../action-change-marker.js";
7
+ import type { ActionPlanModeConfig } from "../action.js";
7
8
  import { appStatePut } from "../application-state/store.js";
8
9
  import { recordChange } from "./poll.js";
9
10
  import { getRequestOrgId, getRequestUserEmail } from "./request-context.js";
10
11
 
12
+ /**
13
+ * Whether THIS invocation should announce a change.
14
+ *
15
+ * `readOnly` describes an action name, but a tool that discriminates on an
16
+ * argument reads on some calls and writes on others — `manage-agent-engine`
17
+ * with `action: "list"` is a status poll, with `action: "set"` a write. Such
18
+ * actions already publish the per-call answer as a Plan-mode `effect`
19
+ * predicate, so read it here instead of treating every call as a write: a poll
20
+ * that bumps the `"action"` change version makes every query keyed on it
21
+ * refetch forever.
22
+ *
23
+ * `readOnly` stays authoritative otherwise. Widening the flag to cover mixed
24
+ * tools would also open them to read-only A2A peers and to the agent's
25
+ * read-result cache, where it would be false.
26
+ */
27
+ export function actionCallIsReadOnly(
28
+ entry: { readOnly?: boolean; planMode?: ActionPlanModeConfig<any> },
29
+ params: unknown,
30
+ fallback: boolean,
31
+ ): boolean {
32
+ const effect = entry.planMode?.effect;
33
+ if (typeof effect === "function") {
34
+ try {
35
+ return effect(params) === "read";
36
+ } catch {
37
+ // A predicate that throws says nothing; fall through to the flag.
38
+ }
39
+ }
40
+ if (typeof entry.readOnly === "boolean") return entry.readOnly;
41
+ return fallback;
42
+ }
43
+
11
44
  export interface NotifyActionChangeOptions {
12
45
  actionName: string;
13
46
  owner?: string;
@@ -21,7 +21,7 @@ import {
21
21
  MCP_EMBED_CORS_ALLOW_HEADERS,
22
22
  shouldAllowMcpEmbedCredentials,
23
23
  } from "../shared/mcp-embed-headers.js";
24
- import { notifyActionChange } from "./action-change.js";
24
+ import { actionCallIsReadOnly, notifyActionChange } from "./action-change.js";
25
25
  import {
26
26
  seedAgentRunOwnerContext,
27
27
  type AgentRunOwnerContext,
@@ -603,14 +603,16 @@ export function mountActionRoutes(
603
603
  // their action queries. The calling tab already refetches via
604
604
  // useActionMutation's onSuccess, so this is mainly cross-tab
605
605
  // sync (and parity with the agent's tool-call path).
606
- // Explicit entry.readOnly (true OR false) wins over the method
607
- // heuristic. defineAction already auto-infers GET readOnly=true,
608
- // so for actions registered through that path entry.readOnly is
609
- // always set and the fallback just guards legacy wrap paths.
610
- const isReadOnly =
611
- typeof entry.readOnly === "boolean"
612
- ? entry.readOnly
613
- : method === "GET";
606
+ // A per-call Plan-mode effect wins over entry.readOnly, which
607
+ // wins (true OR false) over the method heuristic. defineAction
608
+ // already auto-infers GET readOnly=true, so for actions
609
+ // registered through that path entry.readOnly is always set and
610
+ // the fallback just guards legacy wrap paths.
611
+ const isReadOnly = actionCallIsReadOnly(
612
+ entry,
613
+ params,
614
+ method === "GET",
615
+ );
614
616
  if (!isReadOnly) {
615
617
  try {
616
618
  await notifyActionChange({
@@ -57,6 +57,7 @@ import {
57
57
  listAppState,
58
58
  deleteAppState,
59
59
  } from "../application-state/script-helpers.js";
60
+ import { redactArgsToValue, redactTextToSummary } from "../audit/redact.js";
60
61
  import { createThread } from "../chat-threads/store.js";
61
62
  import type {
62
63
  BackgroundAgentRun,
@@ -1112,13 +1113,20 @@ function summarizeAgentChatEvent(event: RunEvent): {
1112
1113
  return {
1113
1114
  kind: "status",
1114
1115
  message: `Running ${payload.tool}`,
1115
- metadata: { tool: payload.tool, input: payload.input },
1116
+ metadata: {
1117
+ tool: payload.tool,
1118
+ input: redactArgsToValue(payload.input),
1119
+ },
1116
1120
  };
1117
1121
  case "tool_done":
1118
1122
  return {
1119
1123
  kind: "artifact",
1120
1124
  message: payload.result,
1121
- metadata: { tool: payload.tool },
1125
+ metadata: {
1126
+ tool: payload.tool,
1127
+ input: redactArgsToValue(payload.input),
1128
+ result: redactTextToSummary(payload.result),
1129
+ },
1122
1130
  };
1123
1131
  case "agent_task":
1124
1132
  return {
@@ -38,6 +38,9 @@ import {
38
38
  pgPoolOptions,
39
39
  neonPoolMax,
40
40
  attachNeonPoolErrorLogger,
41
+ sharedDbPool,
42
+ onSharedDbPoolsClosed,
43
+ onSharedDbPoolReplaced,
41
44
  } from "../db/client.js";
42
45
  import { ensureTableExists } from "../db/ddl-guard.js";
43
46
  import { saveOAuthTokens } from "../oauth-tokens/store.js";
@@ -808,15 +811,34 @@ export async function getBetterAuthInternalAdapter(
808
811
  export async function resetBetterAuth(): Promise<void> {
809
812
  _auth = undefined;
810
813
  _initPromise = undefined;
811
- if (_neonAuthPool) {
812
- try {
813
- await _neonAuthPool.end();
814
- } catch {
815
- // Pool may have already closed (process exiting, etc.) — don't block reset.
816
- }
814
+ // The Postgres pool belongs to the process (see `sharedDbPool`), not to Better
815
+ // Auth — ending it here would take the framework's and every store's database
816
+ // access down with it. `closeDbExec()` owns that.
817
+ _neonAuthPool = undefined;
818
+ await closePgliteClients();
819
+ }
820
+
821
+ // A `closeDbExec()` releases the pool this instance's adapter is bound to, so
822
+ // the next `getAuth()` must build a fresh one. Registered from the pooled
823
+ // branches rather than at module load: core's specs widely mock
824
+ // `db/client.js`, and an import-time call into the mock breaks every one of
825
+ // them that doesn't stub this export.
826
+ let _poolCloseHookRegistered = false;
827
+ function resetAuthOnPoolClose(driver?: string, url?: string): void {
828
+ if (_poolCloseHookRegistered) return;
829
+ _poolCloseHookRegistered = true;
830
+ onSharedDbPoolsClosed(() => {
831
+ _auth = undefined;
832
+ _initPromise = undefined;
817
833
  _neonAuthPool = undefined;
834
+ });
835
+ if (driver && url) {
836
+ onSharedDbPoolReplaced(driver, url, () => {
837
+ _auth = undefined;
838
+ _initPromise = undefined;
839
+ _neonAuthPool = undefined;
840
+ });
818
841
  }
819
- await closePgliteClients();
820
842
  }
821
843
 
822
844
  // ---------------------------------------------------------------------------
@@ -1160,10 +1182,12 @@ async function buildDatabaseConfig(
1160
1182
  // session lookup on essentially every authenticated request, so an
1161
1183
  // un-capped pool here is a primary contributor to "Max client
1162
1184
  // connections reached" across concurrent serverless instances.
1163
- _neonAuthPool = new Pool({
1164
- connectionString: url,
1165
- max: neonPoolMax(),
1166
- });
1185
+ resetAuthOnPoolClose("neon", url);
1186
+ _neonAuthPool = sharedDbPool(
1187
+ "neon",
1188
+ url,
1189
+ () => new Pool({ connectionString: url, max: neonPoolMax() }),
1190
+ );
1167
1191
  attachNeonPoolErrorLogger(_neonAuthPool, "db/neon-auth");
1168
1192
  const { drizzle } = await import("drizzle-orm/neon-serverless");
1169
1193
  const db = drizzle(buildResilientNeonPool(_neonAuthPool), {
@@ -1182,7 +1206,10 @@ async function buildDatabaseConfig(
1182
1206
  // un-capped pool here is a primary contributor to "Max client connections
1183
1207
  // reached" across concurrent serverless instances.
1184
1208
  const { default: postgres } = await import("postgres");
1185
- const sql = postgres(url, pgPoolOptions(url));
1209
+ resetAuthOnPoolClose("postgres-js", url);
1210
+ const sql = sharedDbPool("postgres-js", url, () =>
1211
+ postgres(url, pgPoolOptions(url)),
1212
+ );
1186
1213
  const { drizzle } = await import("drizzle-orm/postgres-js");
1187
1214
  const db = drizzle(buildResilientPostgresJsClient(sql), {
1188
1215
  schema: pgAuthSchema,
@@ -46,6 +46,7 @@ import {
46
46
  putComposeDraft,
47
47
  deleteComposeDraft,
48
48
  deleteAllComposeDrafts,
49
+ getStateMany,
49
50
  } from "../application-state/handlers.js";
50
51
  import { mountBrowserSessionRoutes } from "../browser-sessions/routes.js";
51
52
  import { mountDbAdminRoutes } from "../db-admin/routes.js";
@@ -152,6 +153,7 @@ import {
152
153
  import type { EnvKeyConfig } from "./create-server.js";
153
154
  import {
154
155
  canUseDeployCredentialFallbackForRequest,
156
+ prefetchSecrets,
155
157
  readDeployCredentialEnv,
156
158
  resolveSecret,
157
159
  } from "./credential-provider.js";
@@ -1286,6 +1288,7 @@ export async function readLegacyCoreRouteInitSettings(
1286
1288
  * GET /_agent-native/health — DB liveness probe + scale-to-zero warmup
1287
1289
  * GET /_agent-native/env-status — env key configuration status (when envKeys provided)
1288
1290
  * POST /_agent-native/env-vars — compatibility route that saves keys to scoped DB secrets
1291
+ * GET /_agent-native/application-state?keys=a,b,c — batched read of many keys
1289
1292
  * GET /_agent-native/application-state/:key — read application state
1290
1293
  * PUT /_agent-native/application-state/:key — write application state
1291
1294
  * DELETE /_agent-native/application-state/:key — delete application state
@@ -3322,10 +3325,17 @@ export function createCoreRoutesPlugin(
3322
3325
  /* org module not present in this template */
3323
3326
  }
3324
3327
  }
3328
+ // One context for the whole sweep so the per-request secret memo is
3329
+ // shared, and one batched read per scope to fill it. Without this
3330
+ // every key pays its own four-scope waterfall.
3331
+ const requestContext = { userEmail, orgId };
3332
+ await runWithRequestContext(requestContext, () =>
3333
+ prefetchSecrets(allowedEnvKeyNames),
3334
+ );
3325
3335
  return Promise.all(
3326
3336
  envKeys.map(async (cfg) => {
3327
3337
  const configured = await runWithRequestContext(
3328
- { userEmail, orgId },
3338
+ requestContext,
3329
3339
  () => resolveSecret(cfg.key).then(Boolean),
3330
3340
  );
3331
3341
  return {
@@ -4245,7 +4255,14 @@ export function createCoreRoutesPlugin(
4245
4255
  (event.url?.pathname || "").replace(/^\/+/, "").split("/")[0] ||
4246
4256
  "";
4247
4257
  // Skip — compose handler above already handled it
4248
- if (key === "compose" || key === "") return;
4258
+ if (key === "compose") return;
4259
+ // Collection root: `GET ?keys=a,b,c` batches many single-key reads
4260
+ // into one request (and one identity resolution) — the chat rail
4261
+ // alone reads ~6 keys on every mount.
4262
+ if (key === "") {
4263
+ if (getMethod(event) === "GET") return getStateMany(event);
4264
+ return;
4265
+ }
4249
4266
  if (event.context) {
4250
4267
  event.context.params = { ...event.context.params, key };
4251
4268
  }
@@ -1276,11 +1276,50 @@ export async function deleteBuilderCredentials(
1276
1276
  // ANTHROPIC_API_KEY, but Builder identity keys keep the stricter scoped policy.
1277
1277
  // ---------------------------------------------------------------------------
1278
1278
 
1279
+ /**
1280
+ * Warm this request's secret memo for many keys with one read per scope.
1281
+ *
1282
+ * `resolveSecret` walks four scopes per key, so a status endpoint asking about
1283
+ * a dozen keys costs ~50 round trips against a remote database. Reading each
1284
+ * scope once for the whole key set collapses that to four, and the subsequent
1285
+ * `resolveSecret` calls answer from the per-request memo — same precedence,
1286
+ * same identity scoping, no new cache to invalidate.
1287
+ *
1288
+ * Best-effort on purpose: `readAppSecrets` only memoizes keys a statement
1289
+ * actually covered, so a failure here leaves each key's own lookup to run and
1290
+ * report the failure. It must never turn an unreadable store into "not set".
1291
+ */
1292
+ export async function prefetchSecrets(keys: readonly string[]): Promise<void> {
1293
+ const email = getRequestUserEmail();
1294
+ if (!email || keys.length === 0) return;
1295
+ const { readAppSecrets } = await import("../secrets/storage.js");
1296
+ const orgId =
1297
+ getRequestOrgId() || (await resolveOrgIdForRequestEmail(email)).orgId;
1298
+ const scopes: Array<{
1299
+ scope: "user" | "org" | "workspace";
1300
+ scopeId: string;
1301
+ }> = [
1302
+ { scope: "user", scopeId: email },
1303
+ ...(orgId
1304
+ ? ([
1305
+ { scope: "org", scopeId: orgId },
1306
+ { scope: "workspace", scopeId: orgId },
1307
+ ] as const)
1308
+ : []),
1309
+ { scope: "workspace", scopeId: `solo:${email}` },
1310
+ ];
1311
+ await Promise.all(
1312
+ scopes.map((s) => readAppSecrets({ keys, ...s }).catch(() => undefined)),
1313
+ );
1314
+ }
1315
+
1279
1316
  /**
1280
1317
  * Resolve a request-scoped secret. Reads from `app_secrets` first (current
1281
1318
  * user override, active org, workspace row for that org, then the solo
1282
1319
  * workspace row); falls back to `process.env` only when the deploy fallback
1283
1320
  * policy allows it.
1321
+ *
1322
+ * Resolving several keys in one request? Call `prefetchSecrets` first.
1284
1323
  */
1285
1324
  export async function resolveSecret(key: string): Promise<string | null> {
1286
1325
  const resolved = await resolveSecretDetailed(key);
@@ -1307,6 +1346,7 @@ export async function resolveSecretDetailed(
1307
1346
  if (email) {
1308
1347
  try {
1309
1348
  const { readAppSecret } = await import("../secrets/storage.js");
1349
+
1310
1350
  // Per-user override first.
1311
1351
  const userSecret = await readAppSecret({
1312
1352
  key,
@@ -1333,14 +1373,25 @@ export async function resolveSecretDetailed(
1333
1373
  orgId = resolved.orgId;
1334
1374
  }
1335
1375
 
1376
+ if (lookupFailed) {
1377
+ return { value: null, lookupFailed: true, cause };
1378
+ }
1379
+
1336
1380
  if (orgId) {
1381
+ // These rows are independent once the org is known, so read them in
1382
+ // parallel while still applying precedence below in the same order.
1383
+ const [orgRead, workspaceRead] = await Promise.allSettled([
1384
+ readAppSecret({ key, scope: "org", scopeId: orgId }),
1385
+ readAppSecret({ key, scope: "workspace", scopeId: orgId }),
1386
+ ]);
1387
+ const unwrap = <T>(settled: PromiseSettledResult<T>): T => {
1388
+ if (settled.status === "rejected") throw settled.reason;
1389
+ return settled.value;
1390
+ };
1391
+
1337
1392
  // Fall back to the active org's shared row, when present. Builder
1338
1393
  // Connect uses this first-class org scope.
1339
- const orgSecret = await readAppSecret({
1340
- key,
1341
- scope: "org",
1342
- scopeId: orgId,
1343
- });
1394
+ const orgSecret = unwrap(orgRead);
1344
1395
  if (orgSecret?.value) {
1345
1396
  if (traceLookup) {
1346
1397
  console.log(
@@ -1353,11 +1404,7 @@ export async function resolveSecretDetailed(
1353
1404
  // Registered secrets historically used "workspace" scope for
1354
1405
  // org-shared configuration. Keep reading it so Settings status and
1355
1406
  // runtime resolution agree.
1356
- const workspaceSecret = await readAppSecret({
1357
- key,
1358
- scope: "workspace",
1359
- scopeId: orgId,
1360
- });
1407
+ const workspaceSecret = unwrap(workspaceRead);
1361
1408
  if (workspaceSecret?.value) {
1362
1409
  if (traceLookup) {
1363
1410
  console.log(
@@ -1368,10 +1415,6 @@ export async function resolveSecretDetailed(
1368
1415
  }
1369
1416
  }
1370
1417
 
1371
- if (lookupFailed) {
1372
- return { value: null, lookupFailed: true, cause };
1373
- }
1374
-
1375
1418
  // Solo-workspace fallback: always checked, even when an org id was found
1376
1419
  // above. A secret written before the user joined/created an org lives
1377
1420
  // here, and must not become unreachable once that org exists. It stays
@@ -367,6 +367,7 @@ export function installHttpResponseTelemetryHooks(nitroApp: any): void {
367
367
  );
368
368
  }
369
369
  if (state.db.operationCount > 0) {
370
+ appendServerTiming(response, event, "db-ops", state.db.operationCount);
370
371
  appendServerTiming(response, event, "db", state.db.operationWallMs);
371
372
  appendServerTiming(
372
373
  response,