@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
@@ -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
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  const SENSITIVE_KEY =
15
- /(pass(word|phrase)?|secret|token|api[_-]?key|apikey|authorization|bearer|credential|cookie|session[_-]?(id|token)|private[_-]?key|client[_-]?secret|signing[_-]?secret|access[_-]?key|refresh[_-]?token|webhook[_-]?(url|secret))/i;
15
+ /(?:pass(?:word|phrase)?|secret|token|api[_-]?key|apikey|authorization|bearer|credential|cookie|session[_-]?(?:id|token)|private[_-]?key|client[_-]?secret|signing[_-]?secret|access[_-]?key|refresh[_-]?token|webhook[_-]?(?:url|secret))/i;
16
16
 
17
17
  const REDACTED = "[redacted]";
18
18
  const MAX_STRING = 2000;
@@ -20,6 +20,11 @@ const MAX_DEPTH = 6;
20
20
  const MAX_KEYS = 100;
21
21
  const MAX_ARRAY = 100;
22
22
  const MAX_JSON = 8000;
23
+ /**
24
+ * Head room reserved for the `…(N more chars)` marker so a truncated summary
25
+ * still fits its caller's cap. Callers below this cannot carry a marker at all.
26
+ */
27
+ const MARKER_SLACK = 32;
23
28
 
24
29
  /** Heuristic: does a bare string value look like a secret? */
