# Architecture

notesoup uses Pi's session branch as the durable source of truth. It does not maintain a second transcript or a hidden prompt overlay. Runtime state exists only to schedule work and reserve a reminder until Pi persists it.

## Ownership

| Module | Responsibility | Boundary |
| --- | --- | --- |
| `index.ts` | The curated host-neutral core surface: the notes library, the boot snapshot/renderer and bindings, the shared operations, and the budget prose | Host entry only; the core never imports a plugin |
| `plugins/pi/index.ts` | The Pi plugin entry: default extension and `createNotesoup` | Pi only; composed from the domains |
| `plugins/claude/src/` | Claude Code helper, persisted-transcript decoding and threshold derivation | Claude only; alongside the self-contained plugin in `plugins/claude/` |
| `notes/identity.ts`, `notes/store.ts`, `notes/address.ts`, `notes/paths.ts` | Explicit `NotesIdentity`, filesystem homes, metadata, addressing and mutations | Node filesystem, no SDK or host environment identity |
| `boot/snapshot.ts` | Load each of the closed five homes once; isolate filesystem errno failures | Explicit identity, captured time and optional scope loader |
| `boot/render.ts`, `boot/text.ts` | Render identity, protocol, maps and recent-note metadata | Explicit snapshot and logical tool-name bindings; no I/O or clock |
| `history/history.ts` | Project decoded events and pair tool calls/results, preserving supplied addresses | Decoded items and branch visibility; never inference messages |
| `history/query.ts` | Bounded paging, `from` selection, literal matching, folding and preview rendering | Query projection only; no native session traversal |
| `budget/constants.ts`, `budget/policy.ts`, `budget/text.ts` | Pure thresholds, unknown/clamped countdown and reminder text | Values supplied by the caller; no settings reads or scheduling |
| `tools/notes.ts`, `tools/history.ts`, `tools/schema.ts`, `tools/result.ts`, `tools/output.ts` | Schema-derived outcomes, note/history operations, bounded result selection and pure text rendering | Explicit identity/projection; one bounded result feeds both text and structured output |
| `settings.ts` | The `notesoup` settings key, the plain-object test and the per-key global/project merge | Raw records only; each host names and validates the fields it consumes |
| `dream/settings.ts` | Dreamer-pattern validation over the merged record | Parsed values only; no SDK or I/O |
| `dream/` | Jail, crumpled-only physical deletion, locks, gates, Git audit, doctor, playbook and result/report logic | Independent filesystem/policy functions; no reverse import into Pi |
| `plugins/dsh/` | Cordis registration, frozen prompt sections, plugin records/checkpoint recovery, token-meter budget, native reset transaction and session-query decoding | Host adapter; imports shared domains, never Pi or Claude |
| `plugins/pi/extension.ts`, `plugins/pi/runtime.ts` | Compose registration, lifecycle hooks, commands, provider operations and UI | Public Pi SDK |
| `plugins/pi/entries.ts` | Persisted native custom-entry/message tags | Pi persistence vocabulary, not an omnibus protocol barrel |
| `plugins/pi/window.ts`, `plugins/pi/session-reader.ts` | Native `WindowMarker`, checkpoint validation, root/fork identity, branch scans, provider projection and usage | Pi entries/messages and read-only native session traversal |
| `plugins/pi/history.ts` | Decode native messages, allocate seqs from append order and select active-branch visibility | Native SessionEntry/AgentMessage decoding; feeds shared query projection |
| `plugins/pi/notes/`, `plugins/pi/history-tools.ts`, `plugins/pi/tool-result.ts` | Extract live execution data, register output schemas, package outcomes and render native note diffs | Native API -> shared operations; no second query or pagination path |
| `plugins/pi/thresholds.ts`, `plugins/pi/budget.ts` | Read settings/usage, cache policy per instance, stage prompts, warn and schedule native reset decisions | Public SettingsManager and native lifecycle |
| `plugins/pi/reset/lifecycle.ts` | Native turn/settle reducer facts, batching, recovery and continuation | Public Pi lifecycle hooks and boundary drafts |
| `plugins/pi/reset/artifacts.ts`, `plugins/pi/reset/committed.ts` | Construct boundaries, repair only safe suffixes, independently confirm committed native boundaries | Active native branch; no shadow commit database |
| `plugins/pi/dream/` | Pi dream sessions, native tool wrappers, settings reads and executable CLI composition | Pi backend over independent dream policy/filesystem functions |

Dependencies flow from a plugin into the core, never back. Core code under `src/` cannot import an SDK or anything under `plugins/`, and a plugin cannot import another plugin's modules or a foreign host SDK; a plugin reaches note storage only through `src/notes/index.ts`. Each plugin owns its host's words: the Pi-only pre-reset hints, the `last_word` nudge and the `/empty <prompt>` close-out live in `plugins/pi/text.ts`, and the Claude wipe, missing-history and warning prose in `plugins/claude/src/text.ts`. `test/dependencies.test.ts` uses TypeScript resolution and AST traversal over every core source and each plugin tree, and tests rejecting indirect, aliased, dynamic and import-type edges. Shared external packages must be declared in the package manifest. `typebox` is declared as a peer and development dependency rather than imported through Pi's SDK.

