Skip to content

fix(claude-sessions): unwrap NBI preamble when joined with user prompt#331

Merged
mbektas merged 1 commit into
plmbr:mainfrom
pjdoland:fix/329-session-summary-context-prefix
May 21, 2026
Merged

fix(claude-sessions): unwrap NBI preamble when joined with user prompt#331
mbektas merged 1 commit into
plmbr:mainfrom
pjdoland:fix/329-session-summary-context-prefix

Conversation

@pjdoland
Copy link
Copy Markdown
Collaborator

Summary

Closes #329. When a Claude Code session has only a single user turn, the chat-history picker showed no preview / summary for that session — only the id and timestamp. The cause was a skip filter that treated the joined preamble + prompt string as if it were only the preamble.

Solution

claude.py joins consecutive user-role chat-history entries with \n before handing them to the Claude Agent SDK. extension.py emits the NBI context preamble ("Additional context: Current directory open in Jupyter is: ...") as its own user-role entry, so a single-turn session records ONE combined user message whose first line is the preamble and whose remainder is the user's actual prompt.

The previous skip filter at notebook_intelligence/claude_sessions.py checked stripped.startswith(_SKIPPABLE_PREFIXES) against the full text, matched the preamble, and dropped the whole message. The session's only user message was thereby skipped, leaving the picker preview blank.

The load-bearing change is a new _strip_nbi_context_preamble helper that drops the preamble line (everything up to and including the first newline) when present. It plugs into the two consumers:

  • _is_skippable_text unwraps before applying the rest of the skip rules so the message is judged by the real-prompt content. The unwrap recurses at most once because NBI_CONTEXT_PREFIX is single-line: the unwrapped tail can never itself lead with the preamble, and the recursive call falls through to the legacy prefix / control-command checks (or bottoms out on the empty-stripped guard).
  • _extract_preview unwraps before whitespace-collapsing so the preview reads as the user's prompt rather than the boilerplate.

The helper deliberately doesn't trim or normalize the unwrapped tail beyond stripping the preamble line: both callers normalize whitespace themselves (_is_skippable_text via .strip(), _extract_preview via " ".join(text.split())), and trailing whitespace in the result belongs to the user's prompt either way.

Testing

Five new tests in tests/test_claude_sessions.py pin the contract:

  • test_unwraps_joined_preamble_and_prompt_in_one_message — the exact bug from the issue body (preamble + \n + prompt as a single string content).
  • test_unwraps_joined_preamble_in_structured_content — same shape, Anthropic structured-content form with the joined string inside one text block.
  • test_joined_preamble_with_skippable_prompt_still_skips — preamble joined with a control slash command (/exit); the combined message must remain skippable so the picker keeps scanning for a real prompt.
  • test_joined_preamble_alone_without_newline_still_skips — preamble recorded with no trailing newline; helper returns empty string and the recursive call bottoms out on the empty-stripped guard.
  • test_joined_preamble_with_empty_post_newline_still_skips — preamble followed by whitespace-only tail; same end state as preamble-alone.

The first two are verified to fail against the pre-fix code; the latter three document downstream behavior the picker has always honored.

All four gates green: pytest tests/ --ignore=tests/test_claude_client.py (1055 passed, 66 in test_claude_sessions.py), jlpm tsc --noEmit, jlpm lint:check, jlpm jest (254 passed).

Risks / follow-ups

  • A user prompt that happens to start with the literal phrase "Additional context: Current directory open in Jupyter is:" would have its first line stripped. The pre-existing _SKIPPABLE_PREFIXES tuple already had the same trade-off (called out in the existing comment at lines 47-49); the helper inherits it.
  • Multi-block structured content where one block is the preamble and another block is the prompt isn't observed today (the SDK joins at the string level, not at the block level). If that shape ever lands, _is_skippable_user_message would need to mirror _extract_preview's block-join behavior. Filed mentally as a follow-up.
  • The NBI_CONTEXT_PREFIX constant remains in _SKIPPABLE_PREFIXES as a backstop. The new early branch consumes the preamble first, but the tuple entry still handles edge cases where the preamble somehow reaches the fallthrough.

Issue linkage

Closes #329.

claude.py joins consecutive user-role chat-history entries with '\n'
before handing them to the Claude Agent SDK. extension.py emits the
NBI context preamble ("Additional context: Current directory open in
Jupyter is: ...") as its own user-role entry, so a single-turn
session records one combined user message whose first line is the
preamble and whose remainder is the user's actual prompt.

The previous skip filter checked startswith(_SKIPPABLE_PREFIXES) on
the whole text, matched the preamble, and dropped the entire message.
With nothing else in the transcript, the picker had no preview and
the session showed up titled only by its id + timestamp (issue plmbr#329).

The fix:

- New _strip_nbi_context_preamble() helper drops the preamble line
  (everything up to and including the first newline) when the text
  starts with NBI_CONTEXT_PREFIX. Returns the input unchanged
  otherwise, and the empty string when the preamble is the only
  content.
- _is_skippable_text() unwraps before applying the rest of the skip
  rules so a joined preamble+prompt is judged by its real-prompt
  content. The unwrap recurses at most once: NBI_CONTEXT_PREFIX is
  single-line, so the tail can't itself lead with the preamble, and
  the recursive call falls through to the legacy prefix / control-
  command checks (or to the empty-stripped guard at the top).
- _extract_preview() unwraps before whitespace-collapsing so the
  preview reads as the user's prompt, not the boilerplate.

Five new tests pin: joined plain-text preamble + prompt, the
Anthropic structured-content variant, joined preamble + skippable
prompt (recursion exercise), preamble alone without trailing newline,
and preamble followed by whitespace-only tail. The first two are
verified to fail against the pre-fix code.

Closes plmbr#329
@pjdoland pjdoland added this to the 5.0.x milestone May 20, 2026
@pjdoland pjdoland added the bug Something isn't working label May 20, 2026
Copy link
Copy Markdown
Collaborator

@mbektas mbektas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

@mbektas mbektas merged commit 4cfe471 into plmbr:main May 21, 2026
4 of 5 checks passed
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude Chat session history is not showing summary

2 participants