Skip to content

Commit b78cde9

Browse files
authored
fix: harden framework and template workflows
Merge follow-up fixes with focused local validation. CI install failures were external Electron/ffmpeg artifact fetch failures.
1 parent 710e0ea commit b78cde9

44 files changed

Lines changed: 1288 additions & 533 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@agent-native/core": patch
3+
---
4+
5+
Keep the chat share popover mounted outside the overflow menu before opening it.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@agent-native/core": patch
3+
---
4+
5+
Preserve signup analytics attribution through Better Auth magic-link verification.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@agent-native/core": patch
3+
---
4+
5+
Hide the workspace destination notice inside workspace apps and present it as an amber warning banner elsewhere.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@agent-native/core": patch
3+
---
4+
5+
Retry transient chat completion persistence failures before handing off background continuations.

packages/code-agents-ui/src/CodeAgentsApp.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ export default function CodeAgentsApp({
10251025
}, [host]);
10261026

10271027
const loadRemoteConnectorStatus = useCallback(async () => {
1028-
if (!host.getRemoteConnectorStatus) return;
1028+
if (!isActive || !host.getRemoteConnectorStatus) return;
10291029
try {
10301030
const result = await withHostCallTimeout(
10311031
host.getRemoteConnectorStatus(),
@@ -1036,10 +1036,10 @@ export default function CodeAgentsApp({
10361036
} catch (err) {
10371037
setRemoteConnectorError(err instanceof Error ? err.message : String(err));
10381038
}
1039-
}, [host]);
1039+
}, [host, isActive]);
10401040

10411041
const loadHostMetadata = useCallback(async () => {
1042-
if (!host.getHostMetadata) return;
1042+
if (!isActive || !host.getHostMetadata) return;
10431043
try {
10441044
const result = await host.getHostMetadata();
10451045
setHostMetadata(result);
@@ -1049,7 +1049,7 @@ export default function CodeAgentsApp({
10491049
error: err instanceof Error ? err.message : String(err),
10501050
});
10511051
}
1052-
}, [host]);
1052+
}, [host, isActive]);
10531053

