# Claude Code Mod

The package also ships `plugins/claude/`, an early-access Claude Code Mod that registers five notes tools as `mcp__notesoup__notes_*`, two history tools as `mcp__notesoup__history_*`, and `mcp__notesoup__empty_brain` and attaches one host-scoped notes boot block through Claude's conversation-scoped `prompt.context` event; Claude persists it across resume and reload. The sandboxed hooks module delegates filesystem and shared-domain work to the plugin-local `plugins/claude/dist/claude/helper.js` through Claude's bounded `$.process.run` capability.

Warning and automatic clear use one budget policy. The existing warning and reset points are derived from Claude's own auto-compact threshold instead of a fixed percent, in `plugins/claude/src/thresholds.ts`, and reach the sandboxed hooks module through the helper's `limits` operation. At the hard point, the next main `turn.step` ends the old turn after its whole tool batch; a successful main `turn.complete` also checks the point when no request is already pending. Both enter the existing non-held reset worker, without a separate cap-specific trigger. Measurement alone never clears context, child events cannot reset the main brain, and aborted/error/refusal turns do not commit a budget reset. The policy resolves its native point as follows:

1. `$.session.usage({breakdown:"summary"}).context.breakdown.autoCompactThreshold` when it is a positive number, with `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` applied the way Claude applies it (it can only lower the point);
2. otherwise computed from `$.session.usage().context.window` plus `CLAUDE_CODE_AUTO_COMPACT_WINDOW`, `modelSettings.<model>.autoCompactWindow` and `autoCompactWindow` (the smallest configured window wins, capped at the model window), with Claude's 20000-token output reserve and 13000-token summary buffer; `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` lowers this fallback point by the same formula. With auto-compaction off, the percent override is ignored;
3. otherwise the percent fallback, which assumes a classic 200K capacity, applies the same configured-window caps and token derivation, then converts the points to percentages.

The hard clear point is `resetMarginTokens` (default 8192) below the native point; the warning is another `warningRunwayTokens` (default `WARNING_RUNWAY_TOKENS`, 16384) below that. Resolved points are reused only while the current model, cap, threshold inputs and settings match; changing context fill alone does not invalidate them. With automatic compaction off (`autoCompactEnabled: false` in the merged settings, or `DISABLE_AUTO_COMPACT=1`) the point becomes the effective window minus the output reserve, so the same warning margins still apply, but automatic notesoup clears are disabled too. Explicit resets remain available. Every undocumented field is type-guarded and degrades to the next source; an unresolvable point, or missing usage, logs once and skips the budget warning. `DISABLE_COMPACT=1` is detected at `session.start`, logs once, suppresses budget warnings and explicit resets, and makes `/fresh-window` report that it cannot run. The margins are read from the `notesoup` object of Claude's merged `$.settings.read()` (`resetMarginTokens`, `warningRunwayTokens`); any other value is ignored. Setting `NOTESOUP_DISABLED` (presence-style, the same convention as `DISABLE_COMPACT`) turns the whole mod off: `session.start` registers no tools or commands and logs once, every event passes through untouched, and native compaction behaves as if the mod were absent.

`/notesoup cap 100k` supplies a runtime-only capacity ceiling, clamped to the smaller model/configured window before the same reserves and margins are deducted. A reported native threshold can only be lowered, never raised. `/notesoup cap` (or bare `/notesoup`) shows the cap; `/notesoup cap off` restores the uncapped policy. Whole tokens and decimal `k`/`m` suffixes are accepted, with caps too small for reserves refused. The cap survives notesoup rollover and model changes but is cleared on plugin reload, session end or session identity change. It does not call `/autocompact`, alter the model, or write settings/environment variables. An already pending reset keeps its continuation/STOP policy when the cap changes.

