@agent-native/core 0.84.18 → 0.84.20
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 +1 -1
- package/corpus/core/CHANGELOG.md +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/run-store.ts +180 -13
- package/corpus/core/src/client/AssistantChat.tsx +49 -0
- package/corpus/core/src/client/active-run-state.ts +30 -0
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +41 -3
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +133 -0
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -2
- package/corpus/core/src/extensions/html-shell.ts +17 -3
- package/corpus/core/src/extensions/theme.ts +39 -0
- package/corpus/templates/analytics/actions/provider-api-request.ts +8 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +91 -33
- package/corpus/templates/clips/changelog/2026-07-01-chrome-recording-uploads-now-serialize-chunks-to-avoid-resum.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -23
- package/dist/agent/run-store.d.ts +19 -6
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +163 -13
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +36 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/active-run-state.d.ts +3 -0
- package/dist/client/active-run-state.d.ts.map +1 -1
- package/dist/client/active-run-state.js +24 -0
- package/dist/client/active-run-state.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +32 -3
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +59 -2
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +2 -2
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/extensions/html-shell.d.ts.map +1 -1
- package/dist/extensions/html-shell.js +17 -3
- package/dist/extensions/html-shell.js.map +1 -1
- package/dist/extensions/theme.d.ts +9 -0
- package/dist/extensions/theme.d.ts.map +1 -1
- package/dist/extensions/theme.js +38 -0
- package/dist/extensions/theme.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.20
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 93ed23b: Use a filter icon instead of a gear icon for the Extensions sidebar section sort/filter options trigger.
|
|
8
|
+
- ce62d53: Fix extension iframe theme inheritance and add source-code editor. Extensions now correctly inherit the host app's dark/light theme and CSS custom properties instead of rendering with a white background. Adds a raw HTML/Alpine.js source editor dialog to ExtensionViewer so extensions can be edited directly without going through the AI chat.
|
|
9
|
+
|
|
10
|
+
## 0.84.19
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 92b576f: Keep durable background agent runs from being reported as stale after a terminal event was already persisted, and show tool activity during tail reconnects.
|
|
15
|
+
|
|
3
16
|
## 0.84.18
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.20",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -29,6 +29,8 @@ export const RUN_STALE_MS = 15_000;
|
|
|
29
29
|
* With the normal 15s window the reaper would falsely kill that freshly-
|
|
30
30
|
* inserted-but-not-yet-heartbeaten row. 90s tolerates a slow background
|
|
31
31
|
* cold-start while still reaping a genuinely dead background worker promptly.
|
|
32
|
+
* Claimed background workers heartbeat during long work; the stale watchdog is a
|
|
33
|
+
* liveness timeout, not the Netlify background-function execution budget.
|
|
32
34
|
*
|
|
33
35
|
* Only applied to rows explicitly marked background-dispatched; ordinary
|
|
34
36
|
* foreground runs keep the tight 15s window unchanged.
|
|
@@ -85,13 +87,14 @@ export const CLAIMED_BACKGROUND_WORKER_FAILED_ERROR_EVENT = {
|
|
|
85
87
|
* Grace period before a never-claimed background run (dispatch_mode still
|
|
86
88
|
* 'background', no worker claim) is treated as a dead handoff and reaped.
|
|
87
89
|
*
|
|
88
|
-
* This is intentionally
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* has, by definition, NO worker — nothing
|
|
90
|
+
* This is intentionally tighter than `BACKGROUND_RUN_STALE_MS`. That wider
|
|
91
|
+
* window protects cold-starting or temporarily delayed background dispatches,
|
|
92
|
+
* while claimed workers stay alive by heartbeat/progress updates. A run that is
|
|
93
|
+
* still `dispatch_mode = 'background'` has, by definition, NO worker — nothing
|
|
94
|
+
* to protect — so once a Netlify
|
|
92
95
|
* background function has had a reasonable cold-start window to claim it and
|
|
93
96
|
* hasn't, the handoff is dead and should surface promptly instead of leaving
|
|
94
|
-
* the user staring at a spinner for
|
|
97
|
+
* the user staring at a spinner for the durable-worker window. 25s comfortably exceeds a normal
|
|
95
98
|
* Netlify Lambda cold start while still failing fast on a silent worker death.
|
|
96
99
|
*/
|
|
97
100
|
export const UNCLAIMED_BACKGROUND_RUN_GRACE_MS = 25_000;
|
|
@@ -125,6 +128,7 @@ async function ensureRunTables(): Promise<void> {
|
|
|
125
128
|
CREATE TABLE IF NOT EXISTS agent_run_events (
|
|
126
129
|
run_id TEXT NOT NULL,
|
|
127
130
|
seq ${intType()} NOT NULL,
|
|
131
|
+
event_at ${intType()},
|
|
128
132
|
event_data TEXT NOT NULL,
|
|
129
133
|
PRIMARY KEY (run_id, seq)
|
|
130
134
|
)
|
|
@@ -200,6 +204,11 @@ async function ensureRunTables(): Promise<void> {
|
|
|
200
204
|
);
|
|
201
205
|
}
|
|
202
206
|
await ensureTableExists("agent_run_events", agentRunEventsCreateSql);
|
|
207
|
+
await ensureColumnExists(
|
|
208
|
+
"agent_run_events",
|
|
209
|
+
"event_at",
|
|
210
|
+
`ALTER TABLE agent_run_events ADD COLUMN IF NOT EXISTS event_at ${intType()}`,
|
|
211
|
+
);
|
|
203
212
|
await ensureTableExists("agent_tool_ledger", agentToolLedgerCreateSql);
|
|
204
213
|
// Widen millisecond-timestamp columns that older deployments created as
|
|
205
214
|
// 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
|
|
@@ -213,6 +222,7 @@ async function ensureRunTables(): Promise<void> {
|
|
|
213
222
|
"heartbeat_at",
|
|
214
223
|
"last_progress_at",
|
|
215
224
|
]);
|
|
225
|
+
await widenIntColumnsToBigInt("agent_run_events", ["event_at"]);
|
|
216
226
|
await widenIntColumnsToBigInt("agent_tool_ledger", ["completed_at"]);
|
|
217
227
|
return;
|
|
218
228
|
}
|
|
@@ -278,6 +288,13 @@ async function ensureRunTables(): Promise<void> {
|
|
|
278
288
|
}
|
|
279
289
|
}
|
|
280
290
|
await client.execute(agentRunEventsCreateSql);
|
|
291
|
+
try {
|
|
292
|
+
await client.execute(
|
|
293
|
+
`ALTER TABLE agent_run_events ADD COLUMN event_at ${intType()}`,
|
|
294
|
+
);
|
|
295
|
+
} catch {
|
|
296
|
+
// Column already exists — ignore
|
|
297
|
+
}
|
|
281
298
|
await client.execute(agentToolLedgerCreateSql);
|
|
282
299
|
// Widen millisecond-timestamp columns that older deployments created as
|
|
283
300
|
// 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
|
|
@@ -291,6 +308,7 @@ async function ensureRunTables(): Promise<void> {
|
|
|
291
308
|
"heartbeat_at",
|
|
292
309
|
"last_progress_at",
|
|
293
310
|
]);
|
|
311
|
+
await widenIntColumnsToBigInt("agent_run_events", ["event_at"]);
|
|
294
312
|
await widenIntColumnsToBigInt("agent_tool_ledger", ["completed_at"]);
|
|
295
313
|
})().catch((err) => {
|
|
296
314
|
// Retry init on the next call after a failed startup.
|
|
@@ -431,6 +449,20 @@ function backgroundAwareStaleCutoffSql(): string {
|
|
|
431
449
|
return `(CAST(? AS BIGINT) - CASE WHEN dispatch_mode LIKE 'background%' THEN ${BACKGROUND_RUN_STALE_MS} ELSE ${RUN_STALE_MS} END)`;
|
|
432
450
|
}
|
|
433
451
|
|
|
452
|
+
function terminalRunEventExclusionSql(runIdColumn = "id"): string {
|
|
453
|
+
return `NOT EXISTS (
|
|
454
|
+
SELECT 1 FROM agent_run_events terminal_events
|
|
455
|
+
WHERE terminal_events.run_id = agent_runs.${runIdColumn}
|
|
456
|
+
AND (
|
|
457
|
+
terminal_events.event_data LIKE '{"type":"done"%'
|
|
458
|
+
OR terminal_events.event_data LIKE '{"type":"error"%'
|
|
459
|
+
OR terminal_events.event_data LIKE '{"type":"missing_api_key"%'
|
|
460
|
+
OR terminal_events.event_data LIKE '{"type":"loop_limit"%'
|
|
461
|
+
OR terminal_events.event_data LIKE '{"type":"auto_continue"%'
|
|
462
|
+
)
|
|
463
|
+
)`;
|
|
464
|
+
}
|
|
465
|
+
|
|
434
466
|
/**
|
|
435
467
|
* Liveness basis for the stale reapers: the MOST RECENT of `heartbeat_at`
|
|
436
468
|
* ("process is up", bumped on a 1.5s timer) and `last_progress_at` ("real work
|
|
@@ -573,6 +605,7 @@ export async function tryClaimRunSlot(
|
|
|
573
605
|
sql: `SELECT id FROM agent_runs
|
|
574
606
|
WHERE thread_id = ?
|
|
575
607
|
AND status = 'running'
|
|
608
|
+
AND ${terminalRunEventExclusionSql()}
|
|
576
609
|
AND COALESCE(heartbeat_at, started_at) >= ?
|
|
577
610
|
ORDER BY started_at DESC LIMIT 1`,
|
|
578
611
|
args: [threadId, heartbeatCutoff],
|
|
@@ -586,6 +619,7 @@ export async function tryClaimRunSlot(
|
|
|
586
619
|
sql: `SELECT id FROM agent_runs
|
|
587
620
|
WHERE thread_id = ?
|
|
588
621
|
AND status = 'running'
|
|
622
|
+
AND ${terminalRunEventExclusionSql()}
|
|
589
623
|
AND COALESCE(heartbeat_at, started_at) >= ${backgroundAwareStaleCutoffSql()}
|
|
590
624
|
ORDER BY started_at DESC LIMIT 1`,
|
|
591
625
|
args: [threadId, now],
|
|
@@ -646,6 +680,112 @@ export async function setRunTerminalReason(
|
|
|
646
680
|
}
|
|
647
681
|
}
|
|
648
682
|
|
|
683
|
+
function terminalStatusForEvent(
|
|
684
|
+
event: AgentChatEvent,
|
|
685
|
+
): "completed" | "errored" | null {
|
|
686
|
+
if (event.type === "error") return "errored";
|
|
687
|
+
if (
|
|
688
|
+
event.type === "done" ||
|
|
689
|
+
event.type === "missing_api_key" ||
|
|
690
|
+
event.type === "loop_limit" ||
|
|
691
|
+
event.type === "auto_continue"
|
|
692
|
+
) {
|
|
693
|
+
return "completed";
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function terminalReasonForEvent(event: AgentChatEvent): string | null {
|
|
699
|
+
if (event.type === "auto_continue") return event.reason || "auto_continue";
|
|
700
|
+
if (event.type === "loop_limit") return "loop_limit";
|
|
701
|
+
if (event.type === "missing_api_key") return "missing_api_key";
|
|
702
|
+
if (event.type === "error") return `error:${event.errorCode || "unknown"}`;
|
|
703
|
+
if (event.type === "done") return "done";
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function getLatestRunEvent(runId: string): Promise<{
|
|
708
|
+
event: AgentChatEvent;
|
|
709
|
+
eventAt: number | null;
|
|
710
|
+
} | null> {
|
|
711
|
+
const client = getDbExec();
|
|
712
|
+
const { rows } = await client.execute({
|
|
713
|
+
sql: `SELECT seq, event_data, event_at FROM agent_run_events WHERE run_id = ? ORDER BY seq DESC LIMIT 1`,
|
|
714
|
+
args: [runId],
|
|
715
|
+
});
|
|
716
|
+
const row = rows[0] as
|
|
717
|
+
| {
|
|
718
|
+
event_at?: number | string | null;
|
|
719
|
+
event_data?: string;
|
|
720
|
+
}
|
|
721
|
+
| undefined;
|
|
722
|
+
const raw = row?.event_data;
|
|
723
|
+
if (!raw) return null;
|
|
724
|
+
try {
|
|
725
|
+
const eventAt = row.event_at == null ? NaN : Number(row.event_at);
|
|
726
|
+
return {
|
|
727
|
+
event: JSON.parse(raw) as AgentChatEvent,
|
|
728
|
+
eventAt: Number.isFinite(eventAt) && eventAt > 0 ? eventAt : null,
|
|
729
|
+
};
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Repair a run whose terminal event was durably appended but whose final
|
|
737
|
+
* `agent_runs.status` write lost a race with reconnect/reaper code.
|
|
738
|
+
*
|
|
739
|
+
* The event ledger is the durable transcript users see. If its latest event is
|
|
740
|
+
* terminal, the run is no longer alive and must not be converted into a stale
|
|
741
|
+
* error later. This keeps `agent_runs` and `agent_run_events` from telling two
|
|
742
|
+
* different stories after delayed DB writes or background function teardown.
|
|
743
|
+
*/
|
|
744
|
+
export async function reconcileTerminalRunFromEvents(
|
|
745
|
+
runId: string,
|
|
746
|
+
): Promise<boolean> {
|
|
747
|
+
await ensureRunTables();
|
|
748
|
+
const latest = await getLatestRunEvent(runId);
|
|
749
|
+
if (!latest) return false;
|
|
750
|
+
const status = terminalStatusForEvent(latest.event);
|
|
751
|
+
const terminalReason = terminalReasonForEvent(latest.event);
|
|
752
|
+
if (!status || !terminalReason) return false;
|
|
753
|
+
|
|
754
|
+
const client = getDbExec();
|
|
755
|
+
const errorCode =
|
|
756
|
+
latest.event.type === "error" ? (latest.event.errorCode ?? null) : null;
|
|
757
|
+
const errorDetail =
|
|
758
|
+
latest.event.type === "error"
|
|
759
|
+
? (latest.event.details || latest.event.error || "").slice(0, 2000) ||
|
|
760
|
+
null
|
|
761
|
+
: null;
|
|
762
|
+
const { rowsAffected } = await client.execute({
|
|
763
|
+
sql: `UPDATE agent_runs
|
|
764
|
+
SET status = ?,
|
|
765
|
+
completed_at = COALESCE(completed_at, ?, ${livenessBasisSql()}),
|
|
766
|
+
error_code = CASE WHEN ? IS NOT NULL THEN ? ELSE error_code END,
|
|
767
|
+
error_detail = CASE WHEN ? IS NOT NULL THEN ? ELSE error_detail END,
|
|
768
|
+
terminal_reason = COALESCE(terminal_reason, ?)
|
|
769
|
+
WHERE id = ?
|
|
770
|
+
AND (
|
|
771
|
+
status = 'running'
|
|
772
|
+
OR (status = 'errored' AND error_code = ?)
|
|
773
|
+
)`,
|
|
774
|
+
args: [
|
|
775
|
+
status,
|
|
776
|
+
latest.eventAt,
|
|
777
|
+
errorCode,
|
|
778
|
+
errorCode,
|
|
779
|
+
errorDetail,
|
|
780
|
+
errorDetail,
|
|
781
|
+
terminalReason,
|
|
782
|
+
runId,
|
|
783
|
+
STALE_RUN_ERROR_EVENT.errorCode,
|
|
784
|
+
],
|
|
785
|
+
});
|
|
786
|
+
return (rowsAffected ?? 0) > 0;
|
|
787
|
+
}
|
|
788
|
+
|
|
649
789
|
/**
|
|
650
790
|
* Diagnostic stage names recorded onto a background run as it moves through the
|
|
651
791
|
* `_process-run` worker pipeline. Each value is the LAST stage successfully
|
|
@@ -774,6 +914,7 @@ export async function reapIfStale(
|
|
|
774
914
|
maxStaleMs?: number,
|
|
775
915
|
): Promise<boolean> {
|
|
776
916
|
await ensureRunTables();
|
|
917
|
+
if (await reconcileTerminalRunFromEvents(runId)) return false;
|
|
777
918
|
const client = getDbExec();
|
|
778
919
|
const completedAt = Date.now();
|
|
779
920
|
// Background-dispatched runs get the wider stale window so a slow cold-start
|
|
@@ -794,6 +935,7 @@ export async function reapIfStale(
|
|
|
794
935
|
terminal_reason = ?
|
|
795
936
|
WHERE id = ?
|
|
796
937
|
AND status = 'running'
|
|
938
|
+
AND ${terminalRunEventExclusionSql()}
|
|
797
939
|
AND ${staleClause}`,
|
|
798
940
|
args: [
|
|
799
941
|
completedAt,
|
|
@@ -805,6 +947,7 @@ export async function reapIfStale(
|
|
|
805
947
|
],
|
|
806
948
|
});
|
|
807
949
|
const reaped = (rowsAffected ?? 0) > 0;
|
|
950
|
+
if (!reaped && (await reconcileTerminalRunFromEvents(runId))) return false;
|
|
808
951
|
if (reaped) {
|
|
809
952
|
await safeAppendTerminalRunEvent(
|
|
810
953
|
runId,
|
|
@@ -823,7 +966,7 @@ export async function reapIfStale(
|
|
|
823
966
|
* before it can claim), the row stays `background`, never heartbeats again, and
|
|
824
967
|
* — because dispatch returned 202 — the foreground already returned the SSE
|
|
825
968
|
* stream, so the existing fast-fail inline fallback never engaged. The run would
|
|
826
|
-
* otherwise hang for the full
|
|
969
|
+
* otherwise hang for the full durable background window and then error opaquely.
|
|
827
970
|
*
|
|
828
971
|
* This reaps such a run EARLY and DISTINCTLY: a row that is still unclaimed
|
|
829
972
|
* (`dispatch_mode = 'background'`) past the tight `UNCLAIMED_BACKGROUND_RUN_GRACE_MS`
|
|
@@ -973,8 +1116,8 @@ export async function insertRunEvent(
|
|
|
973
1116
|
// run aborts at the same time the producer emits its final event.
|
|
974
1117
|
// Treat the second write as a no-op so the run completes cleanly.
|
|
975
1118
|
await client.execute({
|
|
976
|
-
sql: `INSERT INTO agent_run_events (run_id, seq, event_data) VALUES (?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
|
|
977
|
-
args: [runId, seq, eventData],
|
|
1119
|
+
sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
|
|
1120
|
+
args: [runId, seq, Date.now(), eventData],
|
|
978
1121
|
});
|
|
979
1122
|
}
|
|
980
1123
|
|
|
@@ -1040,8 +1183,8 @@ export async function getRunByThread(
|
|
|
1040
1183
|
await ensureRunTables();
|
|
1041
1184
|
const client = getDbExec();
|
|
1042
1185
|
const sql = options?.includeTerminal
|
|
1043
|
-
? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
|
|
1044
|
-
: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
|
|
1186
|
+
? `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage, error_code FROM agent_runs WHERE thread_id = ? ORDER BY started_at DESC LIMIT 1`
|
|
1187
|
+
: `SELECT id, thread_id, turn_id, status, started_at, heartbeat_at, completed_at, last_progress_at, dispatch_mode, terminal_reason, diag_stage, error_code FROM agent_runs WHERE thread_id = ? AND status = 'running' ORDER BY started_at DESC LIMIT 1`;
|
|
1045
1188
|
const { rows } = await client.execute({ sql, args: [threadId] });
|
|
1046
1189
|
if (rows.length === 0) return null;
|
|
1047
1190
|
const r = rows[0] as {
|
|
@@ -1056,7 +1199,15 @@ export async function getRunByThread(
|
|
|
1056
1199
|
dispatch_mode?: string | null;
|
|
1057
1200
|
terminal_reason?: string | null;
|
|
1058
1201
|
diag_stage?: string | null;
|
|
1202
|
+
error_code?: string | null;
|
|
1059
1203
|
};
|
|
1204
|
+
const canReconcileFromEvents =
|
|
1205
|
+
r.status === "running" ||
|
|
1206
|
+
(r.status === "errored" &&
|
|
1207
|
+
r.error_code === STALE_RUN_ERROR_EVENT.errorCode);
|
|
1208
|
+
if (canReconcileFromEvents && (await reconcileTerminalRunFromEvents(r.id))) {
|
|
1209
|
+
return getRunByThread(threadId, options);
|
|
1210
|
+
}
|
|
1060
1211
|
return {
|
|
1061
1212
|
id: r.id,
|
|
1062
1213
|
threadId: r.thread_id,
|
|
@@ -1212,6 +1363,12 @@ export async function reapAllStaleRuns(): Promise<number> {
|
|
|
1212
1363
|
AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
|
|
1213
1364
|
args: [now],
|
|
1214
1365
|
});
|
|
1366
|
+
for (const row of stale.rows) {
|
|
1367
|
+
const id = (row as { id?: unknown }).id;
|
|
1368
|
+
if (typeof id === "string") {
|
|
1369
|
+
await reconcileTerminalRunFromEvents(id);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1215
1372
|
const completedAt = Date.now();
|
|
1216
1373
|
const { rowsAffected } = await client.execute({
|
|
1217
1374
|
sql: `UPDATE agent_runs
|
|
@@ -1221,6 +1378,7 @@ export async function reapAllStaleRuns(): Promise<number> {
|
|
|
1221
1378
|
error_detail = ?,
|
|
1222
1379
|
terminal_reason = ?
|
|
1223
1380
|
WHERE status = 'running'
|
|
1381
|
+
AND ${terminalRunEventExclusionSql()}
|
|
1224
1382
|
AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
|
|
1225
1383
|
args: [
|
|
1226
1384
|
completedAt,
|
|
@@ -1272,6 +1430,12 @@ export async function cleanupOldRuns(
|
|
|
1272
1430
|
)`,
|
|
1273
1431
|
args: [now, cutoff],
|
|
1274
1432
|
});
|
|
1433
|
+
for (const row of stale.rows) {
|
|
1434
|
+
const id = (row as { id?: unknown }).id;
|
|
1435
|
+
if (typeof id === "string") {
|
|
1436
|
+
await reconcileTerminalRunFromEvents(id);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1275
1439
|
const completedAt = Date.now();
|
|
1276
1440
|
await client.execute({
|
|
1277
1441
|
sql: `UPDATE agent_runs
|
|
@@ -1280,7 +1444,9 @@ export async function cleanupOldRuns(
|
|
|
1280
1444
|
error_code = ?,
|
|
1281
1445
|
error_detail = ?,
|
|
1282
1446
|
terminal_reason = ?
|
|
1283
|
-
WHERE status = 'running'
|
|
1447
|
+
WHERE status = 'running'
|
|
1448
|
+
AND ${terminalRunEventExclusionSql()}
|
|
1449
|
+
AND started_at < ?`,
|
|
1284
1450
|
args: [
|
|
1285
1451
|
completedAt,
|
|
1286
1452
|
STALE_RUN_ERROR_EVENT.errorCode,
|
|
@@ -1299,6 +1465,7 @@ export async function cleanupOldRuns(
|
|
|
1299
1465
|
error_detail = ?,
|
|
1300
1466
|
terminal_reason = ?
|
|
1301
1467
|
WHERE status = 'running'
|
|
1468
|
+
AND ${terminalRunEventExclusionSql()}
|
|
1302
1469
|
AND ${livenessBasisSql()} < ${backgroundAwareStaleCutoffSql()}`,
|
|
1303
1470
|
args: [
|
|
1304
1471
|
completedAt,
|
|
@@ -1493,7 +1660,7 @@ async function appendTerminalRunEvent(
|
|
|
1493
1660
|
}
|
|
1494
1661
|
const nextSeq = last ? Number(last.seq ?? -1) + 1 : 0;
|
|
1495
1662
|
await client.execute({
|
|
1496
|
-
sql: `INSERT INTO agent_run_events (run_id, seq, event_data) VALUES (?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
|
|
1497
|
-
args: [runId, nextSeq, JSON.stringify(event)],
|
|
1663
|
+
sql: `INSERT INTO agent_run_events (run_id, seq, event_at, event_data) VALUES (?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`,
|
|
1664
|
+
args: [runId, nextSeq, Date.now(), JSON.stringify(event)],
|
|
1498
1665
|
});
|
|
1499
1666
|
}
|
|
@@ -37,9 +37,11 @@ import React, {
|
|
|
37
37
|
import { LLM_MISSING_CREDENTIALS_MESSAGE } from "../agent/engine/credential-errors.js";
|
|
38
38
|
import type { ReasoningEffort } from "../shared/reasoning-effort.js";
|
|
39
39
|
import {
|
|
40
|
+
getActiveRunActivityTool,
|
|
40
41
|
getActiveRun,
|
|
41
42
|
resolveReconnectAfterSeq,
|
|
42
43
|
setActiveRun,
|
|
44
|
+
updateActiveRunActivity,
|
|
43
45
|
updateActiveRunSeq,
|
|
44
46
|
} from "./active-run-state.js";
|
|
45
47
|
import {
|
|
@@ -553,6 +555,23 @@ function reconnectContentFollowKey(content: ContentPart[]): string {
|
|
|
553
555
|
return content.map(contentPartFollowKey).join("|");
|
|
554
556
|
}
|
|
555
557
|
|
|
558
|
+
export function reconnectActivityFallbackContent(
|
|
559
|
+
toolName: string | null | undefined,
|
|
560
|
+
): ContentPart[] {
|
|
561
|
+
const tool = toolName?.trim();
|
|
562
|
+
if (!tool) return [];
|
|
563
|
+
return [
|
|
564
|
+
{
|
|
565
|
+
type: "tool-call",
|
|
566
|
+
toolCallId: `reconnect-activity:${tool}`,
|
|
567
|
+
toolName: tool,
|
|
568
|
+
argsText: "",
|
|
569
|
+
args: {},
|
|
570
|
+
activity: true,
|
|
571
|
+
},
|
|
572
|
+
];
|
|
573
|
+
}
|
|
574
|
+
|
|
556
575
|
const RECOVERY_USER_MESSAGE_PREFIXES = [
|
|
557
576
|
"Continue from where you left off",
|
|
558
577
|
"Continue from where you stopped",
|
|
@@ -1395,6 +1414,9 @@ const AssistantChatInner = forwardRef<
|
|
|
1395
1414
|
const [runningActivityLabel, setRunningActivityLabel] = useState<
|
|
1396
1415
|
string | null
|
|
1397
1416
|
>(null);
|
|
1417
|
+
const [runningActivityTool, setRunningActivityTool] = useState<string | null>(
|
|
1418
|
+
null,
|
|
1419
|
+
);
|
|
1398
1420
|
// Delayed-reveal state for the activity label (see ACTIVITY_LABEL_REVEAL_DELAY_MS).
|
|
1399
1421
|
// `latest` holds the most recent activity label; `surfaced` flips true once the
|
|
1400
1422
|
// reveal timer fires; `timer` is the pending one-shot reveal.
|
|
@@ -1409,6 +1431,8 @@ const AssistantChatInner = forwardRef<
|
|
|
1409
1431
|
latestActivityLabelRef.current = null;
|
|
1410
1432
|
activityLabelSurfacedRef.current = false;
|
|
1411
1433
|
setRunningActivityLabel(null);
|
|
1434
|
+
setRunningActivityTool(null);
|
|
1435
|
+
updateActiveRunActivity(null);
|
|
1412
1436
|
}, []);
|
|
1413
1437
|
const [reconnectContent, setReconnectContent] = useState<ContentPart[]>([]);
|
|
1414
1438
|
// When stop is clicked during reconnect, keep content visible (don't wipe it)
|
|
@@ -1445,6 +1469,18 @@ const AssistantChatInner = forwardRef<
|
|
|
1445
1469
|
: isReconnecting && reconnectContent.length > 0
|
|
1446
1470
|
? "Reconnecting"
|
|
1447
1471
|
: "Thinking";
|
|
1472
|
+
const reconnectActivityContent = useMemo(
|
|
1473
|
+
() =>
|
|
1474
|
+
(isReconnecting || reconnectFrozen) && reconnectContent.length === 0
|
|
1475
|
+
? reconnectActivityFallbackContent(runningActivityTool)
|
|
1476
|
+
: [],
|
|
1477
|
+
[
|
|
1478
|
+
isReconnecting,
|
|
1479
|
+
reconnectFrozen,
|
|
1480
|
+
reconnectContent.length,
|
|
1481
|
+
runningActivityTool,
|
|
1482
|
+
],
|
|
1483
|
+
);
|
|
1448
1484
|
const lastBroadcastRunningRef = useRef(isRunning);
|
|
1449
1485
|
const tiptapRef = useRef<TiptapComposerHandle>(null);
|
|
1450
1486
|
// Stable ref to the "stop active run" action so addToQueue can abort
|
|
@@ -1700,11 +1736,14 @@ const AssistantChatInner = forwardRef<
|
|
|
1700
1736
|
|
|
1701
1737
|
reconnectRunIdRef.current = runId;
|
|
1702
1738
|
const afterSeq = resolveReconnectAfterSeq(threadId, runId);
|
|
1739
|
+
const storedActivityTool = getActiveRunActivityTool(threadId, runId);
|
|
1740
|
+
setRunningActivityTool(storedActivityTool);
|
|
1703
1741
|
setReconnectAfterSeq(afterSeq);
|
|
1704
1742
|
setActiveRun({
|
|
1705
1743
|
threadId,
|
|
1706
1744
|
runId,
|
|
1707
1745
|
lastSeq: afterSeq > 0 ? afterSeq - 1 : -1,
|
|
1746
|
+
...(storedActivityTool ? { activityTool: storedActivityTool } : {}),
|
|
1708
1747
|
});
|
|
1709
1748
|
setIsReconnecting(true);
|
|
1710
1749
|
setReconnectFrozen(false);
|
|
@@ -2407,7 +2446,10 @@ const AssistantChatInner = forwardRef<
|
|
|
2407
2446
|
const label =
|
|
2408
2447
|
typeof detail?.label === "string" ? detail.label.trim() : "";
|
|
2409
2448
|
if (!label) return;
|
|
2449
|
+
const tool = typeof detail?.tool === "string" ? detail.tool.trim() : "";
|
|
2410
2450
|
setIsAutoResuming(false);
|
|
2451
|
+
setRunningActivityTool(tool || null);
|
|
2452
|
+
updateActiveRunActivity(tool || null);
|
|
2411
2453
|
latestActivityLabelRef.current = label;
|
|
2412
2454
|
// Already past the delay → keep the visible label current.
|
|
2413
2455
|
if (activityLabelSurfacedRef.current) {
|
|
@@ -3549,6 +3591,13 @@ const AssistantChatInner = forwardRef<
|
|
|
3549
3591
|
reconnectContent.length > 0 && (
|
|
3550
3592
|
<ReconnectStreamMessage content={reconnectContent} />
|
|
3551
3593
|
)}
|
|
3594
|
+
{(isReconnecting || reconnectFrozen) &&
|
|
3595
|
+
reconnectAfterSeq > 0 &&
|
|
3596
|
+
reconnectActivityContent.length > 0 && (
|
|
3597
|
+
<ReconnectStreamMessage
|
|
3598
|
+
content={reconnectActivityContent}
|
|
3599
|
+
/>
|
|
3600
|
+
)}
|
|
3552
3601
|
{showRunningInUI && (
|
|
3553
3602
|
<RunningActivityStatus label={runningStatusLabel} />
|
|
3554
3603
|
)}
|
|
@@ -4,6 +4,13 @@ export interface ActiveRunState {
|
|
|
4
4
|
threadId: string;
|
|
5
5
|
runId: string;
|
|
6
6
|
lastSeq: number;
|
|
7
|
+
activityTool?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeActivityTool(toolName: unknown): string | null {
|
|
11
|
+
if (typeof toolName !== "string") return null;
|
|
12
|
+
const tool = toolName.trim();
|
|
13
|
+
return tool || null;
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
export function setActiveRun(state: ActiveRunState): void {
|
|
@@ -30,6 +37,29 @@ export function updateActiveRunSeq(seq: number): void {
|
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
export function updateActiveRunActivity(
|
|
41
|
+
toolName: string | null | undefined,
|
|
42
|
+
): void {
|
|
43
|
+
const state = getActiveRun();
|
|
44
|
+
if (!state) return;
|
|
45
|
+
const activityTool = normalizeActivityTool(toolName);
|
|
46
|
+
if (activityTool) {
|
|
47
|
+
setActiveRun({ ...state, activityTool });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { activityTool: _activityTool, ...nextState } = state;
|
|
51
|
+
setActiveRun(nextState);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getActiveRunActivityTool(
|
|
55
|
+
threadId: string,
|
|
56
|
+
runId: string,
|
|
57
|
+
): string | null {
|
|
58
|
+
const stored = getActiveRun();
|
|
59
|
+
if (stored?.threadId !== threadId || stored.runId !== runId) return null;
|
|
60
|
+
return normalizeActivityTool(stored.activityTool);
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
export function clearActiveRun(): void {
|
|
34
64
|
try {
|
|
35
65
|
sessionStorage.removeItem(STORAGE_KEY);
|
|
@@ -9,8 +9,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
9
9
|
import { useNavigate } from "react-router";
|
|
10
10
|
|
|
11
11
|
import { extensionPath } from "../../extensions/path.js";
|
|
12
|
+
import { THEME_VAR_NAMES } from "../../extensions/theme.js";
|
|
12
13
|
import { sendToAgentChat } from "../agent-chat.js";
|
|
13
14
|
import { agentNativePath } from "../api-path.js";
|
|
15
|
+
import { useAppearance } from "../appearance.js";
|
|
14
16
|
import {
|
|
15
17
|
Popover,
|
|
16
18
|
PopoverContent,
|
|
@@ -53,6 +55,22 @@ interface Extension {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
// Read the host app's *actual* computed theme values for the shared token set
|
|
59
|
+
// (THEME_VAR_NAMES). The iframe ships with a generic baked palette
|
|
60
|
+
// (getThemeVars); syncing the live values keeps embedded extensions visually
|
|
61
|
+
// identical to the surrounding app even when a template overrides the default
|
|
62
|
+
// palette (e.g. analytics uses a neutral-gray dark theme, not near-black).
|
|
63
|
+
function readHostThemeVars(): Record<string, string> {
|
|
64
|
+
if (typeof document === "undefined") return {};
|
|
65
|
+
const computed = getComputedStyle(document.documentElement);
|
|
66
|
+
const vars: Record<string, string> = {};
|
|
67
|
+
for (const name of THEME_VAR_NAMES) {
|
|
68
|
+
const value = computed.getPropertyValue(name).trim();
|
|
69
|
+
if (value) vars[name] = value;
|
|
70
|
+
}
|
|
71
|
+
return vars;
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
function serializeChatValue(value: unknown): string | undefined {
|
|
57
75
|
if (value === undefined || value === null) return undefined;
|
|
58
76
|
if (typeof value === "string") return value;
|
|
@@ -112,7 +130,11 @@ export function EmbeddedExtension({
|
|
|
112
130
|
onReadyRef.current?.();
|
|
113
131
|
};
|
|
114
132
|
const [height, setHeight] = useState<number>(initialHeight);
|
|
115
|
-
const [isDark, setIsDark] = useState(
|
|
133
|
+
const [isDark, setIsDark] = useState(
|
|
134
|
+
() =>
|
|
135
|
+
typeof document !== "undefined" &&
|
|
136
|
+
document.documentElement.classList.contains("dark"),
|
|
137
|
+
);
|
|
116
138
|
// (audit H4) Mirror ExtensionViewer's role-aware gating; deny-by-default until
|
|
117
139
|
// the iframe's render binding announcement arrives.
|
|
118
140
|
const bridgeContextRef = useRef<BridgePolicyContext>({
|
|
@@ -200,11 +222,16 @@ export function EmbeddedExtension({
|
|
|
200
222
|
readyFiredRef.current = false;
|
|
201
223
|
}, [extensionId, extension?.updatedAt]);
|
|
202
224
|
|
|
225
|
+
const appearance = useAppearance();
|
|
226
|
+
|
|
203
227
|
useEffect(() => {
|
|
204
228
|
const win = iframeRef.current?.contentWindow;
|
|
205
229
|
if (!win) return;
|
|
206
|
-
win.postMessage(
|
|
207
|
-
|
|
230
|
+
win.postMessage(
|
|
231
|
+
{ type: "agent-native-theme-update", isDark, vars: readHostThemeVars() },
|
|
232
|
+
"*",
|
|
233
|
+
);
|
|
234
|
+
}, [isDark, appearance]);
|
|
208
235
|
|
|
209
236
|
// Forward slot context whenever it changes. The iframe's own load handler
|
|
210
237
|
// posts the initial value once it's ready; this effect handles updates.
|
|
@@ -372,6 +399,17 @@ export function EmbeddedExtension({
|
|
|
372
399
|
{ type: "agent-native-slot-context", context: context ?? {} },
|
|
373
400
|
"*",
|
|
374
401
|
);
|
|
402
|
+
// Re-assert theme once the iframe document is live. The src bakes in
|
|
403
|
+
// the initial dark state, but this covers the race where isDark
|
|
404
|
+
// settled before the iframe's message listener existed.
|
|
405
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
406
|
+
{
|
|
407
|
+
type: "agent-native-theme-update",
|
|
408
|
+
isDark,
|
|
409
|
+
vars: readHostThemeVars(),
|
|
410
|
+
},
|
|
411
|
+
"*",
|
|
412
|
+
);
|
|
375
413
|
// Fallback readiness signal in case the extension never reports a
|
|
376
414
|
// height (e.g. fixed-height content that skips auto-resize).
|
|
377
415
|
fireReady();
|