# How it works

Technical background for [pi-deepseek-v4-anchor](../README.md): the evidence
behind the two-phase design, the runtime state machine, and where to look when
diagnosing behavior. For usage see the README; for development setup see
[CONTRIBUTING](../CONTRIBUTING.md).

## Why two-phase anchoring exists

DeepSeek V4 Pro conditions strongly on the API-visible tool catalog and the
system-prompt persona (measured on a single private benchmark, Project2, n=2 —
**not** a universal performance claim; verify with your own A/B tests):

| Configuration | Project2 Ability | Trajectory |
|---|---:|---|
| Standard (full catalog + full prompt) | 91 / 92 | `Let me` (standard-like) |
| Minimal (two tools + one-line persona) | 99 / 96 | `We need` (minimal-like) |
| **Anchored Standard (two-phase)** | **98 / 99** | `We need` first, full tools after |

A permanently-Minimal session loses the broad tool set; a permanently-Standard
session loses the trajectory. Anchored Standard gets both.

## The two phases

1. **Request #1 (bootstrap).** Expose only the **real Minimal pair** (`bash` +
   `str_replace_editor`, byte-identical schemas from DeepSeek Harness) and
   replace the system prompt with the Minimal persona
   (`You are a helpful software engineer assistant.`). No AGENTS.md digest, no
   skill-catalog reminder. Output is capped by `bootstrapMaxTokens` (1024 by
   default) so the first turn stays short.
2. **Promotion.** The first durable `tool/call` **or** `assistant/message`
   (`promoteOn`, default `either` — whichever comes first) restores the full
   catalog. The persona stays (DSH's verified configuration), and the stripped
   pi context is re-injected as a hidden user message so the model does not
   lose pi's rules. The cap is removed.

**Tool declaration.** `str_replace_editor` is registered once when the
extension loads (`defaultActive: false`) and declared to the model per run from
`before_agent_start`, because pi builds a run's tool loadout after that hook —
so a `setActiveTools()` call there takes effect for the same request. Anything
that changes the model *after* session creation (picking another model in a
session that started on a non-target one, a subagent session) therefore still
gets the Minimal pair; the declaration follows the current model, not the model
that happened to be active at `session_start`.

**Who keeps the persona.** After promotion the persona stays — but only for a
session that actually bootstrapped. A session that reaches promotion *without*
having anchored (it started on a non-target model, and the switch to a target
model happened later) is sent untouched instead: full catalog, pi's own system
prompt, no output cap. Anchoring is a first-request transformation, so such a
session cannot earn the DSH trajectory anyway — swapping its prompt would only
drop pi's rules, with nothing captured to reinject (`contextReinject` runs
during bootstrap).

The classification is per session. A session whose history was already
non-empty at `session_start` (resume, fork, `pi -c`) is treated as anchored,
because it cannot be classified either way — this keeps resumed and forked
sessions behaving exactly as before. Note that `session_start.reason` cannot be
used for the fresh/resume distinction: resuming at startup reports `startup`
too; the check is the absence of any message entry. The two sets that back this
are deliberately kept across `/reload`, since the same session re-emits
`session_start` there.

**Sticky promotion.** Once promoted, the session stays promoted across
compaction: promotion is re-detected from history, and once seen it is
remembered in-process, so the collapsed history no longer causes an accidental
re-bootstrap.

**Safety nets.** A server 4xx promotes the session (avoids retry loops with a
stripped catalog). Any extension error is swallowed — the request always goes
out. If a configured bootstrap tool is missing from the catalog, **or the host
exposes no tool catalog at all** (`tools: []`, e.g. `--no-tools`), the **whole**
bootstrap is skipped for that request (persona, tool filter and output cap all
left alone), so the request goes out as the host's native one with a warning —
never as "Minimal persona + full catalog".

## Payload format adapters

Providers speak different wire formats. `src/format.ts` detects the payload
format per request and dispatches to an adapter:

- `anthropic-messages` — `system` as a content-block array, `tool_use` blocks
- `openai-chat` — string system / system-role message, `tool_calls`

The state machine itself is format-agnostic. This landed in v1.1.0 (commit
"v4.2"): before that, the Minimal persona swap silently no-op'd on
anthropic-messages payloads and `promoteOn: "tool-call"` could not see
`tool_use` blocks.

## Module map

| Module | Responsibility |
|---|---|
| `index.ts` | Extension entry: event wiring, bootstrap/promotion state machine, session maps, per-run declaration of `str_replace_editor` |
| `src/config.ts` | Presets, settings.json load/merge/validation, pattern matching (glob / regex / exact) |
| `src/format.ts` | Payload format detection + per-format adapters (prompt swap, tool filter, max-token cap) |
| `src/promotion.ts` | Promotion detection from payload history and from pi session entries |
| `src/editor.ts` | `str_replace_editor` tool (DSH Minimal port: view/create/str_replace/insert) |
| `src/menu.ts` | `/anchored-tools` interactive menu + i18n dictionary |
| `src/log.ts` | Startup marker / activity log / debug log helpers |

## Diagnosing behavior

- **Startup marker** — `~/.pi/agent/tmp/anchored-loaded.log`: one line per
  load, with the **global** effective `models` patterns and `enabled` state.
  Check this first to confirm which version is active and what the global config
  resolves to. Project-level `.pi/settings.json` overrides are not visible here
  (they are merged per session), so the marker does not always describe the
  final per-session value — the activity log's per-request lines do.
- **Status bar** — while bootstrapping, `[⛓ bootstrap: bash+str_replace_editor]`
  is shown; it clears on promotion.
- **Activity log** — `~/.pi/agent/tmp/anchored-activity.log`: every request is
  logged with session id, model, tool names, and the decision (`bootstrap` /
  `promoted` / `not-targeted` / `subagent-exempt` / `disabled`), plus tool
  declaration changes (`editor-declared` / `editor-withdrawn`) and, for a
  promoted request, whether the persona was kept or skipped because the session
  never anchored.
- **Debug log** — `~/.pi/agent/tmp/anchored-debug.log` (enable via
  `"debug": true` or `PI_ANCHORED_DEBUG=1`): per-request prompt mode and final
  tool list.

## Upstream lineage

This project builds on several MIT-licensed upstreams — see
[NOTICE](../NOTICE) for license attribution:

| Repository | Role |
|---|---|
| [xiaobright/dsh-anchored-standard](https://github.com/xiaobright/dsh-anchored-standard) | The original DSH anchored-standard preset this project ports to pi (two-phase bootstrap + promote). |
| [xiaobright/modeltest](https://github.com/xiaobright/modeltest) | The Project2 evaluation harness and experimental evidence for the tool-catalog conditioning. |
| [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) | Source of the Minimal persona and `str_replace_editor` schemas (byte-identical here). |
| [kxh4892636/pi-deepseek-anchor](https://github.com/kxh4892636/pi-deepseek-anchor) | Earlier pi port; its `str_replace_editor` implementation was adapted. |
| [vavilonska/SeekAnchor](https://github.com/vavilonska/SeekAnchor) | Multi-host implementation; another `str_replace_editor` reference. |
| [Aurzex/omp-pi-anchored-standard](https://github.com/Aurzex/omp-pi-anchored-standard) | omp + pi implementation; config/menu ideas (settings.json `anchoredTools`, presets). |