Once per window, when the measured context crosses the warning point, main-agent tool results or prompt submission carry exactly the same budget warning as Pi, ending with `empty_brain` and the countdown. Delivery is consumed only when the downstream chain accepts the warning. The warned flag resets with the window ordinal. Its own `session.compact` hook answers native compaction in the main conversation instead of letting it summarize: an automatic compaction (threshold or prompt-too-long) becomes the freshly built notes boot followed by one randomly selected handoff paragraph (normal only when the model called `empty_brain`, otherwise forced), selected once for the new window and reused on retry — the same boot-then-handoff order as Pi — while user `/compact` and precompute are vetoed with a one-line notice. `prompt.context` still recomputes but does not duplicate that retained boot. The boot's protocol block binds the shared reset sentence to `mcp__notesoup__empty_brain`; Pi's boot text is unchanged. Boot window IDs end in the ordinal `:0`, `:1`, etc.; retained boots restore the ordinal on resume/reload. Original transcript rows remain on disk. `session.end` forgets the old window, and identity is refreshed lazily from `$.session.id()`, `$.session.cwd()` and the documented `$.session.model()` (slugified, with `claude` only as a fallback). `/clear`, `/resume` and `/branch` therefore acquire a boot again without needing another `session.start`. A failed boot build falls back to Claude's own summary, which invalidates a removed retained boot; vetoes, subagent compaction and precompute leave it alone.

The reset flow has three separate lifetimes. `ResetFlow.request` lasts until successful reset or cancellation; it owns the first valid tool call's sticky note and continuation policy. `ResetState.commit` lasts for one native command attempt and tracks proof that Claude kept the boot. The per-turn cutoff can outlive the request when native auto compaction consumes it before the next step. Successful explicit reset and automatic reset share the same window/boot bookkeeping. A failed compaction retains the request for retry; an aborted, failed, or refused main turn cancels it. Session changes discard the flow and invalidate asynchronous work by generation.

`empty_brain` accepts optional `last_word` and boolean `terminate` (default `false`). Non-boolean `terminate` is refused before recording a request. The first valid tool call owns both arguments for that pending reset; duplicates can re-arm the cutoff but cannot replace them. Every tool in the response's batch finishes, including parallel siblings. At the requesting main turn's next `turn.step`, the cutoff yields a synthetic `end_turn` without another model request or aborting tools. The automatic hard point uses the same cutoff and commit path; a budget reset uses the forced handoff pool and continues once only after a confirmed commit, unless manual STOP or natural input takes over. Subagent steps pass through, and subagents cannot reset the main brain. After successful main `turn.complete`, the non-held reset worker compacts; a confirmed commit permits one plugin-origin continuation unless `terminate: true`, manual STOP, or natural input has taken over. The handoff and sticky are already retained alongside the boot and are not repeated by that prompt.

Accepted natural input owns its continuation. Input received within the running turn, including input accepted before the first tool latch, releases the cutoff at the next step for the whole subsequent tool workflow. Input not yet delivered to a model request prevents reset work from starting. A separately queued natural turn also runs normally. No input is replayed. Native auto compaction consumes a pending request without resetting twice, while preserving its armed cutoff until turn completion or natural-input delivery; further auto compaction does not disarm it. Once that reset has completed and natural input releases its cutoff, a new `empty_brain` owns its own sticky and `terminate` policy. A consumed request's STOP does not carry into the new request.

`/empty` and `/fresh-window` request STOP for the same pending or in-flight reset, including one first requested by a tool. While idle they start one hidden normal-model close-out using the shared budget warning, so the model can save notes; while busy they use the current turn. A close-out tool call may select the normal handoff pool and sticky but cannot undo manual STOP. If the warning cannot load, submission fails, or another hook drops it, the request is cancelled so the command can retry. STOP controls automatic continuation; handoff selection still follows actual tool origin (normal) or no tool origin (forced). `DISABLE_COMPACT` refuses explicit requests and suppresses warnings.

