diff --git a/packages/webapp/src/lody/local-bridge.ts b/packages/webapp/src/lody/local-bridge.ts
index aab701f0..0c675d60 100644
--- a/packages/webapp/src/lody/local-bridge.ts
+++ b/packages/webapp/src/lody/local-bridge.ts
@@ -618,6 +618,7 @@ export function publishLodyLocalBridge(
): () => void {
target.ipc = bridge.ipc;
target.__LODY_LOCAL_BRIDGE__ = true;
+ target.__BLITZ_BUILTIN_DEFAULT_MODE_IDS__ = { claude: "bypassPermissions" };
return () => {
// ONLY CLEAR THE GLOBAL IF IT IS STILL OURS.
//
@@ -628,6 +629,7 @@ export function publishLodyLocalBridge(
if (target.ipc === bridge.ipc) {
delete target.ipc;
delete target.__LODY_LOCAL_BRIDGE__;
+ delete target.__BLITZ_BUILTIN_DEFAULT_MODE_IDS__;
}
};
}
diff --git a/packages/webapp/src/lody/window-globals.d.ts b/packages/webapp/src/lody/window-globals.d.ts
index 4292cc28..588bc65e 100644
--- a/packages/webapp/src/lody/window-globals.d.ts
+++ b/packages/webapp/src/lody/window-globals.d.ts
@@ -1,7 +1,7 @@
/**
- * The two window globals the Lody local bridge owns.
+ * The three window globals the Lody local bridge owns.
*
- * `vendor/lody/packages/components/src/window-globals.d.ts` declares both, but
+ * The vendored components declare `ipc` and `__LODY_LOCAL_BRIDGE__`, but
* `vendor-modules.d.ts` deliberately keeps the vendor tree out of our
* typecheck, so its ambient declarations never reach us. These are the BlitzOS
* side of the same seam, and they are narrower on purpose: `ipc` is exactly the
@@ -30,6 +30,7 @@ declare global {
send: (channel: string, payload?: LodyIpcSendPayload) => void;
};
__LODY_LOCAL_BRIDGE__?: true;
+ __BLITZ_BUILTIN_DEFAULT_MODE_IDS__?: { claude?: string };
}
}
diff --git a/packages/webapp/test/lody-default-permission-mode.test.ts b/packages/webapp/test/lody-default-permission-mode.test.ts
new file mode 100644
index 00000000..84303974
--- /dev/null
+++ b/packages/webapp/test/lody-default-permission-mode.test.ts
@@ -0,0 +1,37 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { getBuiltinDefaultModeId } from "@lody/shared/ai";
+import {
+ createLodyLocalBridge,
+ publishLodyLocalBridge,
+} from "../src/lody/local-bridge.js";
+
+afterEach(() => {
+ delete window.ipc;
+ delete window.__LODY_LOCAL_BRIDGE__;
+ delete window.__BLITZ_BUILTIN_DEFAULT_MODE_IDS__;
+});
+
+describe("the builtin permission-mode host override", () => {
+ it("uses the BlitzOS Claude default only while its local bridge is published", () => {
+ expect(getBuiltinDefaultModeId("builtin", "claude")).toBe("auto");
+
+ const bridge = createLodyLocalBridge({
+ syncUrl: "wss://box.invalid/webapp/7445/lody/sync",
+ rpcUrl: "https://box.invalid/webapp/7445/lody/rpc",
+ controlUrl: "https://box.invalid/webapp/7445/lody/control",
+ projectUrl: "https://box.invalid/webapp/7445/lody/project",
+ platformUrl: "https://box.invalid/webapp/7445/lody/platform",
+ filesBase: "https://box.invalid/webapp/5000/",
+ });
+ const unpublish = publishLodyLocalBridge(bridge);
+ try {
+ expect(getBuiltinDefaultModeId("builtin", "claude")).toBe("bypassPermissions");
+ expect(getBuiltinDefaultModeId("builtin", "codex")).toBe("agent-auto-review");
+ } finally {
+ unpublish();
+ bridge.dispose();
+ }
+
+ expect(getBuiltinDefaultModeId("builtin", "claude")).toBe("auto");
+ });
+});
diff --git a/packages/webapp/test/lody-unsent-run-config.test.tsx b/packages/webapp/test/lody-unsent-run-config.test.tsx
new file mode 100644
index 00000000..184d2ed0
--- /dev/null
+++ b/packages/webapp/test/lody-unsent-run-config.test.tsx
@@ -0,0 +1,88 @@
+import { act } from "react";
+import { describe, expect, it } from "vitest";
+import { useAcpSessionConfigSelectionState } from "@lody/components/hooks/use-acp-session-config-selection";
+import { render } from "./dom.js";
+
+function Harness({
+ targetKey,
+ preserveUnsentUserEdits,
+ preferenceModeId,
+}: {
+ targetKey: string;
+ preserveUnsentUserEdits?: boolean;
+ preferenceModeId?: string;
+}) {
+ const selection = useAcpSessionConfigSelectionState({
+ targetKey,
+ preferenceRevision: `revision:${preferenceModeId ?? "none"}`,
+ preferences: preferenceModeId === undefined ? {} : { modeId: preferenceModeId },
+ preserveUnsentUserEdits,
+ });
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function userMode(container: HTMLElement): string | undefined {
+ return container.querySelector("[data-user-mode]")?.textContent ?? undefined;
+}
+
+async function pickMode(container: HTMLElement): Promise {
+ await act(async () => container.querySelector("button")?.click());
+}
+
+describe("unsent run-configuration retention", () => {
+ it("retains only opted-in edits per target and drops captured preferences", async () => {
+ let view = await render();
+ await pickMode(view.container);
+ await view.unmount();
+ view = await render();
+ expect(userMode(view.container)).toBe("bypassPermissions");
+ await view.unmount();
+
+ view = await render();
+ await pickMode(view.container);
+ await act(async () => view.root.render(
+ ,
+ ));
+ expect(userMode(view.container)).toBe("none");
+ await act(async () => view.root.render(
+ ,
+ ));
+ expect(userMode(view.container)).toBe("bypassPermissions");
+ await view.unmount();
+
+ const retentionCases: Array = [
+ ["absent-retention", undefined],
+ ["false-retention", false],
+ ];
+ for (const [targetKey, preserveUnsentUserEdits] of retentionCases) {
+ view = await render(
+ ,
+ );
+ await pickMode(view.container);
+ await view.unmount();
+ view = await render(
+ ,
+ );
+ expect(userMode(view.container)).toBe("none");
+ await view.unmount();
+ }
+
+ view = await render();
+ await pickMode(view.container);
+ await view.unmount();
+ view = await render(
+ ,
+ );
+ expect(userMode(view.container)).toBe("none");
+ await view.unmount();
+ });
+});
diff --git a/vendor/lody/BLITZ-PATCHES.md b/vendor/lody/BLITZ-PATCHES.md
index 13a82f29..d5b8e4b0 100644
--- a/vendor/lody/BLITZ-PATCHES.md
+++ b/vendor/lody/BLITZ-PATCHES.md
@@ -2162,6 +2162,50 @@ the same file fallback and limit rule.
hunks. If the landing gains the prop itself, keep only the host wiring. If
image and file limits merge, preserve file access when cloud upload is absent.
+### 28. A host may name its own builtin default mode (declared 2026-09-05)
+
+**One idea, two hunks, one file.** Lody chooses `auto` for new builtin Claude
+sessions. BlitzOS requires `bypassPermissions` for those sessions. Without this
+seam, the Lody table overrides the host's agent configuration.
+
+`packages/shared/src/ai.ts`
+
+| # | File | Line (at `f4b1ba25`) | Upstream anchor | What it does |
+|---|---|---|---|---|
+| 1 | `packages/shared/src/ai.ts` | 405 | immediately after the `BUILTIN_DEFAULT_MODE_IDS` object | declares an optional host override on `globalThis` |
+| 2 | same | 412 | the builtin branch in `getBuiltinDefaultModeId` | prefers the host value before the Lody value |
+
+The override is optional. An absent value preserves every upstream default.
+The BlitzOS bridge publishes one Claude override. Its guarded disposer removes
+that override during the existing bridge hand-over.
+
+Open upstream as "allow an embedding host to name builtin mode defaults".
+
+### 29. An unsent run-config pick survives leaving the session (declared 2026-09-05)
+
+**One idea, four hunks, one file.** A session picker stores unsent run
+configuration only in mounted React state. Leaving the session unmounts that
+state. Without this seam, returning restores only the latest sent turn.
+
+`packages/components/src/hooks/use-acp-session-config-selection.ts`
+
+| # | File | Line (at `f4b1ba25`) | Upstream anchor | What it does |
+|---|---|---|---|---|
+| 1 | `packages/components/src/hooks/use-acp-session-config-selection.ts` | 1 | the React hook import | imports `useEffect` for cleanup only |
+| 2 | same | 74 | after `AcpSessionConfigSelectionHandle` | adds a bounded cache for non-empty unsent edits |
+| 3 | same | 92 | the render-phase target fence | stashes the outgoing edits, then seeds the incoming target from the cache |
+| 4 | same | 101 | after the fence, above the preference-stabilizing refs | holds the latest fence in a ref, and stashes it from one cleanup-only effect |
+
+Only callers passing `preserveUnsentUserEdits: true` use the cache. Other
+callers keep the upstream behavior. The cache retains eight targets and evicts
+the oldest target first. Incoming edits pass through the existing fence. The
+fence drops a pick when durable preferences capture it.
+
+The cleanup writes only the module cache. It cannot update or oscillate React
+state.
+
+Open upstream as "retain opted-in unsent run configuration across session mounts".
+
## Retired compiled-bundle patches
| Retired script | Disposition | Evidence |
diff --git a/vendor/lody/packages/components/src/hooks/use-acp-session-config-selection.ts b/vendor/lody/packages/components/src/hooks/use-acp-session-config-selection.ts
index 7a31af48..6b6735dc 100644
--- a/vendor/lody/packages/components/src/hooks/use-acp-session-config-selection.ts
+++ b/vendor/lody/packages/components/src/hooks/use-acp-session-config-selection.ts
@@ -1,4 +1,4 @@
-import { useCallback, useMemo, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { AcpConfigOptionValue } from '@lody/shared';
import {
areAcpSessionConfigPreferencesEqual,
@@ -74,6 +74,21 @@ export type AcpSessionConfigSelectionHandle = {
replaceConfigOptions: (values: Record) => void;
};
+// Blitz seam 29: see vendor/lody/BLITZ-PATCHES.md.
+const retainedUnsentUserEdits = new Map();
+
+function retainUnsentUserEdits(targetKey: string | null, edits: AcpSessionUserConfigEdits): void {
+ if (targetKey === null) return;
+ retainedUnsentUserEdits.delete(targetKey);
+ if (!edits.mode && !edits.model && Object.keys(edits.configOptions).length === 0) return;
+ retainedUnsentUserEdits.set(targetKey, edits);
+ if (retainedUnsentUserEdits.size <= 8) return;
+ for (const oldestTargetKey of retainedUnsentUserEdits.keys()) {
+ retainedUnsentUserEdits.delete(oldestTargetKey);
+ break;
+ }
+}
+
export function useAcpSessionConfigSelectionState({
enabled = true,
targetKey,
@@ -92,17 +107,37 @@ export function useAcpSessionConfigSelectionState({
enabled &&
(fence.targetKey !== targetKey || fence.preferenceRevision !== preferenceRevision)
) {
+ const targetChanged = fence.targetKey !== targetKey;
+ if (targetChanged && preserveUnsentUserEdits) {
+ retainUnsentUserEdits(fence.targetKey, fence.edits);
+ }
+ const seededEdits =
+ targetChanged && preserveUnsentUserEdits
+ ? targetKey === null
+ ? EMPTY_ACP_SESSION_USER_CONFIG_EDITS
+ : (retainedUnsentUserEdits.get(targetKey) ?? EMPTY_ACP_SESSION_USER_CONFIG_EDITS)
+ : fence.edits;
setFence({
targetKey,
preferenceRevision,
- edits: fenceAcpSessionUserEdits(fence.edits, {
- targetChanged: fence.targetKey !== targetKey,
+ edits: fenceAcpSessionUserEdits(seededEdits, {
+ targetChanged: targetChanged && !preserveUnsentUserEdits,
preserveUnsentUserEdits,
preferences,
}),
});
}
+ const latestFenceRef = useRef({ fence, preserveUnsentUserEdits });
+ latestFenceRef.current = { fence, preserveUnsentUserEdits };
+ useEffect(() => () => {
+ const latest = latestFenceRef.current;
+ if (latest.preserveUnsentUserEdits) {
+ retainUnsentUserEdits(latest.fence.targetKey, latest.fence.edits);
+ }
+ // This cleanup only writes the module cache, so it cannot oscillate React state.
+ }, []);
+
/* VALUE-stabilize the preference inputs. `preferences`/`runtimePreferences`
are object literals resolved from `sessionDoc.history`, and the doc mirror
rebuilds `history` with unchanged values on every merge frame while an
diff --git a/vendor/lody/packages/shared/src/ai.ts b/vendor/lody/packages/shared/src/ai.ts
index 960b9bf4..164aa89c 100644
--- a/vendor/lody/packages/shared/src/ai.ts
+++ b/vendor/lody/packages/shared/src/ai.ts
@@ -404,6 +404,13 @@ const BUILTIN_DEFAULT_MODE_IDS: Record = {
deepseek: 'workspace-write',
};
+declare global {
+ // Blitz seam 28: see vendor/lody/BLITZ-PATCHES.md.
+ var __BLITZ_BUILTIN_DEFAULT_MODE_IDS__:
+ | Partial>
+ | undefined;
+}
+
/**
* Lody-owned mode default for builtin agents when a turn has no
* persisted selection. Capability-aware callers should use it only when the
@@ -414,7 +421,8 @@ export const getBuiltinDefaultModeId = (
agentType: AgentType | null | undefined
): string | undefined =>
cliType === 'builtin' && agentType && isBuiltinAgentType(agentType)
- ? BUILTIN_DEFAULT_MODE_IDS[agentType]
+ ? (globalThis.__BLITZ_BUILTIN_DEFAULT_MODE_IDS__?.[agentType] ??
+ BUILTIN_DEFAULT_MODE_IDS[agentType])
: undefined;
const DEEPSEEK_HARNESS_CONFIG_OPTIONS: AcpConfigOptionSummary[] = [