fix(claude-sessions): unify session listing across NBI surfaces, drop history.jsonl gate#310
Merged
Merged
Conversation
…gate
The Launcher tile's session picker silently disagreed with /resume:
inside Claude, /resume found sessions; in the launcher picker the same
user saw "No previous sessions found." Root cause was the backend's
reliance on ~/.claude/history.jsonl as the gating source. Recent Claude
Code versions don't reliably populate history for SDK-driven flows, and
the existing fallback only scanned the Jupyter cwd's transcript dir, so
sessions launched from a different cwd disappeared from both NBI
pickers while /resume continued to find them.
Rebuild list_all_sessions on a direct walk of ~/.claude/projects/*/
which is the durable, authoritative source. The cwd for each session
is taken from the transcript's own "cwd" field (Claude Code writes it
on most message envelopes) so paths with literal dashes round-trip
correctly. Falls back to a dash-decoded directory name only when the
transcript carries no cwd at all (older flows + synthetic fixtures).
A small mtime-keyed cache memoizes per-transcript parses so repeated
list calls don't re-open every .jsonl on each click (D207).
Handler scope=cwd switches from comparing transcript-path dirnames to a
realpath comparison on the session's cwd field. This fixes symlink-
alias mismatches common on JupyterHub user dirs (NFS, EFS) where the
mounted path differs from the path Claude Code originally recorded.
Launcher picker rows now render the cwd basename as the inline label
with the full path on hover, so a user with several similarly-named
projects can disambiguate without losing the path entirely.
Tests:
- Existing 63 session tests adapted to the new transcript-first
preview contract; one ("history display wins") rewritten to assert
the inverse ("transcript wins over stale history display").
- Six new TestCrossSurfaceConsistency cases pin the structural
invariants: launcher returns every on-disk session; launcher works
with no history.jsonl; cwd-scope is a subset of all-scope; every
session carries a usable cwd; dash-decode fallback for older
transcripts; mtime cache skips reparse on unchanged files.
- Handler test adds a symlink-alias case for scope=cwd.
- New launcher-picker.test.tsx pins the basename label rendering and
the empty state.
Six-reviewer pass surfaced several convergent improvements: - Memoize realpath() per cwd inside the scope=cwd filter at extension.py so a 1k-session request on NFS doesn't fan out into 1k network round trips. The target cwd is already resolved once at comprehension entry; this caches each unique session.cwd too (performance). - Add an autouse fixture that clears the module-level _SESSION_INFO_CACHE between tests. The cache key includes the tmp_path-anchored absolute path, so cross-test collisions were already unlikely, but explicit clearing makes ordering and re-run behavior deterministic (test architecture). - Add a handler test pinning the documented behavior that scope=cwd drops sessions with empty cwd (their cwd is unmatched, so they cannot belong to the current workspace; scope=all still surfaces them). Documents the silent-drop so a future refactor doesn't reintroduce them by accident (test architecture). - Sharpen the TestCrossSurfaceConsistency docstring to say what these tests actually pin (NBI's own surfaces agree on the on-disk transcript set) versus what they don't (claude --resume is not shelled out; a Galata or shell-out integration test is the right cross-binary check) (test architecture). - Fix the misleading comment in _read_session_info about how the per-line loop terminates (waits for both preview AND cwd, capped by _MAX_LINES_SCANNED) (code reuse). - Add aria-label fallback on the launcher-picker project span: screen readers do not reliably expose title on <span>, so duplicate the full path into aria-label whenever it differs from the visible basename. Also guard title against undefined cwd (frontend / UX).
mbektas
approved these changes
May 19, 2026
pjdoland
added a commit
to pjdoland/notebook-intelligence
that referenced
this pull request
May 22, 2026
Promotes the [Unreleased] CHANGELOG snapshot to [5.0.0] - 2026-05-22 and expands it to cover everything merged into upstream/main after PR plmbr#287's docs refresh. Bumps package.json to 5.0.0. CHANGELOG additions cover the post-plmbr#287 surface: - Settings tabs: plugin marketplace picker (plmbr#284), plugin marketplace details + Update button (plmbr#303), per-workspace MCP disable (plmbr#286), JSON-paste path in Add MCP server (plmbr#285). - Launchers: hide-with-policy (plmbr#288), brand icons for Codex / opencode (plmbr#325, plmbr#333), per-launch directory picker (plmbr#332). - Chat sidebar and agentic UX: workspace @-mention in Claude mode (plmbr#327), reload-open-files-on-disk (plmbr#330), steered system prompt away from over-eager notebook creation (plmbr#336). - Skills: multi-manifest support (plmbr#321), tracks-upstream for user- imported skills (plmbr#322), HTTP kill switch for the reconciler (plmbr#291). - Accessibility: full sub-section covering plmbr#305-plmbr#320. - Security: shell-tool sandbox (plmbr#290), Claude UI-bridge sandbox (plmbr#323), 0o600 on encrypted token (plmbr#293), env-secret scrubbing (plmbr#295), MCP config shape validation (plmbr#299), XSS allowlist (plmbr#296), Copilot WS auth + origin (plmbr#301), GHE host detection (plmbr#292), fastmcp -> mcp SDK swap (plmbr#324). - Fixed: session listing unification (plmbr#310), session preview unwrap (plmbr#331), down-area runtime throw (plmbr#330 follow-up), WS message-handler leak (plmbr#294). - Removed: fastmcp dependency, history.jsonl session gate. Adds a Migration note covering the five behavior changes operators should review before upgrading from 4.x: fastmcp swap, path sandboxes, history.jsonl gate removal, workspace @-mention pointer shape, and the Copilot WebSocket auth/origin tightening. Two reviewer rounds (six personas each) applied: - Round 1 caught security overclaims (plmbr#293, plmbr#299, plmbr#323), the plmbr#284/plmbr#303 mis-attribution, missing migration note, 3 em dashes, and the stale `fastmcp==2.x.*` recommendation in the admin guide. - Round 2 caught the missing plmbr#301 migration bullet, missing version- matrix 5.0.x row, missing README TOC entry, and a couple of style nits (sub-heading overpromise, orphan bullet). Skipped (deferred to future PRs): - README first-run tour mention. - Admin guide HTTP kill-switch row in Failure-modes table. - Terminal drag-drop trust-model precision update after plmbr#327. - Cipher description nit in plmbr#293 (Fernet AES-128-CBC+HMAC, not AES-GCM).
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.
Summary
A user reported that the Launcher tile's Claude Code session picker showed "No previous sessions found" while
/resumeinside the same Claude session listed previous sessions. Root cause: the backend used~/.claude/history.jsonlas the gating source for session listing. Recent Claude Code releases don't reliably populate that file for SDK-driven invocations, and the cwd-only fallback path only scanned the Jupyter root's transcript dir, so any session launched from a different cwd disappeared from both NBI pickers while Claude Code's own/resume(which walks the project dirs directly) continued to find them.Solution
Rebuild
list_all_sessionson a direct walk of~/.claude/projects/*/, which is the durable on-disk source/resumeconsumes. The cwd for each session comes from the transcript's own"cwd"field (Claude Code writes it on most message envelopes) so paths with literal dashes round-trip correctly; a dash-decode of the directory name is only used as a fallback for older transcripts that lack the field.The handler's
scope=cwdfilter switches from comparing transcript-path dirnames to a realpath compare onsession.cwd, fixing symlink-alias mismatches common on JupyterHub user dirs (NFS / EFS), and memoizes the per-session realpath inside the request so a 1k-session filter on NFS doesn't fan out to 1k network round trips.A mtime-keyed cache memoizes per-transcript parse results so repeated list calls don't re-open every
.jsonlon each click (addresses D207 from the audit).The Launcher tile's row now renders the cwd basename as the inline label with the full path duplicated on both
titleandaria-labelso screen-reader users get the disambiguating path too.Testing
pytest tests/ --ignore=tests/test_claude_client.py -q882 passed (6 new TestCrossSurfaceConsistency cases, 1 new symlink-alias handler case, 1 new empty-cwd silent-drop pin, autouse fixture clears_SESSION_INFO_CACHEbetween tests).jlpm tsc --noEmitclean.jlpm lint:checkclean.jlpm jest183 passed (2 newtests/ts/launcher-picker.test.tsxcases).notebook-dir=/tmp/nbitour:scope=allreturned 107 cross-project sessions (every project on the machine), each row labeled with its project basename and the full path ontitle+aria-label.scope=cwdreturned 0 (no transcripts for/tmp/nbitour), confirming the realpath filter works.Risks / follow-ups
cd+claude --resume) is deferred to a follow-up; the Launcher tile remains the path for cross-project resumes.claude --resumenon-interactively and diffs session ids against ours is the right cross-binary check but is deferred (it needs the CLI on the Galata runner's PATH).mtimeresolution on some filesystems (HFS+ 1s) could theoretically serve stale cache entries; the cache is a perf hint, not authoritative for correctness, and transcripts are append-only so collision odds are essentially zero in practice. Worth a note but not worth complicating the cache key.