Both interactive and headless (`-p` / SDK) resets use `$.command.run({command:"compact",args:...})`. A waiter armed in `session.start` runs this work outside held hooks and re-arms before submitting a close-out, avoiding prompt deadlock and lost completion signals. The command carries a unique instruction marker stored in its commit attempt; only matching manual compaction is admitted, while user `/compact` stays vetoed. Direct `$.session.compact` skips the calling mod's hook interactively and is unavailable headlessly. The command is enqueued before `turn.complete` returns, supporting single-prompt `-p` runs and open SDK streams.

A command receipt and prepared boot are not commit evidence. On Claude Code 2.1.319, interactive completion exposes the installed boot through `$.session.messages()`. Headless completion still exposes the old request snapshot and precedes appends, so the worker waits for successful main `session.append` results: a kept compact boundary followed by the unchanged boot with its UUID. A missing or rewritten boot, veto, or rejected command cannot start continuation. Boot acquisition failure vetoes an explicit reset without advancing the window. After commit, a dropped or failed continuation is reported without compacting again. Native command/output rows remain alongside the fresh boot; no compaction model request is made. A Claude dream backend is not implemented, and budget reporting (`get_context_remaining`) remains Pi-only.

The notes tools also serve subagent calls, using the same session identity and shared operations. The plugin exposes the shared memory and dream skills and the dream playbook; the build keeps these copies byte-identical to the canonical files.

## Persisted history

`history_list` and `history_read` use the same schemas, literal search (`history_list.query`), paging, folding, pairing and text rendering as Pi. With `session` omitted they read the current main session's persisted JSONL, via the documented `classic.SessionStart.transcript_path`; no path is guessed and `$.session.messages()` is not treated as full history. The classic binding survives typed `session.start`, which fires later. Without that path, current-session history calls refuse and the boot says history is unavailable. Subagent history calls refuse rather than silently exposing the main transcript.

Explicit `session` selectors use the [local cross-harness corpus](history-sessions.md), including Pi/Claude/Codex files, without needing the current main transcript to have a head yet. Explicitly selecting the current Claude session still uses its strict live binding. Offline Claude targets prefer non-attachment graph leaves when available, then select the last remaining leaf in transcript order; no leaves still refuses. The persisted Claude decoder is shared under `src/history/claude.ts`; its plugin entry remains in `plugins/claude/src/history.ts`.

The active head comes from the main loop's successful `session.append` result UUID, including attachments and notices. On startup/resume, before such an append, the public API supplies no authoritative selected leaf: history explicitly refuses instead of assuming the last file row or `last-prompt.leafUuid` is current. The append result means the engine kept a row, but its disk write may lag; a head missing on disk is a retryable refusal, not permission to switch branches. Session end clears the binding; a new classic start rebinds it.

Seqs are allocated over first-occurrence persisted append order, before active-branch filtering: one address per logical message/attachment/notice and additional addresses for each tool use/result. Repeated UUID records do not allocate again. List pages skip empty user/assistant envelopes even with explicit roles, so tool-only and thinking-only rows do not crowd out dialogue; direct reads preserve those addresses and explicitly say when no visible text was recorded. Results pair by `tool_use_id`; pending calls remain unfinished, and a paired result's seq redirects to its call. Only text blocks and recorded attachment renderings become visible text; thinking and raw, unrendered attachment payloads are never exposed. Record types other than user, assistant, system and attachment are ignored, including future metadata, without allocating addresses or exposing payloads. If the selected branch requires a skipped record as an ancestor, that UUID is an ancestry gap, not a visible metadata event. Invalid supported message/block schemas, conflicting duplicates, session/sidechain mismatches, cycles and malformed committed JSONL lines still refuse explicitly. An incomplete final unterminated write is tolerated. Valid forward parent references are resolved after reading the snapshot, without reallocating addresses.

