fix(agent-hooks): keep a dev run's endpoint file out of the installed profile - #272
Open
miadisabelle wants to merge 2 commits into
Open
fix(agent-hooks): keep a dev run's endpoint file out of the installed profile#272miadisabelle wants to merge 2 commits into
miadisabelle wants to merge 2 commits into
Conversation
… profile `getStateDir()` in persistence.ts and `getKeybindingsDir()` in register.ts carry the same rule, copied twice: take `userData`, append `-dev` when the build is not packaged, so `npm run dev` does not read or write an installed build's data. `agent-hooks/runtime.ts` joins raw `userData` and gets no such separation. That directory holds `endpoint.env` — the loopback port and the bearer token the generated hook script posts with. Both instances write it, and it is written only inside `startAgentHookServer`, so: - Whichever instance started last owns the file. Every Claude agent launched outside Docker re-sources it on every hook event, so agents belonging to the *other* instance post their status there. The token comes from the same file, so the request authenticates and succeeds — an instance reports status for agents it never launched, with nothing logged on either side. - When that instance quits, the file still names its dead port. Nothing rewrites it until an app starts again, so the surviving instance's agents post into nothing and fall back to PTY heuristics until that instance is itself restarted. Rather than add a third copy of the suffix rule, extract it: `electron/user-data-dir.ts` exports `resolveUserDataDir(userDataPath, isPackaged)` — pure, so the rule is testable without an Electron runtime — and `getUserDataDir()` for callers. persistence.ts (state.json and custom themes), the keybindings handlers and agent-hooks now all go through it, and reaching past it is the thing to look for in review. Packaged builds resolve exactly as before: `resolveUserDataDir` returns its input unchanged when `isPackaged`. 3 unit tests on the rule. `npm run compile`, `npm run check:static` and the full suite pass.
Same class as the agent-hooks fix, found while making that one: SaveArenaData
and LoadArenaData join `app.getPath('userData')` directly, so a dev run reads
and writes the installed build's `arena-*.json`.
No silent misrouting here the way the hook endpoint had — it is the plainer
version of the same slip. Worth knowing before merging: a dev run picks this
change up as an empty arena, since `arena-presets.json` and
`arena-history.json` stay with the installed build.
Separate commit because it is not the bug that prompted the change; drop it if
you would rather keep this PR to agent-hooks.
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.
The dev-instance cross-wiring you spotted reviewing #270. Your diagnosis was right; two things about it turned out to be worse than the comment said, and the fix wanted to be slightly bigger than one line.
Why it isn't one line
The suffix rule already exists twice, verbatim in shape:
getStateDir()inpersistence.ts:5andgetKeybindingsDir()inregister.ts:769. It is missing from three places —agent-hooks/runtime.ts:18and both arena handlers. Mirroring it intoruntime.tswould have made a third copy of a rule that has now been forgotten more often than it has been remembered.So
electron/user-data-dir.tsinstead:resolveUserDataDir(userDataPath, isPackaged)is pure, so the rule is testable without an Electron runtime, andgetUserDataDir()is what callers use. After this,app.getPath('userData')appears exactly once in the tree, which makes "someone reached past the helper" a one-line grep instead of an audit.Same reasoning you used on #270 for the third call site: leaving the copies in place would leave the next one to drift.
Worse than the comment said
The misroute succeeds. The port and the token both come from
endpoint.env, so an agent launched by instance A posts to instance B and passestokenMatchescleanly. Nothing logs on either side —coordinator.ts:318returns early on an unknown agent id, andapplyAgentHookEventnever checks the agent belongs to this instance, so the wrong instance writes phantom status entries.Quitting one instance breaks hooks for the other.
endpoint.envis written only insidestartAgentHookServer, at startup. Close the instance that owns the file and it still names a dead port, so the surviving instance's agents post into nothing and fall back to PTY heuristics until that instance is itself restarted.Observed, not just reasoned
Neither CI nor the tests exercise the dev/packaged separation — the unit tests cover the string rule only. Same gap as the lock in #270, so the same treatment: packaged build under Xvfb, dev run started second, both pointed at the same
--user-data-dir.Before (upstream
runtime.ts) — the dev run overwrites the packaged instance's file:After — the packaged file is untouched:
Packaged builds are unchanged
resolveUserDataDirreturns its input on the first line whenisPackaged, andgetUserDataDir()is its only caller. All four converted sites resolve to exactly the same paths they did before in a packaged build. agent-hooks needs no migration either —endpoint.env,hook.shandclaude-settings.jsonare rewritten every launch.The second commit is droppable
SaveArenaData/LoadArenaDataare the same slip without the silent misrouting, found while making the first fix. It is a behavior change for dev runs:arena-presets.jsonandarena-history.jsonstay with the installed build, so a dev run picks this up as an empty arena. Split out so you can drop it and keep this PR to agent-hooks.Checks
npm run compile,npm run check:static,vitest run(2216 passed / 25 skipped) andnpm run test:client(36 passed / 6 skipped) all green.One note in case it is useful to you beyond this PR:
npm run typecheckandnpm run check:staticdo not type-checkelectron/**— roottsconfig.jsonhas"include": ["src", "electron/ipc/channels.ts"], so onlynpm run compilecovers the main process. I had a main-process type error passing both of the checks you used to verify #270 before I caught it.🤖 Generated with Claude Code
https://claude.ai/code/session_01MoJfWuU93BeL2W48mz9bC6