25
30
  function looksSecret(value: string): boolean {
@@ -42,21 +47,32 @@ function looksSecret(value: string): boolean {
42
47
  return false;
43
48
  }
44
49
 
45
- function redactString(value: string): string {
50
+ function redactString(value: string, maxString: number): string {
46
51
  if (looksSecret(value)) return REDACTED;
47
- if (value.length > MAX_STRING) {
48
- return `${value.slice(0, MAX_STRING)}…(${value.length - MAX_STRING} more chars)`;
52
+ if (value.length > maxString) {
53
+ return `${value.slice(0, maxString)}…(${value.length - maxString} more chars)`;
49
54
  }
50
55
  return value;
51
56
  }
52
57
 
53
- function redact(value: unknown, depth: number): unknown {
58
+ function redactEmbeddedSecrets(value: string): string {
59
+ const bearer = value.replace(/\b(bearer\s+)([^\s"',}]+)/gi, `$1${REDACTED}`);
60
+ const credentialField = new RegExp(
61
+ `(["']?\\b${SENSITIVE_KEY.source}\\b["']?\\s*[:=]\\s*)(["']?)(?!bearer\\b)([^"'\\s,}]+)\\2`,
62
+ "gi",
63
+ );
64
+ return bearer.replace(credentialField, `$1$2${REDACTED}$2`);
65
+ }
66
+
67
+ function redact(value: unknown, depth: number, maxString: number): unknown {
54
68
  if (value == null) return value;
55
- if (typeof value === "string") return redactString(value);
69
+ if (typeof value === "string") return redactString(value, maxString);
56
70
  if (typeof value === "number" || typeof value === "boolean") return value;
57
71
  if (depth >= MAX_DEPTH) return "[…]";
58
72
  if (Array.isArray(value)) {
59
- const out = value.slice(0, MAX_ARRAY).map((v) => redact(v, depth + 1));
73
+ const out = value
74
+ .slice(0, MAX_ARRAY)
75
+ .map((v) => redact(v, depth + 1, maxString));
60
76
  if (value.length > MAX_ARRAY)
61
77
  out.push(`…(${value.length - MAX_ARRAY} more)`);
62
78
  return out;
@@ -70,7 +86,9 @@ function redact(value: unknown, depth: number): unknown {
70
86
  break;
71
87
  }
72
88
  n += 1;
73
- out[k] = SENSITIVE_KEY.test(k) ? REDACTED : redact(v, depth + 1);
89
+ out[k] = SENSITIVE_KEY.test(k)
90
+ ? REDACTED
91
+ : redact(v, depth + 1, maxString);
74
92
  }
75
93
  return out;
76
94
  }
@@ -78,32 +96,100 @@ function redact(value: unknown, depth: number): unknown {
78
96
  return undefined;
79
97
  }
80
98
 
99
+ export interface RedactLimits {
100
+ /**
101
+ * Hard cap on the serialized output, INCLUDING the truncation envelope.
102
+ * Surfaces with their own wire budget (the A2A activity snapshot) pass a
103
+ * tighter cap than the audit-log default.
104
+ */
105
+ maxJson?: number;
106
+ /** Cap on any single string value before it gets a truncation marker. */
107
+ maxString?: number;
108
+ }
109
+
81
110
  /**
82
111
  * Redact and serialize call arguments to a capped JSON string, or `null` when
83
112
  * there is nothing to record. Never throws.
84
113
  */
85
- export function redactArgsToJson(args: unknown): string | null {
114
+ export function redactArgsToJson(
115
+ args: unknown,
116
+ limits?: RedactLimits,
117
+ ): string | null {
118
+ const maxJson = Math.max(MARKER_SLACK * 4, limits?.maxJson ?? MAX_JSON);
119
+ const maxString = limits?.maxString ?? MAX_STRING;
86
120
  try {
87
121
  if (args == null) return null;
88
- const redacted = redact(args, 0);
122
+ const redacted = redact(args, 0, maxString);
89
123
  if (redacted === undefined) return null;
90
124
  const json = JSON.stringify(redacted);
91
125
  if (json == null) return null;
92
- if (json.length > MAX_JSON) {
93
- // Slicing the serialized JSON would yield an unparseable string. Wrap a
94
- // preview in a valid envelope so `get-audit-event` can always JSON.parse
95
- // the stored `input`.
96
- return JSON.stringify({
126
+ if (json.length <= maxJson) return json;
127
+ // Slicing the serialized JSON would yield an unparseable string. Wrap a
128
+ // preview in a valid envelope so `get-audit-event` can always JSON.parse
129
+ // the stored `input`. JSON escaping makes the envelope overhead
130
+ // content-dependent, so shrink the preview until the whole thing measures
131
+ // under the cap rather than guessing at the overhead.
132
+ const envelope = (preview: string) =>
133
+ JSON.stringify({
97
134
  _auditTruncated: true,
98
135
  originalBytes: json.length,
99
- preview: json.slice(0, MAX_JSON),
136
+ preview,
100
137
  });
138
+ let take = maxJson;
139
+ let out = envelope(json.slice(0, take));
140
+ while (out.length > maxJson && take > 0) {
141
+ take = Math.max(0, take - Math.max(1, out.length - maxJson));
142
+ out = envelope(json.slice(0, take));
101
143
  }
102
- return json;
144
+ return out;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Same bounds as `redactArgsToJson`, returned as a structural value for
152
+ * surfaces whose consumers expect an object (transcript `metadata.input`).
153
+ */
154
+ export function redactArgsToValue(
155
+ args: unknown,
156
+ limits?: RedactLimits,
157
+ ): unknown {
158
+ const json = redactArgsToJson(args, limits);
159
+ if (json == null) return null;
160
+ try {
161
+ return JSON.parse(json);
103
162
  } catch {
104
163
  return null;
105
164
  }
106
165
  }
107
166
 
167
+ /**
168
+ * Bounded, redacted plain-text summary of a tool result. Oversized text keeps
169
+ * its head and gains an explicit `…(N more chars)` marker, so a reader can
170
+ * always tell a short result from a clipped one.
171
+ */
172
+ export function redactTextToSummary(
173
+ text: string,
174
+ maxChars = MAX_STRING,
175
+ ): string | null {
176
+ if (!text) return null;
177
+ // Only ever walk a bounded head: a multi-megabyte tool result must not be
178
+ // scanned in full just to produce a preview of it.
179
+ const head = text.slice(0, maxChars + MARKER_SLACK);
180
+ const redactedHead = redactEmbeddedSecrets(head);
181
+ if (looksSecret(redactedHead.trim())) return REDACTED;
182
+ if (text.length <= maxChars && redactedHead.length <= maxChars) {
183
+ return redactedHead;
184
+ }
185
+ const keep = Math.max(0, maxChars - MARKER_SLACK);
186
+ return `${redactedHead.slice(0, keep)}…(${text.length - keep} more chars)`;
187
+ }
188
+
108
189
  /** Exposed for tests. */
109
- export const __test = { looksSecret, redact, SENSITIVE_KEY };
190
+ export const __test = {
191
+ looksSecret,
192
+ redact,
193
+ redactEmbeddedSecrets,
194
+ SENSITIVE_KEY,
195
+ };
@@ -88,6 +88,7 @@ import {
88
88
  } from "./agent-sidebar-state.js";
89
89
  import { trackEvent } from "./analytics.js";
90
90
  import { agentNativePath, appPath } from "./api-path.js";
91
+ import { readClientAppState } from "./application-state.js";
91
92
  import { assistantUiRecoverableRenderErrorKind } from "./assistant-ui-recovery.js";
92
93
  import type { AssistantChatProps } from "./AssistantChat.js";
93
94
  import { shouldParentFrameOwnAgentPanel } from "./builder-frame.js";
@@ -2356,13 +2357,7 @@ function URLSync({ browserTabId }: { browserTabId?: string }) {
2356
2357
  queryKey: setUrlQueryKey,
2357
2358
  queryFn: async () => {
2358
2359
  const read = async (key: string) => {
2359
- const res = await fetch(
2360
- agentNativePath(`/_agent-native/application-state/${key}`),
2361
- );
2362
- if (!res.ok || res.status === 204) return null;
2363
- const text = await res.text();
2364
- if (!text) return null;
2365
- const data = JSON.parse(text);
2360
+ const data = await readClientAppState<Record<string, unknown>>(key);
2366
2361
  return data ? { key, command: data } : null;
2367
2362
  };
2368
2363
  try {
@@ -2610,6 +2610,9 @@ export function MultiTabAssistantChat({
2610
2610
  hasInFlightWork={() =>
2611
2611
  chatRefs.current.get(tabId)?.hasInFlightWork() ?? false
2612
2612
  }
2613
+ isAwaitingResponse={() =>
2614
+ chatRefs.current.get(tabId)?.isRunning() ?? false
2615
+ }
2613
2616
  onRetry={() => {
2614
2617
  const handle = chatRefs.current.get(tabId);
2615
2618
  handle?.sendRecoveryMessage(
@@ -50,6 +50,16 @@ export interface RunStuckBannerProps {
50
50
  * render; omit to keep the unconditional Retry/Cancel behavior.
51
51
  */
52
52
  hasInFlightWork?: () => boolean;
53
+ /**
54
+ * Returns true when this chat is actually waiting on a reply - typically
55
+ * backed by `chatHandle.isRunning()`. "Stuck" is a claim about THIS client,
56
+ * but the run row is the server's; a turn that finished normally can leave a
57
+ * row in `running` (terminal marking is best-effort, and the stale-run reaper
58
+ * runs later). Without this gate an idle, completed chat shows the warning
59
+ * and auto-retry re-prompts a thread the user considers done. Checked fresh
60
+ * on every render; omit to keep the unconditional behavior.
61
+ */
62
+ isAwaitingResponse?: () => boolean;
53
63
  /**
54
64
  * Called whenever the stuck state transitions. Useful for surfacing
55
65
  * observability events (Sentry, PostHog) at the call site.
@@ -170,6 +180,7 @@ export function RunStuckBanner({
170
180
  autoRetry = false,
171
181
  autoRetryOwnerId,
172
182
  hasInFlightWork,
183
+ isAwaitingResponse,
173
184
  className,
174
185
  }: RunStuckBannerProps) {
175
186
  const state = useRunStuckDetection({
@@ -180,6 +191,11 @@ export function RunStuckBanner({
180
191
  });
181
192
  const abortRun = useAbortRun(apiUrl);
182
193
  const [busy, setBusy] = useState<BusyState>({ type: "none" });
194
+ // "An automatic attempt was made for this run" is a different fact from
195
+ // "the controls are locked while a request is in flight". Sharing one flag
196
+ // for both would force the buttons to stay disabled just to keep the
197
+ // message on screen.
198
+ const [autoRetriedRunId, setAutoRetriedRunId] = useState<string | null>(null);
183
199
  const autoRetriedRunIdsRef = useRef<Set<string>>(new Set());
184
200
  const generatedOwnerIdRef = useRef<string | null>(null);
185
201
  if (!generatedOwnerIdRef.current) {
@@ -204,6 +220,7 @@ export function RunStuckBanner({
204
220
  // either signal says so.
205
221
  const inFlightWork =
206
222
  state.hasInFlightWork === true || (hasInFlightWork?.() ?? false);
223
+ const awaitingResponse = isAwaitingResponse?.() ?? true;
207
224
  // Server-continued runs are recovered by the SERVER (chained continuation
208
225
  // chunks + lost-handoff sweep); an automatic client abort would kill a live
209
226
  // server-chained run. Auto-retry is therefore disabled unconditionally for
@@ -261,6 +278,9 @@ export function RunStuckBanner({
261
278
  // A live tool/A2A call in flight — never auto-abort real work (see
262
279
  // `inFlightWork` comment above).
263
280
  inFlightWork ||
281
+ // Nothing is waiting on this run here; re-prompting would restart a
282
+ // thread the user already considers finished.
283
+ !awaitingResponse ||
264
284
  !state.isStuck ||
265
285
  !state.runId ||
266
286
  busy.type !== "none" ||
@@ -274,21 +294,22 @@ export function RunStuckBanner({
274
294
  autoRetriedRunIdsRef.current.add(runId);
275
295
  if (!claimed) return;
276
296
  setBusy({ type: "retry", runId });
297
+ setAutoRetriedRunId(runId);
277
298
  trackEvent("agent_chat_stuck_auto_retry", {
278
299
  runId,
279
300
  threadId: threadId ?? null,
280
301
  stuckSinceMs: state.stuckSinceMs ?? null,
281
302
  });
282
303
  void abortRun(runId, "auto_stuck_retry").then((aborted) => {
283
- if (aborted) {
284
- onRetry?.(aborted);
285
- return;
286
- }
304
+ // Release the controls on both outcomes. If the abort does not move
305
+ // the row off `running`, polling may never observe a new run id. The
306
+ // automatic-attempt set still prevents re-entry.
287
307
  setBusy((current) =>
288
308
  current.type !== "none" && current.runId === runId
289
309
  ? { type: "none" }
290
310
  : current,
291
311
  );
312
+ if (aborted) onRetry?.(aborted);
292
313
  });
293
314
  });
294
315
  }, [
@@ -304,6 +325,7 @@ export function RunStuckBanner({
304
325
  state.runId,
305
326
  state.stuckSinceMs,
306
327
  threadId,
328
+ awaitingResponse,
307
329
  ]);
308
330
 
309
331
  // A stale progress timestamp is not actionable while the server is still
@@ -314,7 +336,8 @@ export function RunStuckBanner({
314
336
  !state.isStuck ||
315
337
  !state.runId ||
316
338
  backgroundWorkerStillAlive ||
317
- inFlightWork
339
+ inFlightWork ||
340
+ !awaitingResponse
318
341
  ) {
319
342
  return null;
320
343
  }
@@ -380,7 +403,7 @@ export function RunStuckBanner({
380
403
  No progress
381
404
  {stuckSeconds != null ? ` for ${stuckSeconds}s` : ""}. The agent may
382
405
  have hit a server timeout or lost its connection.
383
- {autoRetry && busyType === "retry"
406
+ {autoRetry && autoRetriedRunId === state.runId
384
407
  ? " Retrying automatically now."
385
408
  : ""}
386
409
  </span>
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type { ReasoningEffort } from "../shared/reasoning-effort.js";
11
11
  import { agentNativePath } from "./api-path.js";
12
+ import { readClientAppState } from "./application-state.js";
12
13
  import {
13
14
  isInBuilderFrame,
14
15
  isTrustedBuilderMessage,
@@ -598,15 +599,9 @@ export async function refreshAgentChatContext(): Promise<AgentChatContextState>
598
599
  return agentChatContextState;
599
600
  }
600
601
  try {
601
- const res = await fetch(
602
- agentNativePath(
603
- `/_agent-native/application-state/${AGENT_CHAT_CONTEXT_STATE_KEY}`,
604
- ),
605
- );
606
- if (!res.ok || res.status === 204) return agentChatContextState;
607
- const text = await res.text();
608
- if (!text) return agentChatContextState;
609
- const state = normalizeAgentChatContextState(JSON.parse(text));
602
+ const raw = await readClientAppState(AGENT_CHAT_CONTEXT_STATE_KEY);
603
+ if (raw === null) return agentChatContextState;
604
+ const state = normalizeAgentChatContextState(raw);
610
605
  if (!state) return agentChatContextState;
611
606
  return publishAgentChatContextItems(state.items, {
612
607
  persist: false,
@@ -93,19 +93,117 @@ function jsonBody(value: unknown): string {
93
93
  return body;
94
94
  }
95
95
 
96
+ /**
97
+ * Result of a batched read. `values` holds only the keys the server has a row
98
+ * for; every other requested key is in `missing`. A key stored with a `null`
99
+ * value lands in `values` with `null` and NOT in `missing` — that is how
100
+ * "never written" stays distinguishable from "written as null/empty".
101
+ */
102
+ export interface ClientAppStateBatch {
103
+ values: Record<string, unknown>;
104
+ missing: string[];
105
+ }
106
+
107
+ /** Server caps a batch at 100 keys; stay under it when splitting. */
108
+ const MAX_BATCH_KEYS = 100;
109
+
110
+ export async function readClientAppStateMany(
111
+ keys: readonly string[],
112
+ options: ClientAppStateReadOptions = {},
113
+ ): Promise<ClientAppStateBatch> {
114
+ const unique = [...new Set(keys)];
115
+ for (const key of unique) appStateUrl(key); // validates the key shape
116
+ if (unique.length === 0) return { values: {}, missing: [] };
117
+
118
+ const merged: ClientAppStateBatch = { values: {}, missing: [] };
119
+ for (let i = 0; i < unique.length; i += MAX_BATCH_KEYS) {
120
+ const chunk = unique.slice(i, i + MAX_BATCH_KEYS);
121
+ const url = `${agentNativePath("/_agent-native/application-state")}?keys=${chunk
122
+ .map(encodeURIComponent)
123
+ .join(",")}`;
124
+ const response = await fetch(url, {
125
+ method: "GET",
126
+ cache: "no-store",
127
+ signal: options.signal,
128
+ });
129
+ const batch = await parseAppStateResponse<ClientAppStateBatch | null>(
130
+ response,
131
+ `Read application state [${chunk.join(", ")}]`,
132
+ );
133
+ if (!batch || typeof batch !== "object" || !batch.values) {
134
+ throw new Error(
135
+ `Read application state [${chunk.join(", ")}] returned an unexpected payload.`,
136
+ );
137
+ }
138
+ Object.assign(merged.values, batch.values);
139
+ merged.missing.push(...(batch.missing ?? []));
140
+ }
141
+ return merged;
142
+ }
143
+
144
+ // Reads issued in the same tick are coalesced into one batched request. Every
145
+ // mounted composer, question card and suggestion hook reads its own key on
146
+ // mount, which used to be one HTTP request (and one full identity resolution)
147
+ // each — ~55 on an analytics dashboard load.
148
+ let pendingBatch:
149
+ | { keys: Set<string>; promise: Promise<ClientAppStateBatch> }
150
+ | undefined;
151
+
152
+ function scheduleBatchedRead(key: string): Promise<ClientAppStateBatch> {
153
+ if (pendingBatch) {
154
+ pendingBatch.keys.add(key);
155
+ return pendingBatch.promise;
156
+ }
157
+ const keys = new Set([key]);
158
+ const promise = new Promise<ClientAppStateBatch>((resolve, reject) => {
159
+ setTimeout(() => {
160
+ pendingBatch = undefined;
161
+ readClientAppStateMany([...keys]).then(resolve, reject);
162
+ }, 0);
163
+ });
164
+ pendingBatch = { keys, promise };
165
+ return promise;
166
+ }
167
+
168
+ function awaitWithAbort<T>(
169
+ promise: Promise<T>,
170
+ signal?: AbortSignal,
171
+ ): Promise<T> {
172
+ if (!signal) return promise;
173
+ const abortReason = () =>
174
+ signal.reason ?? new Error("The operation was aborted");
175
+ if (signal.aborted) return Promise.reject(abortReason());
176
+ return new Promise<T>((resolve, reject) => {
177
+ const onAbort = () => {
178
+ cleanup();
179
+ reject(abortReason());
180
+ };
181
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
182
+ signal.addEventListener("abort", onAbort, { once: true });
183
+ promise.then(
184
+ (value) => {
185
+ cleanup();
186
+ if (signal.aborted) reject(abortReason());
187
+ else resolve(value);
188
+ },
189
+ (error) => {
190
+ cleanup();
191
+ reject(error);
192
+ },
193
+ );
194
+ });
195
+ }
196
+
96
197
  export async function readClientAppState<T = unknown>(
97
198
  key: string,
98
199
  options: ClientAppStateReadOptions = {},
99
200
  ): Promise<T | null> {
100
- const response = await fetch(appStateUrl(key), {
101
- method: "GET",
102
- cache: "no-store",
103
- signal: options.signal,
104
- });
105
- return parseAppStateResponse<T | null>(
106
- response,
107
- `Read application state "${key}"`,
108
- );
201
+ appStateUrl(key); // validates the key shape before it joins a batch
202
+ if (options.signal?.aborted) {
203
+ throw options.signal.reason ?? new Error("Aborted");
204
+ }
205
+ const batch = await awaitWithAbort(scheduleBatchedRead(key), options.signal);
206
+ return (batch.values[key] ?? null) as T | null;
109
207
  }
110
208
 
111
209
  export async function writeClientAppState<T = unknown>(
@@ -129,9 +227,14 @@ export async function deleteClientAppState(
129
227
  ): Promise<void> {
130
228
  const response = await fetch(appStateUrl(key), {
131
229
  method: "DELETE",
132
- headers: options.requestSource
133
- ? { "X-Request-Source": options.requestSource }
134
- : undefined,
230
+ // DELETE carries no JSON body, so this custom header is the only
231
+ // same-origin marker the CSRF check can see from an embedded frame.
232
+ headers: {
233
+ "X-Agent-Native-CSRF": "1",
234
+ ...(options.requestSource
235
+ ? { "X-Request-Source": options.requestSource }
236
+ : {}),
237
+ },
135
238
  keepalive: options.keepalive,
136
239
  signal: options.signal,
137
240
  });
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
2
 
3
- import { agentNativePath } from "./api-path.js";
3
+ import { readClientAppState } from "./application-state.js";
4
4
  import { useChangeVersions } from "./use-change-version.js";
5
5
  import type { ChatThreadScope } from "./use-chat-threads.js";
6
6
 
@@ -89,17 +89,9 @@ function appStateKeyForBrowserTab(key: string, browserTabId?: string): string {
89
89
  }
90
90
 
91
91
  async function readAppState(key: string): Promise<unknown> {
92
- const res = await fetch(
93
- agentNativePath(`/_agent-native/application-state/${key}`),
94
- );
95
- if (!res.ok || res.status === 204) return null;
96
- const text = await res.text();
97
- if (!text) return null;
98
- try {
99
- return JSON.parse(text);
100
- } catch {
101
- return null;
102
- }
92
+ // Reads issued in the same tick coalesce into one batched request, so the
93
+ // four keys below cost one round trip rather than four.
94
+ return readClientAppState(key).catch(() => null);
103
95
  }
104
96
 
105
97
  async function readScopedAppState(
@@ -12,8 +12,11 @@ import {
12
12
  import { useCallback, useEffect, useMemo, useState } from "react";
13
13
 
14
14
  import { sendToAgentChat } from "./agent-chat.js";
15
- import { agentNativePath } from "./api-path.js";
16
- import { setClientAppState } from "./application-state.js";
15
+ import {
16
+ deleteClientAppState,
17
+ readClientAppState,
18
+ setClientAppState,
19
+ } from "./application-state.js";
17
20
  import { useChangeVersions } from "./use-change-version.js";
18
21
  import { cn } from "./utils.js";
19
22
 
@@ -998,10 +1001,6 @@ export function useGuidedQuestionFlow({
998
1001
  () => normalizeBrowserTabId(browserTabId),
999
1002
  [browserTabId],
1000
1003
  );
1001
- const endpointFor = useCallback(
1002
- (key: string) => agentNativePath(`/_agent-native/application-state/${key}`),
1003
- [],
1004
- );
1005
1004
  const scopedKey = normalizedBrowserTabId
1006
1005
  ? `${stateKey}:${normalizedBrowserTabId}`
1007
1006
  : stateKey;
@@ -1036,17 +1035,11 @@ export function useGuidedQuestionFlow({
1036
1035
  enabled,
1037
1036
  queryFn: async () => {
1038
1037
  const read = async (key: string) => {
1039
- const res = await fetch(endpointFor(key));
1040
- if (!res.ok) return null;
1041
- const text = await res.text();
1042
- if (!text) return null;
1043
- try {
1044
- const parsed = JSON.parse(text);
1045
- if (Array.isArray(parsed?.questions) && parsed.questions.length > 0) {
1046
- return parsed as GuidedQuestionPayload;
1047
- }
1048
- } catch {
1049
- return null;
1038
+ const parsed = await readClientAppState<GuidedQuestionPayload>(
1039
+ key,
1040
+ ).catch(() => null);
1041
+ if (Array.isArray(parsed?.questions) && parsed.questions.length > 0) {
1042
+ return parsed;
1050
1043
  }
1051
1044
  return null;
1052
1045
  };
@@ -1086,16 +1079,12 @@ export function useGuidedQuestionFlow({
1086
1079
  const clear = useCallback(() => {
1087
1080
  setPayload(null);
1088
1081
  queryClient.setQueryData(resolvedQueryKey, null);
1089
- const del = (key: string) =>
1090
- fetch(endpointFor(key), {
1091
- method: "DELETE",
1092
- headers: { "X-Agent-Native-CSRF": "1" },
1093
- }).catch(() => {});
1082
+ const del = (key: string) => deleteClientAppState(key).catch(() => {});
1094
1083
  // Clear whichever key actually held the payload (scoped or bare) so the
1095
1084
  // card doesn't reappear on the next poll.
1096
1085
  del(scopedKey);
1097
1086
  if (scopedKey !== stateKey) del(stateKey);
1098
- }, [endpointFor, queryClient, resolvedQueryKey, scopedKey, stateKey]);
1087
+ }, [queryClient, resolvedQueryKey, scopedKey, stateKey]);
1099
1088
 
1100
1089
  const handleSubmit = useCallback(
1101
1090
  (answers: GuidedQuestionAnswers) => {