fix(lody): new Claude sessions start in Bypass, and a permission pick survives a session switch - #246
Merged
Merged
Conversation
… survives a session switch
Two faults in the composer's permission selector.
A new Claude session started in Auto. `BUILTIN_DEFAULT_MODE_IDS.claude` is
`auto`, and our agent-config rows are `cliType: "builtin"`, so the Lody table
won. The `bypassPermissions` pins in this repo reach only the terminal and
Remote Control.
A permission pick was also lost when the member left the session. `selectMode`
wrote an unsent edit into React state alone. A target change emptied it, and a
sidebar navigation unmounted the surface. The picker then fell back to the
latest SENT turn, so the old mode came back.
Seam 28 lets a host name its own builtin default mode. The override is absent by
default, so upstream behaviour does not change. The BlitzOS bridge publishes
`{ claude: "bypassPermissions" }` and its existing disposer removes it.
Seam 29 retains unsent run-config edits per target in a bounded module cache.
Only a caller that passes `preserveUnsentUserEdits` uses it. A restored pick
passes through the existing fence, so a pick the durable preference captured is
dropped.
Both seams are declared in vendor/lody/BLITZ-PATCHES.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZvBaFqU4mgX75HAbiYJYV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What part this touches
The chat surface in the webapp is Lody. We vendor Lody as a git subtree at
vendor/lody. The composer at the bottom of a session carries a permissionselector. It offers Auto, Manual, Accept Edits, Plan, Don't Ask and Bypass
Permissions.
Three parts decide which mode a session runs:
use-acp-session-config-selection.ts.document.
packages/shared/src/ai.ts.The selector reads them in this order: an unsent pick, then the agent-confirmed
runtime value, then the latest sent turn, then the built-in default.
BlitzOS only edits
vendor/lodyat a seam declared invendor/lody/BLITZ-PATCHES.md. This PR declares seams 28 and 29.The bug
Two faults, both reported from the product.
One: a new Claude session does not start in Bypass Permissions.
BUILTIN_DEFAULT_MODE_IDSsetsclaude: 'auto'(
vendor/lody/packages/shared/src/ai.ts:402). Our agent-config rows usecliType: "builtin", sogetBuiltinDefaultModeIdreturns that value. Theexisting
bypassPermissionspins in this repo cover only the terminal(
packages/box/rootfs/usr/local/bin/blitz-term:38) and Remote Control. Theynever reach the Lody composer.
Two: a permission pick is lost when you leave the session.
selectModewrites an unsent edit into React state, and nothing else. Two things then erase
it.
fenceAcpSessionUserEditsreturns an empty set when the target key changes(
vendor/lody/packages/components/src/lib/acp-session-config-selection.ts:71).A sidebar navigation also unmounts the surface, which destroys the state.
The picker then falls back to the durable value. That value comes only from the
latest SENT turn
(
vendor/lody/packages/shared/src/session-input.ts:141). So the member seesthis: pick Bypass, switch session, come back, and the selector reads Auto again.
The fault looks intermittent, and it is not. Pick and SEND, and the mode sticks.
Pick and LEAVE, and the mode is gone.
The fix
Seam 28 — a host may name its own builtin default mode. Two hunks in
vendor/lody/packages/shared/src/ai.ts.getBuiltinDefaultModeIdreads an optional host override before the Lodytable.
publishLodyLocalBridgepublishes{ claude: "bypassPermissions" }besidethe
__LODY_LOCAL_BRIDGE__flag it already sets.Seam 29 — an unsent run-config pick survives leaving the session. Four hunks
in
use-acp-session-config-selection.ts.preserveUnsentUserEdits: trueuses the cache.draft-session-chat-interface.tsxdoes not pass it, so it keeps today'sbehaviour.
from the cache.
when the session's durable preference has captured it.
The risk trade
Bypass Permissions removes the human approval step for a new Claude session. The
member can still pick another mode, and that pick now survives. A box is an
isolated VM, and the terminal already runs Claude with
--dangerously-skip-permissions, so this makes the composer agree with theterminal.
The seams cost upstream merge burden. Both are small and default-inert: 6 added
lines in
ai.tsand 41 in one hook file. Absent host configuration leavesupstream behaviour identical, so a conflict resolves by reapplying one predicate.
The module cache is process state, not React state. Its cleanup writes only the
cache and never calls
setState, so it cannot restart the nested-update loopthat the hook's header comment describes.
The rejected alternative: push the pick to the daemon at pick time, through a
new
set-modeRPC that writes the runtime-config snapshot. That fixes more —the mode would reach the live agent without a turn — but it needs a new daemon
verb on a cross-runtime contract.
applyAcpRuntimeConfigPatchalso refuses topersist for a session that has no user turn yet
(
vendor/lody/apps/cli/src/lib/loro/doc.ts:2222), so a pick before the firstmessage still had nowhere to live.
Tests
Two new files in
packages/webapp/test/, 2 tests.lody-default-permission-mode.test.ts—getBuiltinDefaultModeIdreturnsautobefore publish,bypassPermissionswhile the bridge is published, andautoagain after the disposer runs. The Codex default is untouched.lody-unsent-run-config.test.tsx— drives the real hook. A pick survives anunmount and remount. A switch to another target shows no pick. A switch back
restores it. A caller without
preserveUnsentUserEditsretains nothing. Apick that the preferences have captured is dropped.
Both tests fail when the source change is reverted. Measured:
Gates:
typecheck— schema, webapp and box guest tests pass. Control-plane passesafter its bindings are regenerated from
wrangler.toml.example; the stalelocal
wrangler.tomlis machine noise, not a finding.lint:gate— passed. 43 anti-slop findings, 0 blitz-house findings, everyper-rule count equal to the baseline. 8 max-lines warnings, unchanged.
npm test -w @blitzos/webapp— 120 files passed, 12 skipped. 1008 testspassed, 1 expected fail, 62 skipped.
git diff -- tools/oxlint/anti-slopis empty.One test on this branch is intermittent, and it is not this change:
lody-terminal-tab-wave3.test.tsx > F7 … is inert on an address with no session to be missing. Six full-suite runs isolate it.origin/mainThe test asserts a pathname after
settle(), which flushes exactly onemacrotask tick (
test/dom.tsx:25). Its own file records that a sibling test inthe same
describewas deleted for the same reason: it "failed one full-suiterun and passed the next two, while passing every time this file ran alone". This
branch does not touch that code. I left the test alone on purpose. Making it
settle longer could hide a real race in
useLodyRail, and that deserves its ownchange.
Deploy
The webapp ships inside the control-plane Worker.
npm run deploy -w packages/control-planebuilds it (.github/workflows/canary.yml:477). A mergeto
maintherefore deploys this to canary with no other step. No box image andno payload change: this is browser code.
# canary, automatic on merge to main npm run deploy -w packages/control-planeRollback: revert the commit and merge. Canary redeploys the previous webapp
bundle. Members get it on the next page load.
🤖 Generated with Claude Code
https://claude.ai/code/session_019ZvBaFqU4mgX75HAbiYJYV