@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +99 -0
- package/corpus/core/docs/content/durable-background-runs.mdx +15 -6
- package/corpus/core/docs/design/durable-agent-runs.md +15 -11
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/activity.ts +127 -10
- package/corpus/core/src/a2a/index.ts +3 -0
- package/corpus/core/src/agent/durable-background.ts +38 -28
- package/corpus/core/src/agent/harness/background.ts +12 -2
- package/corpus/core/src/agent/production-agent.ts +18 -15
- package/corpus/core/src/agent/run-store.ts +45 -3
- package/corpus/core/src/application-state/handlers.ts +45 -0
- package/corpus/core/src/application-state/index.ts +3 -0
- package/corpus/core/src/application-state/store.ts +28 -11
- package/corpus/core/src/audit/redact.ts +104 -18
- package/corpus/core/src/client/AgentPanel.tsx +2 -7
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +3 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +29 -6
- package/corpus/core/src/client/agent-chat.ts +4 -9
- package/corpus/core/src/client/application-state.ts +115 -12
- package/corpus/core/src/client/dynamic-suggestions.ts +4 -12
- package/corpus/core/src/client/guided-questions.tsx +12 -23
- package/corpus/core/src/db/client.ts +148 -28
- package/corpus/core/src/db/create-get-db.ts +39 -6
- package/corpus/core/src/db/runtime-diagnostics.ts +41 -6
- package/corpus/core/src/deploy/build.ts +26 -33
- package/corpus/core/src/feature-flags/store.ts +10 -5
- package/corpus/core/src/integrations/a2a-continuations-store.ts +11 -0
- package/corpus/core/src/integrations/config-store.ts +13 -0
- package/corpus/core/src/integrations/google-docs-poller.ts +26 -2
- package/corpus/core/src/mcp-client/routes.ts +33 -2
- package/corpus/core/src/onboarding/plugin.ts +31 -25
- package/corpus/core/src/org/accept-pending.ts +2 -0
- package/corpus/core/src/org/auto-join-domain.ts +2 -0
- package/corpus/core/src/org/context.ts +35 -10
- package/corpus/core/src/org/handlers.ts +6 -0
- package/corpus/core/src/org/request-org-cache.ts +71 -0
- package/corpus/core/src/resources/store.ts +10 -9
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/secrets/storage.ts +97 -5
- package/corpus/core/src/server/action-change.ts +33 -0
- package/corpus/core/src/server/action-routes.ts +11 -9
- package/corpus/core/src/server/agent-teams.ts +10 -2
- package/corpus/core/src/server/better-auth-instance.ts +39 -12
- package/corpus/core/src/server/core-routes-plugin.ts +19 -2
- package/corpus/core/src/server/credential-provider.ts +57 -14
- package/corpus/core/src/server/http-response-telemetry.ts +1 -0
- package/corpus/core/src/server/poll.ts +95 -57
- package/corpus/core/src/server/voice-providers-status.ts +19 -4
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +38 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
- package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +299 -0
- package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +34 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +82 -0
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +2 -2
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +40 -34
- package/corpus/templates/analytics/app/hooks/use-user-pref.ts +3 -3
- package/corpus/templates/analytics/app/i18n-data.ts +40 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +18 -10
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +3 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-dashboards-now-expose-native-funnel-heatmap-callou.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-30-analytics-now-ships-reusable-native-v2-dashboard-manifests-w.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +81 -1
- package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +457 -0
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +73 -2
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/shared/dashboard-funnel.ts +83 -0
- package/corpus/templates/content/changelog/2026-07-30-long-running-delegated-requests-now-continue-reliably.md +6 -0
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +2 -1
- package/corpus/templates/slides/actions/patch-deck.ts +25 -1
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +17 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +9 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +11 -3
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +39 -11
- package/corpus/templates/slides/app/pages/Index.tsx +2 -2
- package/corpus/templates/slides/changelog/2026-07-30-slide-previews-keep-their-aspect-ratio-isolate-embedded-styl.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-30-slides-no-longer-lose-decks-while-organization-access-is-loa.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +8 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +16 -11
- package/dist/a2a/activity.d.ts +22 -4
- package/dist/a2a/activity.d.ts.map +1 -1
- package/dist/a2a/activity.js +97 -11
- package/dist/a2a/activity.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/agent/durable-background.d.ts +13 -8
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +36 -28
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/harness/background.d.ts.map +1 -1
- package/dist/agent/harness/background.js +12 -2
- package/dist/agent/harness/background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -15
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts +2 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +44 -3
- package/dist/agent/run-store.js.map +1 -1
- package/dist/application-state/handlers.d.ts +15 -0
- package/dist/application-state/handlers.d.ts.map +1 -1
- package/dist/application-state/handlers.js +42 -2
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/store.d.ts +11 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +23 -10
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/redact.d.ts +25 -2
- package/dist/audit/redact.d.ts.map +1 -1
- package/dist/audit/redact.js +85 -21
- package/dist/audit/redact.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -7
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts +11 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +20 -7
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +4 -6
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/application-state.d.ts +11 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +87 -9
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/dynamic-suggestions.d.ts.map +1 -1
- package/dist/client/dynamic-suggestions.js +4 -13
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +6 -22
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/db/client.d.ts +57 -10
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +122 -31
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +31 -7
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/runtime-diagnostics.d.ts +1 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -1
- package/dist/db/runtime-diagnostics.js +31 -6
- package/dist/db/runtime-diagnostics.js.map +1 -1
- package/dist/deploy/build.d.ts +5 -20
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +18 -30
- package/dist/deploy/build.js.map +1 -1
- package/dist/feature-flags/store.d.ts.map +1 -1
- package/dist/feature-flags/store.js +10 -5
- package/dist/feature-flags/store.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +12 -0
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/config-store.d.ts +1 -0
- package/dist/integrations/config-store.d.ts.map +1 -1
- package/dist/integrations/config-store.js +11 -0
- package/dist/integrations/config-store.js.map +1 -1
- package/dist/integrations/google-docs-poller.d.ts.map +1 -1
- package/dist/integrations/google-docs-poller.js +19 -2
- package/dist/integrations/google-docs-poller.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +30 -2
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/plugin.d.ts.map +1 -1
- package/dist/onboarding/plugin.js +12 -8
- package/dist/onboarding/plugin.js.map +1 -1
- package/dist/org/accept-pending.d.ts.map +1 -1
- package/dist/org/accept-pending.js +2 -0
- package/dist/org/accept-pending.js.map +1 -1
- package/dist/org/auto-join-domain.d.ts.map +1 -1
- package/dist/org/auto-join-domain.js +2 -0
- package/dist/org/auto-join-domain.js.map +1 -1
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/context.js +29 -9
- package/dist/org/context.js.map +1 -1
- package/dist/org/handlers.d.ts.map +1 -1
- package/dist/org/handlers.js +5 -0
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/request-org-cache.d.ts +16 -0
- package/dist/org/request-org-cache.d.ts.map +1 -0
- package/dist/org/request-org-cache.js +62 -0
- package/dist/org/request-org-cache.js.map +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +11 -9
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +3 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +82 -5
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/action-change.d.ts +20 -0
- package/dist/server/action-change.d.ts.map +1 -1
- package/dist/server/action-change.js +29 -0
- package/dist/server/action-change.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +7 -8
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +10 -2
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +31 -14
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +18 -4
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/credential-provider.d.ts +16 -0
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +49 -13
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts.map +1 -1
- package/dist/server/http-response-telemetry.js +1 -0
- package/dist/server/http-response-telemetry.js.map +1 -1
- package/dist/server/poll.d.ts +6 -0
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +84 -47
- package/dist/server/poll.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/voice-providers-status.d.ts.map +1 -1
- package/dist/server/voice-providers-status.js +12 -3
- package/dist/server/voice-providers-status.js.map +1 -1
- package/docs/content/durable-background-runs.mdx +15 -6
- package/docs/design/durable-agent-runs.md +15 -11
- package/package.json +2 -2
- package/src/a2a/activity.ts +127 -10
- package/src/a2a/index.ts +3 -0
- package/src/agent/durable-background.ts +38 -28
- package/src/agent/harness/background.ts +12 -2
- package/src/agent/production-agent.ts +18 -15
- package/src/agent/run-store.ts +45 -3
- package/src/application-state/handlers.ts +45 -0
- package/src/application-state/index.ts +3 -0
- package/src/application-state/store.ts +28 -11
- package/src/audit/redact.ts +104 -18
- package/src/client/AgentPanel.tsx +2 -7
- package/src/client/MultiTabAssistantChat.tsx +3 -0
- package/src/client/RunStuckBanner.tsx +29 -6
- package/src/client/agent-chat.ts +4 -9
- package/src/client/application-state.ts +115 -12
- package/src/client/dynamic-suggestions.ts +4 -12
- package/src/client/guided-questions.tsx +12 -23
- package/src/db/client.ts +148 -28
- package/src/db/create-get-db.ts +39 -6
- package/src/db/runtime-diagnostics.ts +41 -6
- package/src/deploy/build.ts +26 -33
- package/src/feature-flags/store.ts +10 -5
- package/src/integrations/a2a-continuations-store.ts +11 -0
- package/src/integrations/config-store.ts +13 -0
- package/src/integrations/google-docs-poller.ts +26 -2
- package/src/mcp-client/routes.ts +33 -2
- package/src/onboarding/plugin.ts +31 -25
- package/src/org/accept-pending.ts +2 -0
- package/src/org/auto-join-domain.ts +2 -0
- package/src/org/context.ts +35 -10
- package/src/org/handlers.ts +6 -0
- package/src/org/request-org-cache.ts +71 -0
- package/src/resources/store.ts +10 -9
- package/src/scripts/runner.ts +6 -3
- package/src/secrets/storage.ts +97 -5
- package/src/server/action-change.ts +33 -0
- package/src/server/action-routes.ts +11 -9
- package/src/server/agent-teams.ts +10 -2
- package/src/server/better-auth-instance.ts +39 -12
- package/src/server/core-routes-plugin.ts +19 -2
- package/src/server/credential-provider.ts +57 -14
- package/src/server/http-response-telemetry.ts +1 -0
- package/src/server/poll.ts +95 -57
- package/src/server/voice-providers-status.ts +19 -4
|
@@ -5673,23 +5673,26 @@ export async function runAgentLoop(opts: {
|
|
|
5673
5673
|
result = `${result}\n\n${formatAgentWarningsForToolResult(agentWarnings)}`;
|
|
5674
5674
|
}
|
|
5675
5675
|
|
|
5676
|
-
// Auto-refresh the UI after a successful mutating tool call. Any
|
|
5677
|
-
// that isn't
|
|
5678
|
-
//
|
|
5679
|
-
//
|
|
5680
|
-
//
|
|
5681
|
-
// remember to call
|
|
5682
|
-
|
|
5676
|
+
// Auto-refresh the UI after a successful mutating tool call. Any call
|
|
5677
|
+
// that isn't read-only — by its own per-call Plan-mode effect, else the
|
|
5678
|
+
// action's readOnly flag — is assumed to mutate. The client's useDbSync
|
|
5679
|
+
// listener sees a change event with source:"action" and invalidates
|
|
5680
|
+
// ["action"] queries so list-* / get-* refetch. This makes refresh after
|
|
5681
|
+
// agent writes reliable without the model needing to remember to call
|
|
5682
|
+
// `refresh-screen` itself.
|
|
5683
|
+
if (!isError) {
|
|
5683
5684
|
try {
|
|
5684
|
-
const { notifyActionChange } =
|
|
5685
|
+
const { actionCallIsReadOnly, notifyActionChange } =
|
|
5685
5686
|
await import("../server/action-change.js");
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5687
|
+
if (!actionCallIsReadOnly(actionEntry, toolCall.input, false)) {
|
|
5688
|
+
const owner = opts.ownerEmail ?? getRequestUserEmail() ?? undefined;
|
|
5689
|
+
const orgId = opts.orgId ?? getRequestOrgId() ?? undefined;
|
|
5690
|
+
await notifyActionChange({
|
|
5691
|
+
actionName: toolCall.name,
|
|
5692
|
+
...(owner ? { owner } : {}),
|
|
5693
|
+
...(orgId ? { orgId } : {}),
|
|
5694
|
+
});
|
|
5695
|
+
}
|
|
5693
5696
|
} catch {
|
|
5694
5697
|
// poll module may be unavailable in non-server contexts — ignore
|
|
5695
5698
|
}
|
|
@@ -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.
|
|
2574
|
-
//
|
|
2575
|
-
//
|
|
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
|
-
*
|
|
117
|
-
*
|
|
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
|
|
121
|
+
export async function appStateGetManyEntries(
|
|
120
122
|
sessionId: string,
|
|
121
123
|
keys: readonly string[],
|
|
122
|
-
): Promise<
|
|
124
|
+
): Promise<Array<{ key: string; value: Record<string, unknown> | null }>> {
|
|
123
125
|
const uniqueKeys = [...new Set(keys)];
|
|
124
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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 >
|
|
48
|
-
return `${value.slice(0,
|
|
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
|
|
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
|
|
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)
|
|
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(
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 &&
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
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,
|