The selected tree follows the latest native `parentUuid` links. Compact boundaries also join their `logicalParentUuid` and recorded preserved segments through original ancestry, retaining old events instead of presenting only retained inference context. Auxiliary preservation UUIDs that were never persisted are skipped. Windows use `claude:<sessionId>:<ordinal>`, matching the current boot: native compact boundaries advance the ordinal, and retained notesoup boot markers name that same ordinal rather than advancing it again. A fork preserves the copied boot's ordinal but uses its own session id for internal window tracking. Copied prompt-context boot blocks are rebuilt with a fresh notes snapshot and the current session identity; historical boot messages in the transcript remain unchanged. Preserved duplicate rows keep their original window; new children of their rewritten versions inherit the new window.

A missing required ancestor stops only that traversal edge. Confirmed selected suffixes and explicitly linked compaction history remain queryable; append order never joins a disconnected prefix or selects a sibling. Both history tools prepend a bounded Claude-specific incomplete-history notice, including missing UUID evidence, even for empty selections or query errors. The same pure text wrapper is supplied to shared selection/read budget checks and final delivery, so the complete text including the notice fits 32 KiB without clipping event text or continuation footers afterward. The notice is outside event documents, search matches and read offsets; the shared query schemas are unchanged. Previously allocated seqs remain unchanged if the missing record later appears: its first occurrence takes the next append address, and a fresh projection removes the gap notice when ancestry is complete.

Without a path back to a root or retained boot, window ordinals are unknown. Those events use `claude:<sessionId>:unknown:<missingUuid>` as an explicitly unnumbered segment (possibly spanning native compactions), not an invented ordinal. A subsequent complete retained boot re-anchors its own and later window ordinals without filling the history gap. Incomplete projections track only evidenced windows/segments, not empty placeholders for unseen ordinals. Confirmed numbered windows are sorted by ordinal (including when forward references put newer-window rows on disk first). Unknown segments are tracked separately after the numbered windows; that placement does not assert chronological order for them. Shared tool pairing scans stable append seq order before regrouping events internally, so it does not depend on window/segment presentation order. The helper's reset-facing `history_state` returns the head's independently resolved ordinal, never `windows.length - 1`; if it is unknown, it explicitly refuses rather than supplying a guessed number. Existing reset-hook refusal handling leaves its prior state intact.

## Validation

`npm run typecheck` includes the mod against the vendored Claude Code 2.1.295 contract. `npm test` also typechecks it and runs `claude plugin validate plugins/claude --strict`, skipping validation with a clear message only if `claude` is not installed. Scanner regression tests deliberately reintroduce each of the three forbidden call shapes and require the gate to fail. Unit regressions drain the `turn.step` generator (including its final return value), distinguish a command receipt from deferred kept rows, and cover optional/default/explicit `terminate`, first-request-wins arguments, invalid booleans, manual STOP, STOP consumed by native automatic compaction, accepted natural input, failed commits, submission failures, and session changes.

The reset sequence was also verified with the installed Claude Code 2.1.319 main loop, using a scripted Anthropic service bound to loopback, isolated homes/settings/working directories, fake credentials, and a sandbox blocking external network access and reads of the real home. Single-prompt `-p`, open `stream-json`, and an interactive PTY all produced one clean request after a committed tool reset. The probe also checked parallel fast/slow `Read` siblings, `terminate:true` (one original API request, one commit, no kickoff) and explicit `terminate:false` (one commit followed by one fresh-window API request), manual close-out STOP even when it calls the tool, compaction vetoes, continuation drops, interruption, and natural input delivered within the running turn. These are host integration observations, separate from the unit harness. A skip returned after `next()` has already compacted is rejected by 2.1.319; that compaction stands. Valid vetoes return before calling `next`.

From a built checkout, check the real load with `claude --plugin-dir plugins/claude -p "ok"`; a model/API failure after load is separate from a hooks-module refusal. The package build and `npm pack --dry-run` include the manifests, hooks module, helper, skills, and playbook.