The configured extension is `plugins/pi/index.ts`, loaded straight from TypeScript — no bundling step, so git installs work without a build. Its `pi-ai/utils/estimate` dependency is vendored in `plugins/pi/estimate.ts` because Pi's root-package aliases misresolve that public subpath on source loads; host-owned SDK APIs stay external. The package root `.` exports the host-neutral core, `./pi` the Pi plugin, `/notes` the standalone library, and `./dsh` plus `./dsh/engine` the Cordis rows. The CLI is `dist/plugins/pi/dream/cli.js`, and the Claude Code plugin is the self-contained `plugins/claude/` directory. Old internal bags, `NotesContext`, root history helpers and the deep-entry export alias are removed. This breaks source/import paths, not storage: the on-disk notes layout is unchanged and host-independent — `pi/session/<sessionId>/` is a historical directory name every host still writes to, not a Pi adapter choice, and nothing migrates it. Note homes, metadata, session paths and raw history remain intact. There is no universal Harness, host-event emulator, registry, daemon or speculative second backend. No `src/window` exists because the current window responsibilities are genuinely native.

For the native dsh reset transaction, prompt ordering, capability checks and limits, see [DeepSeek Harness](dsh.md). Its adapter keeps the shared domains unchanged and uses only dsh public services.

## State and persistence

`plugins/pi/reset/lifecycle.ts` is the sole `turn_end` composer: it accepts incoming drafts, drains budget-owned guidance/warning drafts, and appends a reset boundary for an explicit tool request or the hard-reserve safety path. Manual and budget close-outs stay armed across note/tool turns and commit from `agent_before_settle` after a successful stop with queued work drained; a manual close-out stops the run while a budget close-out continues in the fresh window. Repeated requests for the same window deduplicate. See [reset lifecycle](reset-lifecycle.md).

A new reset is a native empty-summary compaction checkpoint configured to retain none, followed by one `notesoup/reset-marker` custom entry with `{ windowId: string, origin: "normal" | "forced" }`, a hidden `notesoup/boot` whose `details.windowId` matches, and a hidden continuation. The checkpoint is the canonical-history cut; the marker is the durable logical window identity. Raw session entries remain available to history tools. Legacy marker-only branches keep a narrow projection fallback. `plugins/pi/window.ts` owns marker/checkpoint validation, active-branch scan, root/current IDs, and per-window message lookup; `plugins/pi/history.ts` consumes those native identity primitives while decoding entries; shared `history/history.ts` only receives decoded, addressed data. The scan never uses a global entry tail.

The runtime in `plugins/pi/runtime.ts` performs final context projection by selecting the active boot through `details.windowId` and folding only the dropped system prefix through Pi's `getCurrentSystemMessage`. Later prompt patches and new messages stay in order. A missing boot aborts the hook with a safe head and notice rather than silently sending raw history. Startup/tree handling repairs only a genuinely incomplete marker tail: a missing boot with no later raw message, custom message, compaction, branch summary, or authoritative raw boot. If later work exists, boot creation is refused and `/fresh-window` is the explicit recovery path; it does not parse or migrate the legacy reset-v2 protocol.

Boot and reminder deduplication inspect the current branch-local window. Reloading JSONL therefore does not duplicate messages, while navigation to a sibling branch cannot inherit another branch's window state. A fork/clone receives a new session ID while copying its selected path, so startup must also verify that a root boot's `details.windowId` matches the new `rootWindowId(sessionId)` before treating it as present.

Manual `/fresh-window` stores command settlement policy and sends a hidden warning: idle commands start a model turn, running commands steer it. Tool requests are recorded independently and retained under that policy. A bare command settles and stops unless later human input arrives; a command with text settles and continues, with its text supplied to the closing window and carried verbatim into the fresh window. Budget warnings create no reset state: only an explicit tool request, hard reserve or bounded overflow recovery triggers a reset. Abort and ordinary errors do not count as successful settlement. Hard reserve preserves a bare command's stop in a run-scoped latch; `context_with_system` cancels only an empty tool-batch follow-up after confirming the committed boundary. Queued user input is answered before stopping. See [reset lifecycle](reset-lifecycle.md) for the state and recovery rules.

Boot is a fixed snapshot for its window, stored as an extension custom message and converted by Pi to a user-role model message. History internally distinguishes native system summaries and extension-authored custom messages; its public `context` event groups both without implying provider instruction priority. A system-role projection is technically possible through `context_with_system`, but would change the authority of the mixed protocol/MAP content and provider-specific prompt/cache behavior; it is not a prerequisite for persistence. Boot injection is silent, including startup, reload, and boot repair. Only an actual context reset produces a brain-cleared notice through `ctx.ui.notify`, after the verified checkpoint, marker, boot, and continuation are present in the session. Committed resets are checked at the next turn start or final settlement. `plugins/pi/reset/committed.ts` confirms an ordered hidden boot/continuation after a checkpoint-backed marker even when later conversation exists. That is distinct from `inspectResetTail`, which refuses suffix repair after real work or malformed ordering. Uncommitted reset drafts never announce success. Boot and continuation messages retain `display: false`.

