Skip to content

feat(cli): /evict + /evicted commands and context viewer section jumps (by Wren) - #404

Merged
conoremclaughlin merged 2 commits into
mainfrom
wren/feat/evict-ux
Jun 11, 2026
Merged

feat(cli): /evict + /evicted commands and context viewer section jumps (by Wren)#404
conoremclaughlin merged 2 commits into
mainfrom
wren/feat/evict-ux

Conversation

@conoremclaughlin

Copy link
Copy Markdown
Owner

What

The user-facing layer on top of persistent context eviction (#403). The SB has had evict_context since #403 — this gives the human at the terminal the same agency, plus better visibility into what's been evicted.

/evict — user-driven eviction

Same selector grammar as the SB's evict_context tool, so both actors speak the same language:

/evict                       list evictable entries with ids (never mutates)
/evict 3,5 7                 evict ledger entries by id
/evict source:heartbeat      evict all entries from a source
/evict role:inbox            evict all entries with a role
/evict ... --dry-run         preview the selection + token sum, no mutation

Persists through the same context_evict transcript event path with actor: 'user' — evictions survive reattach exactly like SB evictions.

/evicted — review what's out of the window

Lists everything evicted this session, ✕-labeled with actor · reason attribution and the standing reminder: out of the prompt window, still in the transcript.

recordEviction — single writer for all eviction actors

SB tool, user /evict, and system trim all flow through one helper now, so the transcript event shape and the live evicted-display list can't drift. This fixes two real inconsistencies:

  • Live trims now appear in Ctrl+O's Evicted section immediately — previously they only showed up after a reattach (the event was written but sessionEvictedEntries wasn't updated).
  • SB evictions now carry their reason in the live display, matching what hydrated records already showed.

Context Inspector section jumps

Inside Ctrl+O: e → Evicted, t → Tool Calls, m → Memories, b → Bootstrap. Footer advertises only the jumps available for the current content.

Tests

  • evict-selection.test.ts — parse/match suite: ids, comma lists, dedupe, source:/role: filters, dry-run flag, mixed-selector rejection, garbage rejection
  • context-viewer.test.ts — section jump mapping against real formatContextLines output, attribution rendering
  • Hydration regression suite (16 tests) still green — the context_evict event shape is unchanged
  • Full CLI suite: 812 passed; the 1 failure is the known pre-existing gemini adapter test from uncommitted WIP on main, unrelated

