fix(state): keep one state store per herdr session - #21
Conversation
5c1bae1 to
cc5881b
Compare
herdr numbers every session's tabs from w1:t1, so the plugin running under `herdr --session work` and the one under `herdr --session home` wrote the same keys into the same state.json. Each full pass prunes the tab ids it did not see, which in the other session's store is every tab there, and a tab whose record is gone reads as renamed by hand on the next pass and opts out of naming for good. A machine running more than one session ended up with every tab frozen, and the shared lock dropped events on top of that. A named session now keeps its store, lock and rerun flag under sessions/<name>/ inside the state directory, named from the socket path herdr exports to plugin commands and pane environments alike, so the herdr-invoked pass and the shell hooks resolve the same file. The default session keeps the store where it always was. Fixes qu8n#22. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review of the per-session store turned up three things. An upgrade froze every tab a named session was already naming, not only the ones the shared store had broken: the new store started empty, and a tab carrying a label the store never wrote reads as renamed by hand. A named session's store is now created once from the owned records of the shared one. A matching record keeps the tab named, a mismatched one opts out exactly as no record would, and opted-out records stay behind so a placeholder label is adopted as it would be from nothing. The copy is a link, which refuses an existing file, so a burst of first events cannot cover a pass that already wrote. The store was keyed off the socket path alone, while the herdr CLI also picks its server from HERDR_SESSION when no socket path is set. A hand run with only the name set talked to that session and pruned the default session's records. The resolver now follows the CLI: the socket path when set, the session name otherwise, and `default` means the root store. A hand-set socket path could alias the store: a relative one with no parent landed in sessions/sessions, and a dot segment put the store in the shared directory or the root. The name is now one real path segment. Docs say what both sides actually receive, that the uninstall strip runs per session, and that a deleted session's store is safe to delete. The test unsets the runner's own session first, pins the rerun flag and the new cases, and uses the suite's exit-status helper. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it end to end ar_herdr_session_dir fell back to the default session's directory whenever no socket path was set, while the herdr CLI picks its server from HERDR_SESSION in that case, so a hand run with only the name set talked to one server and read another's session.json and config.toml. It now follows the same order as the store. The two-session regression is also run through ar_main against the mock herdr, so the steps only an executed pass takes are pinned: the nested store's mkdir, the lock, and the prune at the end of the pass. The scenario fails on the unpatched engine and passes here. Docs say why the socket path is the key rather than the name, that an override server outside any sessions directory shares the root store, that config.sh must not set HERDR_* variables, and that the state directory can be deleted after the uninstall strip. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
22f6480 to
6deed32
Compare
Seeding had no upgrade boundary. The root store is not only the store an upgrade leaves behind: it is the default session's live store, and it stays populated for as long as anybody uses that session. So a session created later seeded from it too, and because every server numbers its tabs from w1:t1 the copied record landed on a tab that had nothing to do with it. That tab still carried herdr's generated number, an owned record against a placeholder label is what a hand rename looks like, and the first tab of every new session opted itself out for good -- the reset action, once per tab, this bug one layer along. Copied records are marked. A mark says the record is a claim about another store's tab rather than a write of ours, so the label is asked to confirm it: where it disagrees the record is dropped and the tab is examined as the unseen one it is. That makes the invariant the seeding was documented with actually hold -- a mismatched record now opts out exactly as no record would, and a placeholder label is adopted as it would be from nothing. The workspace path gets the same rule. "ws:w1" collides across sessions the way "w1:t1" does, and its first branch covers most of it already, but opting out is permanent there with no reset action to undo it. Nothing clears the mark: ar_state_set writes a record whole, so our first write replaces it.
The socket route takes the segment after the last separator and cannot carry one. $HERDR_SESSION is whatever the variable says, and it was interpolated: a value with a slash in it put the store, and the config directory the session's files are read from, outside `sessions/` altogether. Nobody is protected from their own environment by this -- anyone who can set that variable can set XDG_STATE_HOME too. It is that a name which is not one segment names no session, and the two routes claimed to agree while only one of them checked. Refs qu8n#21
|
LGTM. Thank you! I pushed two commits to the branch rather than send it back round. The seeding had no upgrade boundary. The root store is the default session's live store as well as the one an upgrade leaves behind, so a session created later seeded from it too, and since every server numbers from The other is small. Tests went from 37 to 58, nine of which fail against the old code. |
Fixes #22.
Before
herdr numbers every session's tabs from
w1:t1. With two servers up, sayherdr --session workandherdr --session home, both plugins write the same keys into the one~/.local/state/herdr-automatic-rename/state.json, and each full pass prunes the tab ids it did not see, which in the other session's store is every tab there. A tab whose record is gone reads as renamed by hand on the next pass and opts out of naming for good.On a machine running three sessions I found the store flipping between two sets of keys within seconds, every tab record it held at
enabled: false, and the current session's tabs absent from it altogether. Titles stopped following the foreground program and the reset action was the only way back, once per tab, until the next pass from another session wiped it again. The shared lock directory drops events as well: a tracedtab.focusedpass exited at once because another session's pass held it.After
A named session keeps its store, lock and rerun flag under
sessions/<name>/inside the state directory. The session is resolved the way the herdr CLI picks its server: from thesessions/<name>/directory in$HERDR_SOCKET_PATHwhenever that is set, the same directoryar_herdr_session_diralready reads, and from$HERDR_SESSIONonly when it is not. herdr exports both to plugin commands and pane environments alike (checked on the live[[startup]]children of three 0.8.2 servers), so the herdr-invoked pass and the shell hooks resolve one file. The default session, which herdr also callsdefault, keeps the store exactly where it was, so a single-session install sees no change.A named session's store is created once from the owned records of the old shared one (
ar_state_seed). Without that, the first pass after the upgrade would read every label the old store wrote as a hand rename and opt every named tab out, which would hurt exactly the single-named-session user who never had the bug. Onlyenabled: truerecords come across, so a matching one keeps the tab named, a mismatched one opts out as no record would, and a placeholder label is adopted as it would be from nothing. The copy is a hard link, which refuses an existing file, so a burst of first events cannot cover a pass that already wrote. Ids the session lacks go on the first prune.The extracted name must be one real path segment, so a relative socket path with no parent, a doubled slash, or a dot segment resolves to the root store rather than aliasing it onto
sessions/itself or the root. Nothing removes a session's directory when the session is deleted; the docs say it is safe to delete by hand.ar_herdr_session_dir, which locatessession.jsonandconfig.toml, follows the same order, so a name-only run reads the files of the server it talks to.tests/test_session.shpins the resolution (no socket path, default-shaped socket, named session, the dot and relative cases, the name fallback and the socket path winning over it), that the lock and the rerun flag follow the store, the regression itself (one session's prune leaves the other session's record alone and that tab stays eligible), and the seeding (owned records come across, opted-out ones do not, an existing store is never seeded over, an unreadable shared store seeds nothing, the root store never seeds onto itself). It ends with the two-session regression run throughar_mainagainsttests/mocks/herdr, a work pass, a home pass, then a work pass with the program changed, which renames on this branch and renames nothing onmain.Two contrived layouts still collapse onto one store and are left as they are: a server started with a socket path override outside any
sessions/<name>/directory, and a config directory whose own parent happens to be namedsessions.Checks
./tests/run.sh: all files pass, including the new one, under bash 3.2 as well. The second and third commits answer a review of the first; their messages list what changed.shellcheck -x -s bashover the Makefile's file list: clean (the one suppression names its code and reason).make lint-md: clean.🤖 Generated with Claude Code