fix(server): default the runtime home to ~/.pylon-code everywhere - #197
Merged
Conversation
The CLI defaulted its base directory to `~/.t3`, which on most machines is T3 Code's install. Its database carries upstream's migration numbering, so a Pylon server that opened it recorded those ids as applied and then died on the first query. `resolveBaseDir` now defaults to `~/.pylon-code`, matching the desktop, and the dev runner's implicit home moves with it. There is deliberately no fallback: adopting another product's database is the hazard being removed. Instead, when a launch resolves the default base dir, finds no state there, and sees an older `~/.t3/userdata`, it logs one line naming that directory and how to keep it — `--base-dir` or `T3CODE_HOME`, both unchanged. It is silent when the user said where their state lives, for dev runs, and on every launch after the first, which creates `userdata` itself. Model: Claude Opus 5. Harness: Claude Code.
The desktop's SSH launcher hard-coded `$HOME/.t3` in the scripts it writes to the remote host: the launcher state directory, the `--base-dir` it passes to `serve`, the runtime file it probes to adopt an already-running server, and the base dir it pairs against. That pointed remote Pylon servers at T3 Code's install. All four move to `$HOME/.pylon-code`, and no generated script names `.t3` at all now. A host that already ran the old launcher keeps its data in `~/.t3`. The new launcher never reads the old state directory, so it starts a fresh server under `~/.pylon-code` on a newly picked port and pairs against that; the old launcher-managed server is not stopped by the new scripts and stays running until the host restarts or the user stops it. To keep the previous data, point the remote server at `~/.t3` yourself with `--base-dir` or `T3CODE_HOME`. Model: Claude Opus 5. Harness: Claude Code.
Moving the SSH launcher to `~/.pylon-code` left hosts that the old launcher had already set up running a server from `~/.t3` that nothing could reach: the new state dir cannot see it and the new stop script no longer points at it, so it sat on its port until the host restarted. The launch script now retires the old state dir once, before it reads any current state. It stops the server only when the old `managed` file says the launcher owned it — the same check `REMOTE_STOP_SCRIPT` makes, so a server the user started themselves is left alone — then clears the pid/port/managed files and renames the directory to `<dir>.migrated`, which keeps the old server.log and stops the block matching on the next launch. Every step is best effort; a host that will not let us clean up is still a host we launch on. One line goes to stderr, not the stdout the desktop parses as JSON, saying earlier state may remain under `~/.t3` and can be moved to `~/.pylon-code` to keep it. The "no `.t3` anywhere" assertion is replaced by one that checks what a script points a server at: every `STATE_DIR`, `DEFAULT_SERVER_HOME`, `DEFAULT_RUNTIME_FILE`, `PAIRING_BASE_DIR`, and `--base-dir` line must name `~/.pylon-code`. The cleanup block is deliberately absent from the stop, log-tail, and pairing scripts. Model: Claude Opus 5. Harness: Claude Code.
Two scripts under `apps/server/scripts` still assumed `~/.t3` was Pylon's, and the runtime home move turned both into hazards AGENTS.md already names. `migrate-dev-db.ts` defaulted its *source* database to `~/.t3/userdata/state.sqlite`, so seeding a worktree's dev database copied T3 Code's schema — carrying upstream's migration numbering — into a Pylon server that then dies on its first query. It now reads `~/.pylon-code` by default. `t3-sqlite-state.ts` keyed its "refuse to exec against the shared home" guard to `~/.t3`, so after the move it no longer protected Pylon's live install. It now refuses both homes: `~/.pylon-code` because it is Pylon's, and `~/.t3` because it may be someone else's. Reads are unaffected. Both take an injected `homeDir` so the new tests can prove the defaults without reading the developer's own home. Model: Claude Opus 5. Harness: Claude Code.
`sharedHome` was doing two jobs: it named the home the default `--source` is read from, and it was the sole entry in the destructive-write guard. Pointing it at `~/.pylon-code` moved both, which quietly removed the only thing refusing `--base-dir ~/.t3` — so the script would happily run removeDatabaseFiles over `~/.t3/userdata/state.sqlite` and its `-wal`/`-shm` siblings. That is a live T3 Code install on most machines. The two roles are now separate values. `sourceHome` decides where a default `--source` is read from and stays `~/.pylon-code`. `protectedHomes` decides which directories `--base-dir` may not name and lists both runtime homes, compared canonically so a symlink cannot slip past, exactly as `t3-sqlite-state.ts` does. Also refreshes four places still describing `~/.t3` as the source — the file header, the `--source` JSDoc and flag description, the command description — and the node:os diagnostics comment, and puts Pylon's name in `t3-sqlite-state.ts`'s missing-database error. Model: Claude Opus 5. Harness: Claude Code.
`auth`, `connect`, and `project` resolve their base dir through `resolveServerConfig`, so the hint fires on their `--json` paths too — and it fires before `runWithEnvironmentAuth` raises the log level for `quietLogs`, so that guard cannot suppress it. Going through `Effect.logInfo` put a sentence on the same stream as the JSON payload, ahead of it, breaking any parser reading `t3 auth pairing list --json` on a first run with `~/.t3/userdata` present. The hint now writes straight to stderr, like the SSH launcher's does, and is renamed `warnAboutLegacyRuntimeHome` so it is not casually routed back through the logger. The sink is injectable so tests never touch the real streams. The new test runs the real `auth pairing list --json` command against a throwaway HOME that meets the hint's conditions, captures the payload stream through TestConsole, and asserts the hint reached stderr while the payload is still parseable JSON with no trace of it. Both halves fail independently if the hint goes back through the logger. Model: Claude Opus 5. Harness: Claude Code.
The legacy cleanup validated the pid from the old launcher's state file with `kill -0` alone. A pid file outlives the process it names, and after a remote reboot that number is very likely to have been handed to something else the same user owns — which the block would then kill. `legacy_pid_is_stale_launcher_server` now reads the process arguments and requires both markers the old launcher's own invocation always carried, `serve --host 127.0.0.1` and `--base-dir $LEGACY_SERVER_HOME`, before any signal. No match, or no ps output, means the process is left alone; the directory is still retired either way. Also adds the `|| true` the hint printf was missing. It was the one step in a best-effort block that could abort the launch under `set -e`. Model: Claude Opus 5. Harness: Claude Code.
The WSL backend deliberately omits the Windows-side base dir so it uses its own home inside the distro, which means the runtime home default moved there too. An existing WSL install's projects and history stay behind in the distro's `~/.t3` and the backend starts empty at `~/.pylon-code`. Documents that third class of install alongside the WSL runtime notes, with the one command that carries the old state forward and the conditions for running it safely. Model: Claude Opus 5. Harness: Claude Code.
The migration command told people to move `~/.t3` onto `~/.pylon-code` and to only do it when `~/.pylon-code` did not exist. That precondition excludes exactly the readers the paragraph is for: the runtime installer creates `$HOME/.pylon-code/wsl-runtime` inside the distro, so the directory is already there for anyone who has launched a WSL backend once. Following the command anyway would have nested the old tree at `~/.pylon-code/.t3`, which nothing reads. It now moves `userdata` — the state itself — and keys off `~/.pylon-code/userdata` being absent, the same condition the in-product hint uses. Settings and secrets live inside `userdata` and come along; `caches` and `worktrees` sit beside it and deliberately stay, the latter because projects reference those paths absolutely. The hint text follows the same correction: it named the base directories where it meant the state directories, so it read as "move that directory to ~/.pylon-code" when the parent is not what should move. Model: Claude Opus 5. Harness: Claude Code.
`migrate-dev-db --base-dir ~/.t3` reported "Source database does not exist" whenever `~/.pylon-code` was not populated yet, because the source check ran first. Nothing destructive happened either way, but it answered a question the user had not asked and hid the one that mattered. The protected-home guard now runs ahead of it, so aiming at a live install is reported as exactly that. The covering test no longer creates a source at all, which is what pins the ordering. Model: Claude Opus 5. Harness: Claude Code.
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
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.
Problem
The desktop app has kept Pylon's state under
~/.pylon-codesince the fork, but two other launch paths still defaulted to~/.t3: thet3CLI (resolveBaseDirinapps/server/src/os-jank.ts) when neither--base-dirnorT3CODE_HOMEis set, and the desktop's SSH remote launcher, which hard-coded--base-dir "$HOME/.t3"for the server it starts and kept its own state under~/.t3/ssh-launch.AGENTS.mdnames~/.t3/userdataas T3 Code's database — with incompatible migration numbering — and forbids opening it from a Pylon server, so a barenpx t3on a machine that also runs T3 Code did exactly the forbidden thing. PR #187's user doc was the first to depend on the default and had to hedge around it.Fix
Every place a Pylon server or its data is opened by default now uses
~/.pylon-code.T3CODE_HOMEand--base-dirare unchanged (compatibility identifiers). There is deliberately no fallback to~/.t3, because it may be T3's data.resolveBaseDirdefaults to~/.pylon-code. When the default is used,<default>/userdatais absent, and~/.t3/userdataexists, the server writes one hint line to stderr telling the user to pass--base-dir ~/.t3(orT3CODE_HOME) or move~/.t3/userdatainto~/.pylon-code/userdata, which brings settings and secrets with it. It never fires when a base dir was given, including the desktop's bootstrap, and never for worktree dev runs. Review caught that the first version went through the Effect logger to stdout ahead of--jsonoutput; it now bypasses the logger and a test captures the JSON stream to prove it stays clean.$HOME/.pylon-codewith state in$HOME/.pylon-code/ssh-launch/…. On first launch after the upgrade, a best-effort legacy block finds the old~/.t3/ssh-launch/<key>state, stops the old launcher-managed server only ifpsconfirms the recorded pid is aserve --host 127.0.0.1 … --base-dir $HOME/.t3process (a reused pid is spared), renames the old state dir to.migrated, and prints the same hint to stderr. Every line in the block ends in|| true, and the test walks the generated scripts requiring that;sh -npasses on all five scripts.migrate-dev-db.tsseeds from~/.pylon-code/userdata/state.sqliteand refuses to rebuild either~/.t3or~/.pylon-code(review caught that moving the shared-home constant had dropped the~/.t3refusal — the guard is now a separate list and runs before the source-exists check).t3-sqlite-state.tsrefusesexecagainst both homes, canonical-path compared.remote-access.md,keybindings.md,environment-theme.md,observability.md,internals/scripts.md, thetest-pylon-appskill, andAGENTS.md's runtime-home glossary line.install.mdgains a WSL paragraph: the in-distro backend takes the new default too, and because~/.pylon-code/wsl-runtimealready exists there, the migration ismv ~/.t3/userdata ~/.pylon-code/userdata, guarded on the destination being absent. That command was run against three sandboxes (migration, populated destination refused, nothing to move).What happens to existing installs
~/.pylon-codeand print the hint; old data stays in~/.t3until moved. On a remote host whosepscannot report process arguments, the old server is left running (safe direction) while its state dir is retired; stop it by hand.install.mdparagraph.Verification
cli/, bin, dev-runner, both maintainer scripts);t3,@t3tools/ssh, and@t3tools/scriptstypechecks clean; lint, format,git diff --checkclean.DEFAULT_SERVER_HOMEfails the SSH assertion; restoring the single-home guard fails the refusal test; reverting the guard hoist fails the ordering test; the--jsontest fails on the pre-fix logger route.sleepholding the pid spared, directory retired, idempotent on re-run, exit 0 on a clean host.Follow-up once #180 lands: append the durable-facts record for this decision (drafted in the review report, not committed here because the file only exists on that branch).
Claude Fable 5 in Pylon.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.