Skip to content

Persist session label per-project so it survives restart - #11

Open
agigante80 wants to merge 2 commits into
yilunzhang:mainfrom
agigante80:feat/persist-label
Open

Persist session label per-project so it survives restart#11
agigante80 wants to merge 2 commits into
yilunzhang:mainfrom
agigante80:feat/persist-label

Conversation

@agigante80

Copy link
Copy Markdown

Closes #10.

What

A --label set 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

  • New bin/profile.py — a small JSON profile in the data dir, keyed by the project root (nearest .git ancestor, else cwd, symlink-resolved), so cd-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--label now defaults to None, 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;
    • neither → load the persisted per-project label.
  • shared.atomic_write_text — extracted the atomic tempfile+os.replace write and reused it in both the profile store and client._write_session_state (removes a duplicated block).
  • Docs — a SKILL.md note, and a new "Session labels" section in both README.md and README.zh.md (kept in sync per the repo convention).

Scope

Label only. team, a separate description field, and a committable .inter-session.json repo 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, and shared.atomic_write_text.

Full suite: 234 passed. The 3 failing test_helpers/test_client two-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 --label persisted to a repo-root-keyed profile; a subsequent no---label run reused it; --label "" cleared it.

agigante80 and others added 2 commits July 12, 2026 13:26
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
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.

Persist session label per-project so it survives restart

1 participant