# Architecture

pi-context 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` | Thin public entrypoint: register the context runtime and tool adapters, and preserve public history/notes re-exports | Pi extension API |
| `context/runtime.ts` | Compose runtime hooks, toggle/reset commands, boot construction, marker/boot boundaries, and provider projection | Pi extension API plus context and notes modules |
| `history/history.ts` | Project branch entries into windows/items using the shared window identity | `SessionReader`, read-only branch and session ID |
| `context/context-window.ts` | Own durable window identity, select the active boot, project provider context, and account for active-window usage | `SessionReader` plus Pi context/system projection APIs |
| `notes/session-replay.ts` | Replay persisted note operations from session entries | `SessionReader`; no filesystem acquisition |
| `notes/address.ts` | Validate virtual note paths and resolve address/glob forms | Note address syntax; no session-branch selection |
| `notes/notes-snapshot.ts` | Acquire the five notes homes once into a closed boot snapshot and isolate filesystem-home failures | Filesystem-backed notes homes; no rendering or UI effects |
| `notes/store.ts` | Read and mutate the filesystem-backed homes; distinguish an absent home from a real read failure | Notes filesystem only; boot acquisition isolates one home at a time |
| `history/history-tools.ts` | Public history schemas and tool results over branch projections | Pi tool API plus read projections |
| `notes/tools.ts` | Filesystem note tool adapters; validate and perform note reads, writes, edits, listings, and searches | Pi tool API plus notes filesystem |
| `context/budget.ts` | Own the default-path per-extension-instance settings cache, read injected policy live, report usable budget, stage guidance/warning drafts, and resolve automatic reset decisions | Pi settings/context hooks plus protocol warning text |
| `context/thresholds.ts` | Read the selected public settings authority and derive reminder/reserve/warning thresholds from Pi's reserve plus the pi-context margins | Pi `SettingsManager`, read-only; no mutable cache or UI effects |
| `settings.ts` | Merge the global and project `pi-context` settings object per key | Parsed Pi settings scopes; no I/O or runtime state |
| `dream/settings.ts` | Validate and resolve the configured dreamer model from the shared settings merge | Pi `SettingsManager`; no runtime context state |
| `context/prompts.ts` | Render the static boot block, note index, and low-budget reminder from explicit data | Snapshot data and protocol text; no filesystem acquisition or UI effects |
| `context/reset-lifecycle.ts` | Own reset requests, turn-end batching, recovery and continuation | Pi lifecycle hooks and injected boundary builder |
| `protocol.ts` | Persisted entry tags, protocol text and defaults | No imports or effects |
| `tool-schema.ts`, `tool-output.ts` | Shared wire-schema primitives and JSON result encoding | No session state |

Dependencies flow from the composition root and tool adapters to projections and protocol constants. The published Pi entry is `dist/extension.js`, built from `src/index.ts`; its public `pi-ai/utils/estimate` dependency is inlined so Pi's root-package aliases cannot misresolve the utility subpath. Host-owned package APIs remain external. Projections cannot send messages, compact, notify, or mutate the session. A runtime framework or generic event bus would add indirection without strengthening these boundaries.

## State and persistence

`turn_end` has one composer in `reset-lifecycle.ts`: it accepts the incoming drafts, drains the budget instance's staged guidance/warning drafts, and only then appends reset drafts. Reset requests are committed after the complete tool batch with `continue: true`, so Pi owns queue scheduling. Repeated `wipe_memory` requests in one batch deduplicate; a later window may still request another reset. Aborts and reset-construction failures preserve already-built drafts without manufacturing a continuation. See [reset lifecycle](reset-lifecycle.md).

The durable boundary is one `pi-context/reset-marker` custom entry with `{ windowId: string }`, followed by one hidden `pi-context/boot` custom message with `details.windowId` equal to the marker identity. The marker is the only window boundary. `context/context-window.ts` owns the marker predicate, active-branch scan, root/current IDs, and per-window message lookup; `history/history.ts` consumes those identity primitives while projecting entries. The scan never uses a global entry tail. Native compaction and branch-summary entries remain history items in the current window, so the old compaction-entry identity is not a window identity.

The runtime in `context/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 `/wipe-memory` 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.

Startup and idle manual resets use `pi.sendMessage(..., { triggerTurn: false })` for the boot. Pi appends it to the session and refreshes context without initiating a model request; during streaming the same call is deferred until the completed tool batch. This is session persistence, not a promise of immediate disk durability: Pi 0.87 defers a new session file until its first assistant message, and the extension API reports send failures through extension errors rather than an awaitable result. Running resets therefore use boundary drafts for ordering and continuation, not `sendMessage`. Drafts are validated together but disk writes are not transactional.

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's `developer` classification records extension authorship, not 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 `pi-context: memory cleared · <windowId>` through `ctx.ui.notify`, after both marker and boot are present in the session. Manual clear is checked immediately; running resets are checked at the next turn start or final settlement. Uncommitted reset drafts never announce success. Boot and continuation messages retain `display: false`.

History reads reconstruct the selected session branch on demand without a cache, so branch navigation cannot expose history from a sibling.

The boot notes index in `notes/notes-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. `context/prompts.ts` then renders that explicit snapshot without reading the filesystem or consulting the clock. MAP bodies and pocket metadata are derived from the same snapshot, so a boot cannot mix two filesystem reads. 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. The window identity, reset/protocol text, and lifecycle boundary are still constructed through the normal runtime path; note reads never mutate files or create fallback state.

`notes/session-replay.ts` accepts only supported operations, safe virtual paths, representable timestamps and results within the UTF-8 size limit. Invalid operations are ignored; they cannot replace a valid note. Notes remain in their filesystem-backed homes, unchanged by session branch navigation.

The `/wipe-memory` command waits for idle, appends the marker and boot with Pi's public `appendEntry`/`sendMessage` APIs, and never calls a model. While enabled, `/compact` is cancelled with an actionable `/wipe-memory` notice. Disabling pi-context stops new automatic resets, but an existing 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, bounded history/notes reads with resumable cursors, notes-home isolation, and settings precedence. The suite is a representative regression set; the standalone coherence, pagination property, history, and threshold suites were removed during test reduction.

Scripted SDK tests execute the real Pi agent loop with no model network request. They inspect provider contexts and durable entries for reset boundaries, mixed-tool completion, queued steering/follow-up delivery, 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 the marker/boot boundary. Queued steering/follow-up messages are delivered exactly once in the new window; they 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.