10541054
const runComputerSetupAction = useCallback(
10551055
async (action: CodeAgentComputerSetupAction) => {

packages/core/src/agent/production-agent.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
resolveSkillReferenceContent,
5252
runAgentLoop,
5353
runAgentLoopWithMainChatInternalContinuations,
54+
runCompletionCallbackWithDatabaseRetry,
5455
shouldChainBackgroundContinuation,
5556
MAX_IDENTICAL_TOOL_CALLS,
5657
MAX_SAME_ERROR_ACROSS_ARGUMENTS,
@@ -65,6 +66,24 @@ import type { ActiveRun } from "./run-manager.js";
6566
import { attachToolSearch, searchToolRegistry } from "./tool-search.js";
6667
import type { AgentChatEvent, RunEvent } from "./types.js";
6768

69+
describe("runCompletionCallbackWithDatabaseRetry", () => {
70+
it("retries transient database failures before giving up the completion boundary", async () => {
71+
const callback = vi
72+
.fn()
73+
.mockRejectedValueOnce({
74+
code: "ECHECKOUTTIMEOUT",
75+
message: "database checkout timed out",
76+
})
77+
.mockResolvedValue(undefined);
78+
const sleep = vi.fn(async (_ms: number) => {});
79+
80+
await runCompletionCallbackWithDatabaseRetry(callback, { sleep });
81+
82+
expect(callback).toHaveBeenCalledTimes(2);
83+
expect(sleep).toHaveBeenCalledWith(250);
84+
});
85+
});
86+
6887
function actionEntry(opts: {
6988
description?: string;
7089
readOnly?: boolean;

packages/core/src/agent/production-agent.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from "../action.js";
2121
import { readAppState } from "../application-state/script-helpers.js";
2222
import { isReadOnlyShellCommand } from "../coding-tools/index.js";
23-
import { getDbExec } from "../db/client.js";
23+
import { getDbExec, isTransientDatabaseError } from "../db/client.js";
2424
import { extensionIdFromPathname } from "../extensions/path.js";
2525
import { preUploadAttachments } from "../file-upload/pre-upload-attachments.js";
2626
import { isMcpActionResult } from "../mcp-client/app-result.js";
@@ -1188,6 +1188,35 @@ export async function resolveAgentOwnerEmail(
11881188
}
11891189

11901190
const MAX_RETRIES = 3;
1191+
const COMPLETION_DATABASE_RETRY_DELAYS_MS = [250, 750, 1_500] as const;
1192+
1193+
/**
1194+
* A transient database failure in the completion callback used to prevent a
1195+
* background continuation from ever being handed off. Keep the retry bounded
1196+
* and database-only so a permanent persistence error still fails loudly.
1197+
*/
1198+
export async function runCompletionCallbackWithDatabaseRetry(
1199+
callback: () => void | Promise<void>,
1200+
options?: { sleep?: (ms: number) => Promise<void> },
1201+
): Promise<void> {
1202+
const sleep =
1203+
options?.sleep ??
1204+
((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
1205+
1206+
for (let attempt = 0; ; attempt += 1) {
1207+
try {
1208+
await callback();
1209+
return;
1210+
} catch (error) {
1211+
const retryDelay = COMPLETION_DATABASE_RETRY_DELAYS_MS[attempt];
1212+
if (!isTransientDatabaseError(error) || retryDelay === undefined) {
1213+
throw error;
1214+
}
1215+
await sleep(retryDelay);
1216+
}
1217+
}
1218+
}
1219+
11911220
/**
11921221
* Retry budget override for `builder_gateway_error` — the no-detail Builder
11931222
* gateway fallback. Production data shows this code is almost never
@@ -9181,7 +9210,9 @@ export function createProductionAgentHandler(
91819210
options.onRunComplete || trackedProgressRunId
91829211
? async (run: ActiveRun) => {
91839212
try {
9184-
await options.onRunComplete?.(run, threadId);
9213+
await runCompletionCallbackWithDatabaseRetry(() =>
9214+
options.onRunComplete?.(run, threadId),
9215+
);
91859216
} catch (err) {
91869217
await completeTrackedProgressRun(run, err);
91879218
throw err;

packages/core/src/agent/run-manager.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,42 @@ describe("run manager soft timeout", () => {
24302430
consoleError.mockRestore();
24312431
});
24322432

2433+
it("does not advertise a continuation when completion persistence fails before handoff", async () => {
2434+
const consoleError = vi
2435+
.spyOn(console, "error")
2436+
.mockImplementation(() => {});
2437+
const events: AgentChatEvent[] = [];
2438+
const run = startRun(
2439+
"run-completion-boundary-failed",
2440+
"thread-completion-boundary-failed",
2441+
async (send) => {
2442+
send({ type: "text", text: "partial response" });
2443+
send({ type: "auto_continue", reason: "stream_ended" });
2444+
},
2445+
async () => {
2446+
throw new Error("thread_data write failed");
2447+
},
2448+
{ softTimeoutMs: 0 },
2449+
);
2450+
run.subscribers.add((event) => events.push(event.event));
2451+
2452+
await run.finalized;
2453+
2454+
expect(events).toContainEqual({
2455+
type: "error",
2456+
error: "Agent response could not be saved.",
2457+
});
2458+
expect(events).not.toContainEqual({
2459+
type: "auto_continue",
2460+
reason: "stream_ended",
2461+
});
2462+
expect(setRunTerminalReason).toHaveBeenCalledWith(
2463+
"run-completion-boundary-failed",
2464+
"completion_error",
2465+
);
2466+
consoleError.mockRestore();
2467+
});
2468+
24332469
it("normalizes missing SQL abort reasons to user aborts", async () => {
24342470
vi.mocked(getRunAbortState).mockResolvedValue({ aborted: true });
24352471
let abortReason: unknown;

packages/core/src/agent/run-manager.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,13 @@ function terminalReasonForRun(
675675
abortReason: string | undefined,
676676
completionError: unknown,
677677
): string {
678+
if (
679+
finalStatus !== "aborted" &&
680+
completionError &&
681+
terminalEvent?.type === "auto_continue"
682+
) {
683+
return "completion_error";
684+
}
678685
if (terminalEvent?.type === "auto_continue") {
679686
return terminalEvent.reason || "auto_continue";
680687
}
@@ -1633,13 +1640,18 @@ export function startRun(
16331640
: terminalEventForCompletion?.event.type === "error" ||
16341641
terminalEventForCompletion?.event.type === "missing_api_key"
16351642
? terminalEventForCompletion.event
1636-
: terminalEventForCompletion?.event.type === "auto_continue"
1643+
: terminalEventForCompletion?.event.type === "auto_continue" &&
1644+
run.continuationTerminalEvent
16371645
? // The run was checkpointed at a soft-timeout/loop boundary and
16381646
// is recoverable: the partial turn is in agent_run_events and
1639-
// the continuation run will re-attempt the thread_data save.
1647+
// the handed-off continuation run will re-attempt the
1648+
// thread_data save.
16401649
// Even though the completion save failed (finalStatus stays
16411650
// "errored" for SQL/diagnostics), re-emit the auto_continue so
1642-
// the client resumes instead of seeing a dead chat.
1651+
// the client resumes instead of seeing a dead chat. A
1652+
// pending auto_continue without this handoff marker is not
1653+
// recoverable: advertising it makes the client poll a dead
1654+
// run until it reports background_run_lost.
16431655
terminalEventForCompletion.event
16441656
: {
16451657
type: "error",

packages/core/src/client/AgentPanel.header.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ describe("AgentPanel header overflow actions", () => {
307307
expect(overflowMenu).toContain(
308308
"<DropdownMenuShortcut>{widenChatHint}</DropdownMenuShortcut>",
309309
);
310-
expect(overflowMenu.match(/deferAgentPanelOverlayOpen/g)).toHaveLength(2);
310+
expect(overflowMenu.match(/deferAgentPanelOverlayOpen/g)).toHaveLength(3);
311311
expect(overflowMenu).toContain('t("agentPanel.openFullView")');
312312
expect(overflowMenu).toContain("onSelect={onFullViewRequest}");
313313
expect(source).toContain("onFullViewRequest={onFullscreenRequest}");
@@ -324,10 +324,12 @@ describe("AgentPanel header overflow actions", () => {
324324
source.indexOf("const renderPageChatOverlay"),
325325
);
326326

327-
expect(overflowMenu).toContain('resourceType="chat_thread"');
328-
expect(overflowMenu).toContain('trigger="label-icon"');
329-
expect(overflowMenu).toContain('triggerClassName="w-full justify-start"');
327+
expect(overflowMenu).toContain("<IconShare3");
328+
expect(overflowMenu).toContain("setShareFromMenuOpen(true)");
329+
expect(overflowMenu).not.toContain('trigger="label-icon"');
330330
expect(overflowMenu).toContain("activeTabMessageCount <= 0");
331+
expect(source).toContain("defaultOpen={onCollapse && shareFromMenuOpen}");
332+
expect(source).toContain("onCollapse ? setShareFromMenuOpen : undefined");
331333
});
332334
});
333335

0 commit comments

Comments
 (0)