Skip to content

[DO NOT MERGE] dev: #441 + #477 + #478 + #479 + #483 for local testing - #480

Draft
abate wants to merge 17 commits into
devsuitup:mainfrom
abate:dev/lazy-restore-clear-rekey
Draft

abate wants to merge 17 commits into
devsuitup:mainfrom
abate:dev/lazy-restore-clear-rekey

Conversation

@abate

@abate abate commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Local test build only, not for merge: current main (eaf59ff) with the heads of #483 (55ad25a), #441 (f4bd11b), #479 (00783c2), #478 (150b213) and #477 (4a670ad) merged in that order. The merges only resolve overlaps: test harness declarations shared by #441, #477 and #479, the onUnsavedCheck handler shared by #478 and #479, and the CHANGELOG and docs.

🤖 Generated with Claude Code

@abate
abate force-pushed the dev/lazy-restore-clear-rekey branch from 86d05f8 to 620070a Compare October 5, 2026 10:59
@abate abate changed the title [DO NOT MERGE] dev: #441 + #477 + #478 + #479 for local testing [DO NOT MERGE] dev: #441 + #477 + #478 + #479 + #483 for local testing Oct 5, 2026
@abate
abate force-pushed the dev/lazy-restore-clear-rekey branch from a0397b4 to c0609de Compare October 5, 2026 15:15
abate added 17 commits October 8, 2026 11:16
Skills and agents are read-only in a sandboxed session because an
unsandboxed claude loads them later. Some workflows need the session to
write them, so SWITCHBOARD_SANDBOX_RW_SKILLS=1 and
SWITCHBOARD_SANDBOX_RW_AGENTS=1 (Pre-launch Command) make them session
state in ~/.claude and every bound .claude. Both stay off by default;
docs/sandbox.md states the protection given up.
Review of devsuitup#483: with SWITCHBOARD_SANDBOX_RW_SKILLS/AGENTS, a skills or
agents directory linked to a repository elsewhere was bound writable
without the protections other writable roots get, so the session could
write that repository's hooks or a settings file linked into it.

- An opted-in link target is a writable root, so files linked into it
  from ~/.claude are mounted read-only.
- The tree under an opted-in entry, real or linked, gets the same .git
  and .claude protection as a bound directory, without creating .claude.
- The Sandbox tooltip names the opt-in among what is not isolated.
- The test rig drops inherited SWITCHBOARD_SANDBOX_* variables, which
  leaked into the wrapper when the suite ran from a sandboxed session.
…d-only path

Linked to the same directory as hooks (or agents, commands), or inside a
read-only target such as hooks, plugins or a repository's hooks directory,
the writable bind of skills or agents overrode the read-only one and left
the protected files writable. The finished mount list is now checked and
such a launch refused, whatever order the entries were bound in.
Restoring the working set resumes a claude process per session at
startup, which costs a lot of memory. The new 'lazy' mode marks last
run's sessions in the sidebar (outlined dot, always visible) and only
resumes each one when it is clicked. Unopened sessions stay in the
persisted working set across restarts.
Review of devsuitup#441:
- openSession clears the dormant marker only after openTerminal succeeds,
  so a resume refused by guardResume or a failed open keeps the entry
  dormant; persistWorkingSet keeps a dormant or skipped entry whose open
  entry is closed.
- "Don't restore" in a dormant session's context menu removes it from
  the saved set without spawning claude.
- restore-lazy tests load the shipped functions with loadAppFunctions
  next to the real sidebar.js, and cover the refused resume, the failed
  open, the .dormant class, the visibility bypass and the dismiss.
- docs/session-restore.md and docs/settings.md describe Restore on click,
  including the empty grid and no selected session at launch.
With Restore on click, a reload resumed the remembered session without a
click, an exited one included: it is now reopened only while its PTY still
runs, which is a reattach. Saved sessions the planner has not marked yet
are kept by the persist through pendingRestoreEntries, now on main. A
project folded for its age opens to show its dormant rows, deleting a
dormant session drops it from the saved set, and the previous/next session
keys reach dormant rows outside the grid.
The working set was written on a 500 ms debounce, so the last change before
a quit could be lost, and the shutdown's own PTY kills arrived as session
exits that could save an empty set. A confirmed close or quit now writes the
set at once before answering main, then stops saving it; main stops sending
process-exited once before-quit kills the sessions.
…rted one

The final write skipped a restore in progress, so a session stopped during
it came back, and wrote [] before the saved set was even restored. It now
writes through pendingRestoreEntries, which keeps the sessions a cold index,
the Restore prompt or the restore still hold, and writes nothing before the
saved set is read. A save asked during the grace period of an exit that did
not happen is written when it ends. A Windows logoff asks the renderer to
save, best effort.
The close button sits next to the strip's controls, and a stray click
stopped every session. The window's own close now asks the renderer with
reason 'close', which adds a confirm unless the unsaved-edits dialog has
already been answered. Quit from the menu or the updater is not asked again.
Once the renderer had acknowledged the check, nothing ended it: a page that
hung after the ack held the close and every later quit, SIGTERM included.
A close or quit that repeats an acknowledged check now pings the page, and
with no answer within 1.5 s ends the check and destroys the window, since a
close would wait on the hung page's beforeunload. The 2.5 s bound before the
ack destroys it too. The close question is now the in-page choice dialog,
with Cancel focused, so Enter keeps the app and the page stays able to answer.
… it apart

/clear makes the CLI open a new jsonl under a new session id that names
nothing of the one it replaced, so fork detection never matched it: the
terminal stayed on the old row and the new conversation appeared as a
separate, unattached session. The CLI's state file switches to the new id
on /clear; resolve its pid up to the PTY's and re-key the session like a fork.
…ins on it

An owner that cannot be established (no /proc: macOS, Windows) was matched
when one Claude PTY ran in the folder, so a claude run outside the app could
be taken over; it is now never matched. A re-key kept no trace of the old id,
so a trigger chain that sent /clear lost its terminal: the trigger context
now resolves ids through the re-keys. A pending owner, or a new file whose
first records are not written yet, is rechecked a second later while fresh,
instead of waiting for another change in the folder. The snapshot-only fork
matcher no longer takes a /clear file. A session with no transcript yet is
left out of the saved set, which is saved again once its first prompt makes
it real.
# Conflicts:
#	test/restore-pending-persist.test.js
…ekey

# Conflicts:
#	public/file-panel.js
#	test/dom-file-panel-unsaved-guard.test.js
# Conflicts:
#	CHANGELOG.md
#	docs/session-restore.md
@abate
abate force-pushed the dev/lazy-restore-clear-rekey branch from bf902c9 to ff3caa4 Compare October 8, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant