Skip to content

Sessions: let the pane say which conversation it is on - #23

Merged
lvwerra merged 2 commits into
mainfrom
sessions/repin-breadcrumbs
Aug 4, 2026
Merged

Sessions: let the pane say which conversation it is on#23
lvwerra merged 2 commits into
mainfrom
sessions/repin-breadcrumbs

Conversation

@thomwolf

@thomwolf thomwolf commented Aug 4, 2026

Copy link
Copy Markdown
Member

Problem

#17 taught the re-pin watcher to follow /clear onto the successor conversation, but it deliberately refuses when several live claude sessions share a folder — a new transcript there cannot be attributed to a pane by scanning. Busy shared workspaces always have rivals, so /clear there is never followed: observed live on Aug 4, a restart resumed a pane's pre-/clear thread and orphaned a day of work in the successor.

Approach

Scanning cannot attribute — but the pane itself knows. A SessionStart hook (scripts/am-repin-hook.sh, registered idempotently into settings.json at server boot) runs inside the pane's process tree, where $AM_ID names the pane and the payload carries the new conversation id. It drops a breadcrumb on local disk; the watcher consumes it and re-pins with nothing to guess. The shared-folder refusal now guards only the scan fallback (kept for hook-less panes and for codex/opencode).

Why breadcrumbs need verifying

SessionStart fires more widely than documented (verified on Claude Code 2.1.220 — print mode fires it too, contrary to the docs):

  • $CLAUDE_PID must descend from the pane's tmux root. A nested claude -p inside a pane inherits $AM_ID and would otherwise claim the pane with a throwaway conversation — worse than the bug. The hook's CLAUDE_CODE_ENTRYPOINT=cli filter drops most; the pid check is the backstop.
  • Payload cwd must equal the pane's folder; the id must not be pinned by another session.
  • No-matcher registration: startup replaces the "--session-id not honoured" heuristic with a fact; resume provably reports the unchanged id (no-op) — and re-validates the chain at every restart.
  • Consumed-on-read, so a stale crumb can never flip a pin backwards past a later scan re-pin. Crumb loss at restart is harmless: the pin persists in sessions.json.

The installer merges (never replaces) settings.json, is idempotent, and refuses to touch a file it cannot parse. Verified on a live established config dir: adding hooks shows no trust prompt.

Tests

server/test/repin.test.mjs extended: 21 checks covering the verdict (attribution, nested-run rejection, claimed/no-op/garbage) and the installer (merge, idempotency, corrupt-file refusal). Full live validation notes (five experiments incl. two concurrent panes in one folder attributing correctly): produced during review with a second agent, available on request.

🤖 Generated with Claude Code

claude and others added 2 commits August 4, 2026 10:38
PR #17 taught the re-pin watcher to follow /clear onto the successor
conversation, but it deliberately refuses in a folder shared by several live
claude sessions — a new transcript there cannot be attributed to a pane by
scanning. Folders like a busy project workspace ALWAYS have rivals, so /clear
there was never followed and a restart still resumed the pre-/clear thread.
Observed live: a pane /cleared on Aug 3, its successor absorbed a day of work,
and the Aug 4 restart brought back the abandoned thread and orphaned the rest.

Scanning cannot attribute, but the pane itself knows. A SessionStart hook
(scripts/am-repin-hook.sh, registered idempotently into settings.json at boot)
runs inside the pane's process tree, where $AM_ID names the pane and the
payload carries the new conversation's id. It drops a breadcrumb on local
disk; the watcher consumes it and re-pins with nothing to guess. The
shared-folder refusal now applies only to the scan fallback, which stays for
panes without a breadcrumb and for codex/opencode.

Breadcrumbs are trusted only after checks, because SessionStart fires more
widely than the docs admit (verified on 2.1.220 — print mode fires it too,
contradicting the documentation):

- $CLAUDE_PID must descend from the pane's tmux root. A nested `claude -p`
  run inside a pane inherits $AM_ID and would otherwise claim the pane with a
  throwaway conversation — worse than the bug. The hook's
  CLAUDE_CODE_ENTRYPOINT=cli filter drops most of these; the pid check is the
  backstop.
- The payload cwd must be the pane's folder, the id must not be pinned by
  another session, and a no-matcher registration means source:"startup" also
  replaces the "--session-id not honoured" heuristic with a fact, while
  source:"resume" provably reports the unchanged id and stays a no-op.

Consumed-on-read so a stale crumb can never flip a pin backwards past a later
scan re-pin. Losing breadcrumbs at restart is harmless: the pin persists in
sessions.json and the relaunch's own resume event writes a fresh crumb —
which incidentally verifies the whole chain at every restart. The installer
merges into settings.json (never replaces — it also holds permissions and
model), is idempotent across boots, and refuses to touch a file it cannot
parse. Verified against a live config dir: adding hooks to an already-trusted
dir shows no trust prompt.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Only conflict was the runner.js import line in index.js: #24 (libghostty
session model) dropped `copySelection`/`paneModes` and added `stopAll`,
`ghosttyReady`, `ghosttyError`; this branch adds `installClaudeRepinHook`.
Took both sides — main's list plus the hook installer — and kept main's
TERM_CTRL sentinel. runner.js merged cleanly: all three capture schedulers
are still wired in ensureRunning, and nothing references the two dropped
exports any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

3 participants