Persist session label per-project so it survives restart - #11
Open
agigante80 wants to merge 2 commits into
Open
Conversation
A `--label` set on the monitor is now remembered per project so it is
reused on restart without re-passing the flag.
- New bin/profile.py: load/save/resolve a per-project label profile in the
data dir, keyed by the git repo root (nearest `.git` ancestor, else cwd),
so a subdirectory of the same repo resolves the same profile. Stored as a
dict ({path, label, updated_at}) with room for future fields; the label is
re-validated on load so a hand-edited/older file can't surface an invalid
one. Filename is a generated hash — no path-traversal surface.
- client.py: `--label` default is now None so "flag absent" (→ load persisted)
is distinguishable from "--label ''" (→ clear). `--label X` persists X;
INTER_SESSION_LABEL is a runtime override that is used but not persisted.
- shared.atomic_write_text: extracted the atomic tempfile+replace write used
by the profile store, and reused it in client._write_session_state (dedup).
- Docs: SKILL.md label note; root README.md + README.zh.md "Session labels".
Scope: label only. team / a separate description field / a committable repo
dotfile are deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwNCo3qLamBCzhMVAHGuH9
Review follow-up covering the untested client-side resolution path and
adding positive/negative scenarios (incl. over-max-length labels):
- profile.resolve_label(explicit, cwd): None → load persisted; a string →
persist ('' clears) and return. Unit-tested directly.
- client._resolve_label(cli, env, cwd): extracts the --label vs
$INTER_SESSION_LABEL vs persisted precedence out of main() so it is
testable; raises ValueError on an invalid explicit label.
- tests: label length boundary (60 persists, 61 is a no-op and is ignored
on load and does not overwrite an existing value), malformed profiles
(non-dict JSON, non-string label, missing key, corrupt JSON), resolve_label
and _resolve_label precedence/persistence, and shared.atomic_write_text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwNCo3qLamBCzhMVAHGuH9
This was referenced Jul 12, 2026
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.
Closes #10.
What
A
--labelset on the monitor is now remembered per project, so it is reused on the next restart without re-passing the flag. Today a label lives only for the life of one monitor process; you must re-supply it every time.How
bin/profile.py— a small JSON profile in the data dir, keyed by the project root (nearest.gitancestor, else cwd, symlink-resolved), socd-ing into a subdirectory of the same repo resolves the same profile. Stored as a dict ({path, label, updated_at}) with room for future fields (e.g.team) so there's no migration later. The label is re-validated on load, so a hand-edited or older-format file can never surface a label the live path would reject. The filename is a hash we generate → no path-traversal surface.bin/client.py—--labelnow defaults toNone, so "flag absent" (→ load the persisted label) is distinguishable from an explicit--label ""(→ clear it). Precedence, extracted into a testable_resolve_label:--label X→ use and persist (''clears);INTER_SESSION_LABEL(non-empty) → one-off runtime override, not persisted;shared.atomic_write_text— extracted the atomic tempfile+os.replacewrite and reused it in both the profile store andclient._write_session_state(removes a duplicated block).README.mdandREADME.zh.md(kept in sync per the repo convention).Scope
Label only.
team, a separatedescriptionfield, and a committable.inter-session.jsonrepo dotfile are intentionally deferred. Also out of scope: an in-session "relabel" command (a label change still requires a reconnect today) — a natural follow-up.Tests
34 label-focused tests, positive and negative — including over-max-length labels (a 61-char label is a no-op on save, ignored on load, and does not overwrite an existing value), malformed profiles (non-dict JSON, non-string label, missing key, corrupt JSON), repo-root keying (same repo / different subdir → same profile), clear semantics, the full
--label/$INTER_SESSION_LABEL/ persisted precedence, andshared.atomic_write_text.Full suite: 234 passed. The 3 failing
test_helpers/test_clienttwo-listener tests are a pre-existing server-election race, unrelated to this change.Verified end-to-end through the real
client.py main(): connecting from a subdirectory with--labelpersisted to a repo-root-keyed profile; a subsequent no---labelrun reused it;--label ""cleared it.