# 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` | Minimal re-export of the Pi extension and `createPiContext` | Host entry only; shared domains cannot import it |
| `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` | Stable-anchor paging, folding and bounded 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`, `dream/settings.ts` | Settings keys, per-key merge and dreamer validation | 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 |
| `pi/extension.ts`, `pi/runtime.ts` | Compose registration, lifecycle hooks, commands, provider operations and UI | Public Pi SDK |
| `pi/entries.ts` | Persisted native custom-entry/message tags | Pi persistence vocabulary, not an omnibus protocol barrel |
| `pi/window.ts`, `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 |
| `pi/history.ts` | Decode native messages, allocate seqs from append order and select active-branch visibility | Native SessionEntry/AgentMessage decoding; feeds shared query projection |
| `pi/notes/`, `pi/history-tools.ts`, `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 |
| `pi/thresholds.ts`, `pi/budget.ts` | Read settings/usage, cache policy per instance, stage prompts, warn and schedule native reset decisions | Public SettingsManager and native lifecycle |
| `pi/reset/lifecycle.ts` | Native turn/settle reducer facts, batching, recovery and continuation | Public Pi lifecycle hooks and boundary drafts |
| `pi/reset/artifacts.ts`, `pi/reset/committed.ts` | Construct boundaries, repair only safe suffixes, independently confirm committed native boundaries | Active native branch; no shadow commit database |
| `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 Pi composition into the domains, never back. Shared code cannot import an SDK, `src/pi`, or the root entry, even through erased types, re-exports, literal dynamic/import-type edges or intermediate modules. `test/dependencies.test.ts` uses TypeScript resolution and AST traversal, checks every shared source (including root settings/text matching), and tests rejecting indirect/aliased paths. 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 `dist/extension.js`, built from the minimal `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 SDK APIs remain external. `/notes` is the intentional standalone library API. The CLI is `dist/src/pi/dream/cli.js`. Old internal bags, `NotesContext`, root history helpers and the deep-entry export alias are removed. This breaks source/import paths, not storage: 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.

## State and persistence

`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 `pi-context/reset-marker` custom entry with `{ windowId: string }`, a hidden `pi-context/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. `pi/window.ts` owns marker/checkpoint validation, active-branch scan, root/current IDs, and per-window message lookup; `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 `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 `/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.

Manual `/wipe-memory` arms a close-out and always sends the hidden checkpoint prompt: when idle it starts one ordinary model turn, while streaming it steers the running turn so the agent closes out promptly. The agent may span several note/tool turns; the reset commits at a successful settlement after queued work drains, and the run stops rather than continuing in the fresh window. That prompt and the budget warning share the same text but have distinct persisted types, so an aborted manual request cannot suppress a later budget warning. The budget warning remains a multi-turn close-out with an explicit `wipe_memory` commit or a successful normal-stop fallback. Abort and ordinary errors never count as successful close-out. Manual overflow commits a reset without recovery continuation, even with automatic reset disabled. A hard-reserve reset preserves the manual stop in a run-scoped `stop-pending` phase; the lifecycle's `context_with_system` hook verifies the actual committed boundary and cancels an empty tool-batch follow-up through public `ctx.abort()`. Pending or already-delivered user input bypasses cancellation and is answered before settlement stops without a second wipe. The checkpoint, marker, boot, and continuation are emitted as ordered session-boundary drafts. Pi's retain-none compaction entry persists the canonical cut; the marker/boot preserve pi-context's logical identity and protocol.

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 `pi-context: memory cleared · <windowId>` 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. `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 and search page chronologically with seq anchors. Tool executions retain structured call/result facts and any native nested-call metadata. A deterministic event document supplies both search 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 `/wipe-memory` 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 `/wipe-memory` notice. Disabling pi-context 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 anchor-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.