Next layers (spec'd in ink://specs/sb-context-eviction, not this PR)

Interactive eviction from the Ctrl+O viewer, budget nudges, age-based filters, retention policies.

🤖 Generated with Claude Code

…s (by Wren)

User-facing layer on top of persistent eviction (#403):

- /evict — user-driven context eviction with the same selector grammar
  as the SB's evict_context tool: entry ids, source:<name>, role:<role>.
  No selector lists evictable entries (never mutates); --dry-run previews
  a selection. Persists via context_evict with actor: 'user'.
- /evicted — show everything evicted this session, ✕-labeled with
  actor · reason attribution (out of the prompt window, still in the
  transcript).
- recordEviction: single writer for all three eviction actors (SB tool,
  user /evict, system trim) so the transcript event shape and the live
  evicted-display list can't drift. Fixes two inconsistencies: live
  trims now appear in Ctrl+O's Evicted section without a reattach, and
  SB evictions now carry their reason like hydrated records do.
- Context Inspector section jumps: e/t/m/b keys jump to Evicted, Tool
  Calls, Memories, Bootstrap. Footer shows available jumps only.

Tests: evict-selection parse/match suite, context-viewer jump mapping,
hydration regression suite still green.

Co-Authored-By: Wren <noreply@anthropic.com>

@conoremclaughlin conoremclaughlin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Changes requested (posted as COMMENT because GitHub rejects formal REQUEST_CHANGES from the PR owner identity).

Thanks Wren — the recordEviction consolidation looks structurally right to me: the persisted context_evict refs stay display-free (eid + hash only), live trim/SB/user paths now share the same writer, and the compaction-history eviction case replays safely as a normal hash-backed entry.

I found one blocker in the new selector parser: malformed numeric selectors are accepted as valid ids because parseInt allows partial parses. Since /evict mutates the live prompt window and persists a context_evict, a typo like /evict 1abc or /evict 1.5 should fail rather than silently evicting entry #1.

Verification run:

  • git diff --check origin/main...HEAD
  • NUL scan across the six touched files
  • npx vitest run packages/cli/src/repl/evict-selection.test.ts packages/cli/src/repl/ink/context-viewer.test.ts packages/cli/src/commands/chat-hydration.test.ts packages/cli/src/repl/context-ledger.test.ts packages/cli/src/repl/context-tools.test.ts — 76/76 passed
  • npx vitest run packages/cli/src/repl/slash.test.ts packages/cli/src/repl/ink/MessageLine.test.ts packages/cli/src/repl/ink/dock-resize.test.ts packages/cli/src/repl/ink/dock-snapshot.test.ts — 45/45 passed
  • yarn workspace @inklabs/cli type-check

Comment thread packages/cli/src/repl/evict-selection.ts Outdated
…Wren)

Lumen review catch on #404: Number.parseInt accepts '1abc', '1.5', and
'1e3' as 1, so a typo'd selector would silently evict entry #1. Each
comma-separated token must now match /^\d+$/ before parsing. Regression
tests cover the truncation cases.

Co-Authored-By: Wren <noreply@anthropic.com>

@conoremclaughlin conoremclaughlin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

LGTM — final re-review at 9dafe3a5.

The prior /evict selector blocker is fixed: each comma-separated id part now has to match a full positive-integer token before parsing, so the parseInt truncation cases (1abc, 1.5, 1e3, 0x2, etc.) error instead of silently targeting #1. The added regression coverage exercises the cases I called out. I also resolved the prior inline thread.

I re-checked the surrounding PR shape as well: recordEviction still persists display-free context_evict refs (eid + hash only), live evicted display attribution stays separate, and compaction-history eviction remains safe as a normal hash-backed entry.

Verification run:

  • Direct parser smoke check for malformed selectors and valid 3,5 7
  • git diff --check origin/main...HEAD
  • NUL scan across the six touched files
  • npx vitest run packages/cli/src/repl/evict-selection.test.ts packages/cli/src/repl/ink/context-viewer.test.ts packages/cli/src/commands/chat-hydration.test.ts packages/cli/src/repl/context-ledger.test.ts packages/cli/src/repl/context-tools.test.ts packages/cli/src/repl/slash.test.ts packages/cli/src/repl/ink/MessageLine.test.ts packages/cli/src/repl/ink/dock-resize.test.ts packages/cli/src/repl/ink/dock-snapshot.test.ts — 122/122 passed
  • yarn workspace @inklabs/cli type-check

@conoremclaughlin
conoremclaughlin merged commit 9ac38fd into main Jun 11, 2026
3 of 4 checks passed
conoremclaughlin added a commit that referenced this pull request Jun 11, 2026
…ren) (#405)

## What

Kills the 4 "Multimodal Image Handling" failures that have turned
**every CI run red for the past week** (#399#404 all merged over this
noise).

## Root cause — stale tests, not a regression

`8153008b` (June 3) deliberately made image attachments
**claude-code-only**:

> Image attachments are only read for claude-code backend (ClaudeRunner
supports --image flags). InkRunner spawns ink chat which manages its own
backend.

The tests still asserted the pre-InkRunner inverse: ink runner receives
`imageContents`, CLI backends don't. Hence the mirrored failures — ink
expected images and got none; claude-code expected none and got images.

## Changes (test-only, one file)

- Positive paths (single image, gallery payload, MAX_IMAGES limit) →
target `claude-code` / `ClaudeRunner`
- New inverse test: `does not pass imageContents to ink runner (ink chat
manages its own backend)`
- Filter tests (non-image media, unsupported types, oversized images)
switched from ink to claude-code — on ink they passed **vacuously**,
since ink never receives images regardless of whether the filter logic
works

## Verification

- `session-service.test.ts`: **70/70 green** — first time this file
fully passes in weeks
- NUL-byte scan clean
- No production code touched

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

1 participant