The Pi history adapter decodes the selected raw session branch on demand without a cache. It allocates stable seq values from the session's append-order entry list, then selects current-branch-only visibility, so navigation cannot expose a sibling's history. Shared history pairs and queries those decoded events; it neither traverses SessionReader nor reconstructs inference messages. The public stream combines each tool call with its result at the call seq; paired result slots are not public event addresses, and unpaired results retain their own seq. Summaries and injected messages project as `context`. List pages chronologically with seq anchors and an explicit `from`. Tool executions retain structured call/result facts and any native nested-call metadata. A deterministic event document supplies both query-match positions and read windows. Nested calls have no independent seq and never imply that unrecorded child outputs are available.

The boot notes index in `boot/snapshot.ts` is a closed snapshot: the current session, project, human, agent, and model homes are each loaded at most once while constructing a boot. `boot/snapshot.ts` takes explicit `NotesIdentity`, captured `openedAt` and an optional home loader; `boot/render.ts` renders that snapshot and explicit tool bindings without reading the filesystem or consulting the clock. Pi supplies its unchanged tool names. MAP bodies and pocket metadata are derived from the same snapshot, so a boot cannot mix two filesystem reads. Note storage and home traversal use `node:fs/promises`; same-file operations queue by absolute physical filename across store instances in this process, without promising symlink/case-alias or cross-process locking. Known persisted metadata is camelCase. Old snake_case keys are unrecognized extras, not interpreted or migrated; startup does not migrate notes. A missing home (`ENOENT`) is normal. A real read failure omits only that home's index, preserves healthy homes, adds a model-facing `notes_list` recovery notice, and notifies the human once for that window. Boot/reset construction captures session, agent, and model identity before awaiting the snapshot, then checks lifecycle generation, active window, enabled state, and abort status before sending or returning artifacts; stale completions cannot commit into a switched or shut-down session. Boot acquisition uses listing and never updates access metadata. Explicit note reads update `lastAccessed` and `accessCount`; no fallback state is created.

Notes remain in their filesystem-backed homes, unchanged by session branch navigation. Tool reads expose metadata separately from body windows; search/read code-point offsets are body-relative and do not depend on frontmatter serialization or access-counter changes.

Notes/history operations return schema-derived `Outcome<T>` values, not host `content` objects. One bounded candidate is measured against both its complete structured serialization and its text renderer. Pi registers the outcome schema and packages that same value as `structuredContent`, derives `isError` from it and puts rendered text in `content`. Claude uses the same operations and renderers. Neither adapter parses text or independently chooses a page. Read continuation hints follow Pi's native trailing-notice style and are not part of the addressed text. See [tool results](tool-results.md) for the breaking protocol and coordinate contract.

The `/fresh-window` command only waits for idle when the session is busy with non-agent work; during model output it arms the close-out and steers the checkpoint prompt into the running turn. While enabled, `/compact` is cancelled with an actionable `/fresh-window` notice. Disabling notesoup stops new automatic resets, but an existing native checkpoint/marker still excludes earlier history and native compaction is still cancelled on that marked branch; a fresh root may use native Pi semantics. Threshold and warning accounting use active-window provider usage rather than pre-reset global usage. Budget policy and staged prompts are instance-owned, so concurrent sessions cannot share reserve/enablement or uncommitted drafts; default file-backed policy is cached per instance, while injected policy is resolved live on each decision and model/session transitions reset the diagnostic lifecycle.

## Evidence and limits

The retained integration tests use real SessionManager and SettingsManager instances. They check reset-window history retention, seq-addressed and cursor-paged history, bounded history/notes reads with resumable character windows, notes-home isolation, and settings precedence. The suite is representative rather than exhaustive. Focused host-neutral tests additionally run boot/tool/history/budget logic with explicit inputs and no SDK context; resolved-AST guards enforce the dependency boundary.

Scripted SDK tests execute the real Pi agent loop with no model/network request. They inspect provider contexts and durable entries for idle and streaming manual requests, budget close-out, explicit and normal-stop commits, abort/error cleanup, same-window command deduplication, queued steering/follow-up delivery, retain-none checkpoints across two resets and disk reopen, bounded overflow recovery, and settings authority. Smaller dream tests retain lock exclusion/ownership, jailed writes, CLI audit/report failure propagation, and read-only doctor behavior. Removing duplicate scenarios and edge-case matrices reduces coverage; the remaining tests do not establish every malformed-input case, branch interleaving, external-provider behavior, or filesystem failure mode.

Mixed tool batches finish before a direct tool or manual reset boundary. Budget close-outs let queued steering/follow-up work run once in the old window before fallback; turn-end resets let Pi schedule queued work across the new boundary. Scripted loop tests check that these messages are neither dropped nor replayed. Runtime overflow recovery is bounded to one reset/retry per failure chain, while ordinary retryable provider errors remain Pi-owned. When either the source or destination branch has a reset marker, `/tree` navigation still succeeds but its generated summary is replaced by an empty summary plus a notice; raw history and branch selection remain available. If neither branch has a marker, Pi's native tree summary is retained.
