auto-model-router 0.2.2 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude/skills/agentdox/SKILL.md +143 -0
  2. package/.mcp.json +11 -0
  3. package/.omp-plugin/marketplace.json +2 -2
  4. package/CLAUDE.md +129 -0
  5. package/README.md +64 -0
  6. package/docs/AGENTDOX-BRIDGE.md +132 -0
  7. package/docs/context-optimization.md +362 -0
  8. package/omp-extension/embed-logic.ts +31 -0
  9. package/omp-extension/router-embed.ts +7 -1
  10. package/package.json +1 -1
  11. package/src/cli/config-cmd.ts +20 -5
  12. package/src/cli/explain.ts +1 -0
  13. package/src/config/defaults.ts +33 -1
  14. package/src/config/load.ts +13 -0
  15. package/src/config/schema.ts +27 -0
  16. package/src/config/types.ts +79 -5
  17. package/src/context/agentdox.ts +113 -0
  18. package/src/context/bridge.ts +166 -0
  19. package/src/context/index.ts +33 -0
  20. package/src/context/store.ts +82 -0
  21. package/src/context/types.ts +78 -0
  22. package/src/cost/ledger.ts +32 -12
  23. package/src/cost/types.ts +15 -4
  24. package/src/router/candidates.ts +19 -8
  25. package/src/router/classify.ts +26 -12
  26. package/src/router/compaction.ts +163 -0
  27. package/src/router/features.ts +26 -13
  28. package/src/router/select.ts +37 -4
  29. package/src/router/state.ts +12 -2
  30. package/src/router/types.ts +33 -1
  31. package/src/server/http.ts +18 -1
  32. package/src/server/turn.ts +88 -1
  33. package/src/upstream/openrouter.ts +8 -1
  34. package/src/util/sqlite.ts +34 -1
  35. package/src/wire/openai/request.ts +86 -1
  36. package/src/wire/types.ts +36 -0
  37. package/test/classify.test.ts +63 -5
  38. package/test/compaction.test.ts +148 -0
  39. package/test/context-bridge.test.ts +337 -0
  40. package/test/embed-logic.test.ts +32 -0
  41. package/test/escalate.test.ts +1 -0
  42. package/test/exploration.test.ts +6 -2
  43. package/test/failover.test.ts +51 -6
  44. package/test/features.test.ts +45 -0
  45. package/test/helpers/inject.ts +23 -0
  46. package/test/hold-exploration.test.ts +4 -2
  47. package/test/select.test.ts +86 -3
  48. package/test/tokens.test.ts +1 -0
  49. package/test/trust-attribution.test.ts +32 -8
  50. package/test/turn.test.ts +20 -10
  51. package/tools/agentdox-e2e.ts +123 -0
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: agentdox
3
+ description: "Use agentdox — the shared memory, docs, and context server — the same way every session. Trigger on connect in any repo whose CLAUDE.md or .env.agentdox names an agentdox scope, and whenever the user mentions agentdox, project memory, remembering/recalling facts, project docs, the project brief, decisions, or session history. Also trigger BEFORE asking the user something they may have already told you, and BEFORE finishing any task that changed architecture, conventions, or decisions."
4
+ ---
5
+
6
+ # agentdox — the standard interaction protocol
7
+
8
+ agentdox is a shared memory + docs + conversation store at `http://localhost:3003`. It is what
9
+ stops you rediscovering the same project facts every session.
10
+
11
+ **Follow this protocol identically every time.** Consistency is the point: the value of the
12
+ store collapses if each session writes it differently.
13
+
14
+ ## 0. Resolve the scope before anything else
15
+
16
+ Everything is namespaced by a **scope** = the project slug. Get it, in this order:
17
+
18
+ 1. `AGENTDOX_SCOPE` in the repo's `.env.agentdox`
19
+ 2. The slug named in the repo's `CLAUDE.md`
20
+ 3. List projects and match the repo name
21
+
22
+ Known scopes: `ashlands` (E:/projects/ashlands/ashlands), `omp-router` (E:/projects/omp-router).
23
+
24
+ **Never write outside your scope.** If you cannot determine it, ask — do not guess, and do not
25
+ fall back to a default.
26
+
27
+ ## 1. Pick your transport — MCP or REST
28
+
29
+ Both hit the same live store with the same RBAC. **Check which you have, then use it:**
30
+
31
+ - **MCP tools present** — use them. Claude Code and Cursor mount them as `memory_add`,
32
+ `context_assemble`, …; **omp mounts them prefixed**, e.g. `agentdox_memory_add`,
33
+ `agentdox_context_assemble`. omp reads `.mcp.json` (repo root), `.omp/mcp.json`,
34
+ `.claude/mcp.json`, and `~/.omp/agent/mcp.json`, and it **does** expand `${VAR}` in headers.
35
+ - **No such tools** — **use the REST API directly.** Never skip recording just because MCP
36
+ tools are absent; that is the most likely way this protocol silently stops happening.
37
+
38
+ If you expected MCP tools and don't have them, the usual cause is the bearer env var missing
39
+ from the **launching shell's** environment. A Windows *User*-scope variable only reaches
40
+ processes started after it was set, so an already-running terminal won't have it. Either
41
+ restart the shell/harness or fall back to REST for this session — don't just skip the writes.
42
+
43
+ REST auth: `Authorization: Bearer <token>`, where the token is `AGENTDOX_TOKEN` from the
44
+ repo's `.env.agentdox`.
45
+
46
+ Cleanest REST call path — a throwaway `bun` script, which avoids PowerShell mangling `$` in
47
+ inline JSON and avoids quoting pain in `curl`:
48
+
49
+ ```ts
50
+ const tok = /AGENTDOX_TOKEN=(.+)/.exec(await Bun.file(".env.agentdox").text())?.[1]?.trim() ?? "";
51
+ const H = { Authorization: `Bearer ${tok}`, "content-type": "application/json" };
52
+ await fetch("http://localhost:3003/memory", { method: "POST", headers: H,
53
+ body: JSON.stringify({ content: "…", category: "<scope>", importance: 0.9 }) });
54
+ ```
55
+
56
+ ## 2. On connect (every session, before other work)
57
+
58
+ | Step | MCP | REST |
59
+ | --- | --- | --- |
60
+ | Ensure the project | `project_ensure {slug, name}` | `POST /projects {slug,name}` (idempotent) |
61
+ | Read the brief | `context_brief {scope}` | `GET /context/brief?scope=<scope>` |
62
+
63
+ The brief is the cumulative on-ramp: overview, repo layout, code style, build/test,
64
+ conventions, gotchas, decision log. **Read it before exploring the repo** — it exists so you
65
+ don't rediscover what a previous session already established.
66
+
67
+ `404 no_brief` means none exists yet: seed it once with `context_brief_seed {scope}` /
68
+ `POST /context/brief/seed {scope}`.
69
+
70
+ ## 3. Before asking the user anything
71
+
72
+ | MCP | REST |
73
+ | --- | --- |
74
+ | `context_assemble {scope, query}` | `POST /context/assemble {scope, query}` |
75
+
76
+ Returns relevant memory + docs + recent conversation as one block. **Consult it before
77
+ re-asking the user about anything that might already be recorded.** Re-asking a question the
78
+ store already answers is the specific failure this system exists to prevent.
79
+
80
+ ## 4. During work — append the session in real time
81
+
82
+ | MCP | REST |
83
+ | --- | --- |
84
+ | `session_start {scope, title}` | `POST /sessions {scope, title}` → returns `id` |
85
+ | `session_append {session_id, role, content}` | `POST /sessions/:id/messages {role, content, refs?}` |
86
+ | — | `POST /sessions/:id/end` when the topic closes |
87
+
88
+ Append **as the conversation happens**, not as one summary at the end. Live history is what
89
+ context assembly draws on for the next session.
90
+
91
+ ## 5. Before finishing any task (mandatory)
92
+
93
+ Updating agentdox is part of completing a task, not an optional extra. Do not close out a task
94
+ while memory, docs, or the brief for the area you touched is stale.
95
+
96
+ | What changed | MCP | REST |
97
+ | --- | --- | --- |
98
+ | A fact you already stored | `memory_update {id, …}` | `PATCH /memory/:id {content?, importance?}` |
99
+ | A new durable fact | `memory_add {content, category, importance}` | `POST /memory {content, category, importance}` |
100
+ | Find what exists first | `memory_search {query, category}` | `GET /memory/search?q=…&category=<scope>` · `GET /memory?category=<scope>` |
101
+ | Architecture / conventions | `docs_update {id, content}` | `PATCH /docs/:id {title?, content?, tags?}` |
102
+ | A genuinely new doc | `docs_write {slug, title, content, scope}` | `POST /docs {slug, title, content, scope}` |
103
+ | List / read docs | `docs_read` · `docs_search` | `GET /docs?scope=<scope>` · `GET /docs/search?q=…` · `GET /docs/slug/:slug` |
104
+ | A decision you made | `context_brief_record {scope, title, decision, rationale}` | `POST /context/brief/decision {scope, title, decision, rationale}` |
105
+ | Edit brief sections | — | `PUT /context/brief {scope, overview?, repoLayout?, codeStyle?, buildTest?, assetConventions?, gotchas?}` |
106
+
107
+ **Search before you add.** Update the existing entry rather than leaving two contradictory
108
+ facts. Record the *why* of a decision, not just the *what*.
109
+
110
+ ## Two inconsistencies that cause silent mistakes
111
+
112
+ 1. **Memory uses `category`; everything else uses `scope`.** `memory_add` / `memory_search` /
113
+ `memory_update` (and `POST /memory`, `GET /memory`) take `category`. Passing `scope` to a
114
+ memory call leaves the entry unscoped, where nothing will ever find it again. `category`
115
+ **is** the scope — set it every time.
116
+ 2. **`session_append` uses `session_id`** (snake_case), and `context_assemble` uses
117
+ `memory_limit` / `docs_limit` / `session_limit`. Everything else uses plain names.
118
+
119
+ ## Writing good entries
120
+
121
+ - **Memory is high-signal and compact.** One fact per entry. Prefer editing an existing entry
122
+ over piling on near-duplicates. Set `importance` deliberately: 0.9+ for things that change
123
+ how work is done, 0.5 for ordinary context.
124
+ - **Do not store what the repo already records.** Code structure, file listings, and git
125
+ history are discoverable. Store what is *not* in the code: why a decision was made, a user
126
+ preference, a constraint, a gotcha that cost time.
127
+ - **Docs are versioned** (`GET /docs/:id/history`), so update freely rather than hedging.
128
+
129
+ ## When agentdox fails
130
+
131
+ - **401** → the bearer token env var referenced by `.mcp.json` is missing from the
132
+ environment. Re-set it from the repo's `.env.agentdox` and restart the harness (`${VAR}`
133
+ substitution happens once, at MCP-server startup).
134
+ - **403** → you are writing outside your granted scope. Re-check the slug.
135
+ - **Connection refused** → the `agentdox-server` Docker container is not running.
136
+
137
+ Report the failure rather than proceeding as if the store were up to date.
138
+
139
+ ---
140
+
141
+ *Canonical copy: `~/.claude/skills/agentdox/SKILL.md`. omp only discovers skills from
142
+ **project-relative** dirs (`.claude/skills/`, `.omp/skills`, `.agent/skills`, …), so this file
143
+ is copied into each participating repo. Edit the canonical copy, then re-copy.*
package/.mcp.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "agentdox": {
4
+ "type": "http",
5
+ "url": "http://localhost:3003/mcp",
6
+ "headers": {
7
+ "Authorization": "Bearer ${AGENTDOX_OMP_TOKEN}"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -7,14 +7,14 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "auto-model-router: a local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
10
- "version": "0.2.2",
10
+ "version": "0.2.7",
11
11
  "pluginRoot": "."
12
12
  },
13
13
  "plugins": [
14
14
  {
15
15
  "name": "auto-model-router",
16
16
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter. Runs in-process, routes per turn by price and task complexity, with budget caps, mid-stream escalation, and cache-aware hysteresis.",
17
- "version": "0.2.2",
17
+ "version": "0.2.7",
18
18
  "author": {
19
19
  "name": "drewappling",
20
20
  "email": "drewappling@gmail.com"
package/CLAUDE.md ADDED
@@ -0,0 +1,129 @@
1
+ # auto-model-router — project memory & conventions
2
+
3
+ `auto-model-router` is a local, cost/complexity-aware model router for [Oh My Pi](https://github.com/oh-my-pi).
4
+ It presents one keyless OpenAI-compatible provider and picks a concrete OpenRouter model
5
+ **per turn**. This file is loaded by coding agents (Claude Code, Cursor) as project memory.
6
+
7
+ > **Repo identity:** this directory (`E:/projects/omp-router`) is the **main** repo.
8
+ > `E:/projects/auto-model-router-research` is a *separate divergent checkout* kept for other
9
+ > feature work — do not edit it expecting changes here. omp's `~/.omp/agent/config.yml`
10
+ > `extensions:` list points at **this** repo.
11
+
12
+ ## agentdox — shared context/memory (**MANDATORY to keep updated**)
13
+
14
+ agentdox is this repo's memory + docs + live-conversation system. The project slug is
15
+ **`omp-router`** — ALWAYS scope agentdox writes to it. The HTTP MCP server in `.mcp.json`
16
+ uses **`AGENTDOX_OMP_TOKEN`** (a bearer token with admin on the `omp-router` scope only).
17
+
18
+ **Where the credentials live:**
19
+
20
+ | What | Where |
21
+ | --- | --- |
22
+ | Token + URL + scope | `.env.agentdox` in this repo root (**gitignored** via `.env.*` — never commit) |
23
+ | What `.mcp.json` reads | the `AGENTDOX_OMP_TOKEN` **environment variable**, not the file |
24
+ | Persisted env value | Windows **User** environment (`[Environment]::GetEnvironmentVariable('AGENTDOX_OMP_TOKEN','User')`) |
25
+ | Server | `http://localhost:3003` — Docker container `agentdox-server` |
26
+ | Admin token (to re-mint scoped PATs) | `E:/projects/agentdox/deploy/.env` |
27
+
28
+ `.env.agentdox` is the durable record; the environment variable is what Claude Code actually
29
+ substitutes into `.mcp.json` at MCP-server startup. If agentdox MCP returns **401**, the
30
+ variable is missing from the environment — re-set it from `.env.agentdox` and restart Claude
31
+ Code (substitution happens once, at startup). Re-mint instructions are in `.env.agentdox`.
32
+
33
+ **Requirement: keeping agentdox current is part of completing any task, not optional.**
34
+ Do NOT close out a task while memory, docs, or conversation history for the area you touched
35
+ is stale or incomplete.
36
+
37
+ Concrete duties (all scoped to `omp-router`):
38
+
39
+ - **On connect:** run `project_ensure` with `slug: "omp-router"` before any memory/docs work.
40
+ - **On startup:** read `context_brief` to onboard on decisions, conventions, and gotchas
41
+ before rediscovering them.
42
+ - **Memory** (`memory_add` / `memory_update` / `memory_search`): record user-stated
43
+ preferences and corrections. When a fact changes, UPDATE the existing entry — never leave
44
+ contradictory facts. Keep entries compact and high-signal.
45
+ - **Docs** (`docs_write` / `docs_update`): keep architecture and decisions current as reality
46
+ changes; writing once is not enough.
47
+ - **Sessions** (`session_start` / `session_append`): append messages in real time, not as an
48
+ end-of-task summary.
49
+ - **Context** (`context_assemble`): consult it (with a query) before re-asking the user about
50
+ anything already captured.
51
+ - **Decisions** (`context_brief_record`): record decisions and conventions as they are made.
52
+
53
+ ### How to actually call it (MCP tools vs REST)
54
+
55
+ The duties above name the **MCP tools** (`memory_add`, `docs_write`, `context_brief_record`, …),
56
+ served from this repo's `.mcp.json`.
57
+
58
+ **omp gets these tools too** — verified 2026-08-28. omp reads `.mcp.json` (repo root),
59
+ `.omp/mcp.json`, `.claude/mcp.json`, and `~/.omp/agent/mcp.json`, and it expands `${VAR}` in
60
+ headers. It mounts them **prefixed**: `agentdox_memory_add`, `agentdox_context_assemble`, …
61
+ (fully qualified `mcp__agentdox_*`). All 17 tools load.
62
+
63
+ The one prerequisite is `AGENTDOX_OMP_TOKEN` being present in the **launching shell's**
64
+ environment. It is persisted at Windows *User* scope, so only shells started afterwards
65
+ inherit it — an already-open terminal will show no agentdox tools until restarted.
66
+
67
+ **A harness genuinely without those tools (e.g. Hermes, or omp before the env var is
68
+ inherited) MUST use the REST API directly** — same live store, same RBAC. Don't skip recording
69
+ just because the MCP tools are absent.
70
+
71
+ REST basics: base `http://localhost:3003`, header `Authorization: Bearer <token>` where the token
72
+ is `AGENTDOX_TOKEN` from `.env.agentdox` (admin on the `omp-router` scope). **memory uses
73
+ `category`, everything else uses `scope`; both are always `"omp-router"`.** MCP-tool → REST map:
74
+
75
+ | Duty / MCP tool | REST |
76
+ | --- | --- |
77
+ | `project_ensure` | `POST /projects` `{slug,name}` (idempotent; re-ensure returns the project) |
78
+ | `memory_search` | `GET /memory?category=omp-router&limit=N` · `GET /memory/search?q=…&category=omp-router` |
79
+ | `memory_add` | `POST /memory` `{content, category:"omp-router", importance:0..1}` |
80
+ | `memory_update` | `PATCH /memory/:id` `{content?, importance?, …}` (edit in place — never pile on dupes) |
81
+ | `docs_write` | `POST /docs` `{slug, title, content, scope:"omp-router", tags?}` |
82
+ | `docs_update` | `PATCH /docs/:id` `{title?, content?, tags?}` · list `GET /docs?scope=omp-router` |
83
+ | read `context_brief` | `GET /context/brief?scope=omp-router` (404 `no_brief` until seeded) |
84
+ | seed the brief | `PUT /context/brief` `{scope, overview?, repoLayout?, codeStyle?, buildTest?, assetConventions?, gotchas?}` |
85
+ | `context_brief_record` | `POST /context/brief/decision` `{scope, title, decision, rationale}` |
86
+ | `context_assemble` | `POST /context/assemble` `{scope, query}` · baseline `GET /context/snapshot?scope=` · `POST /context/refresh` `{scope}` |
87
+ | `session_start` / `session_append` | `POST /sessions` `{scope, title}` → `POST /sessions/:id/messages` `{role, content, refs?}` → `POST /sessions/:id/end` |
88
+
89
+ Cleanest call path (avoids Windows PowerShell mangling `$` in inline JSON): a throwaway
90
+ `bun` script that reads the token and `fetch`es —
91
+
92
+ ```ts
93
+ const tok = /AGENTDOX_TOKEN=(.+)/.exec(await Bun.file(".env.agentdox").text())?.[1]?.trim() ?? "";
94
+ const H = { Authorization: `Bearer ${tok}`, "content-type": "application/json" };
95
+ await fetch("http://localhost:3003/memory", { method: "POST", headers: H,
96
+ body: JSON.stringify({ content: "…", category: "omp-router", importance: 0.9 }) });
97
+ ```
98
+
99
+ Endpoints are defined in `E:/projects/agentdox/packages/server/src/index.ts`; the router's own
100
+ read/write client is `src/context/agentdox.ts` (assemble / createSession / append only).
101
+
102
+ ## This repo also *implements* an agentdox client
103
+
104
+ Beyond consuming agentdox as an agent, `src/context/` is the **router↔agentdox bridge**: it
105
+ injects shared project context into every routed turn and records turns back, attributed to
106
+ the model that served them. See `docs/AGENTDOX-BRIDGE.md` for the current state, how to run
107
+ it, and the open issue. Design rationale lives in
108
+ `E:/projects/agentdox/docs/architecture/router-context-bridge.md`.
109
+
110
+ Turning the bridge on for the router itself (distinct from the MCP wiring above):
111
+
112
+ ```bash
113
+ export AGENTDOX_URL=http://localhost:3003
114
+ export AGENTDOX_TOKEN=<same PAT as AGENTDOX_OMP_TOKEN>
115
+ export AGENTDOX_SCOPE=omp-router
116
+ ```
117
+
118
+ A URL + token is enough to enable it; `GET /health` on the router confirms.
119
+
120
+ ## Conventions
121
+
122
+ - **Bun + TypeScript**, `exactOptionalPropertyTypes: true`. Use `...(x === undefined ? {} : { x })`
123
+ rather than assigning `undefined` to an optional property.
124
+ - **`src/util/sqlite.ts` is the ONLY migration path.** New tables go in the idempotent
125
+ `MIGRATIONS` block; column additions get a `MIGRATE_Vn` const plus a `PRAGMA table_info`
126
+ guard, and `USER_VERSION` is bumped. `test/trust-attribution.test.ts` asserts the version.
127
+ - **bun:sqlite named params must be written `$name`** in the bind object. Bare keys bind
128
+ nothing and every column silently lands NULL.
129
+ - Verify with `bunx tsc --noEmit` and `bun test` (406+ tests) before declaring done.
package/README.md CHANGED
@@ -566,6 +566,70 @@ harness's reliability from only its own ledger rows.
566
566
 
567
567
  ---
568
568
 
569
+ ## Shared project context across model switches (agentdox)
570
+
571
+ Switching models mid-conversation loses more than a prompt cache: the new model
572
+ has none of the project knowledge the last one built up. Because every harness
573
+ routes through this one provider, the router is the single place that can fix
574
+ that for all of them at once.
575
+
576
+ Point it at an [agentdox](https://github.com/…/agentdox) server and every turn —
577
+ whatever model wins the routing decision — carries the same project memory, docs,
578
+ and brief:
579
+
580
+ ```bash
581
+ export AGENTDOX_URL=http://localhost:3003
582
+ export AGENTDOX_TOKEN=<pat with read+write on the scope>
583
+ export AGENTDOX_SCOPE=ashlands # optional; the omp extension derives it from the workspace
584
+ ```
585
+
586
+ Setting a URL and a token is enough to turn it on.
587
+
588
+ ### It does not cost you a cache miss per turn
589
+
590
+ The context block sits at the front of the prompt, so re-fetching it every turn
591
+ would invalidate the cached prefix every turn — costing far more than routing
592
+ saves. Instead a block is **pinned per conversation** and refreshed only when the
593
+ prefix is already cold:
594
+
595
+ | Trigger | Cache cost |
596
+ | --- | --- |
597
+ | First turn of a conversation | none — nothing is warm yet |
598
+ | The router switches model | none — already forfeited by the switch |
599
+ | Escalation or failover retry | none — a new dispatch is cold anyway |
600
+ | Staleness TTL (`context.maxStalenessMs`, default 900s) | paid once |
601
+
602
+ Between those moments the identical bytes are re-injected and the cache holds.
603
+ The refresh rides on a cache miss that was happening regardless — which is why
604
+ "context follows the model switch" is nearly free.
605
+
606
+ A block is versioned by **content hash**, not by agentdox's `assembledAt`.
607
+ agentdox re-assembles on a timer, so a timestamp would change on every tick and
608
+ break a warm cache for nothing; an unchanged re-assembly hashes identically and
609
+ costs nothing.
610
+
611
+ The block is appended to the **last system message** rather than inserted as a
612
+ new one, so the cache-breakpoint indices the core computed stay valid and the
613
+ block lands inside the prefix `planCacheBreakpoints` already marks.
614
+
615
+ ### Turns are recorded back, attributed to the model that served them
616
+
617
+ With `context.recordTurns` (default on), each settled turn is written to an
618
+ agentdox session tagged `model:<slug>` and `tier:<tier>` — a transcript that
619
+ shows which model produced which turn. Those messages feed back into the next
620
+ `context_assemble`, so the model you switch *to* inherits what the model you
621
+ switched *from* actually did.
622
+
623
+ Write-backs are queued, bounded, and never awaited: agentdox is an enrichment,
624
+ not a dependency. If it is unreachable the turn routes and dispatches normally,
625
+ and a pinned block keeps being served.
626
+
627
+ `GET /health` reports the bridge's URL, default scope, and `recordTurns` — never
628
+ the token. Design notes: `docs/architecture/router-context-bridge.md` in the
629
+ agentdox repo. Live check: `bun tools/agentdox-e2e.ts`.
630
+
631
+ ---
632
+
569
633
  ## Toast notifications for the chosen model
570
634
 
571
635
  auto-model-router is headless and cannot draw into omp's TUI, so chosen-model toasts
@@ -0,0 +1,132 @@
1
+ # agentdox bridge — handoff
2
+
3
+ **Status:** implemented, typechecks clean, 409 tests pass, injection verified end-to-end
4
+ through omp. **One open bug** in the write-back path (§5). Pick up there.
5
+
6
+ Design rationale (why it is built this way):
7
+ `E:/projects/agentdox/docs/architecture/router-context-bridge.md`.
8
+
9
+ ---
10
+
11
+ ## 1. What it does
12
+
13
+ The router is the only choke point that sees every model in every harness. The bridge
14
+ injects an agentdox project-context block into every routed turn, so switching models never
15
+ loses project memory/docs/brief — and records each settled turn back to agentdox, attributed
16
+ to the model that served it.
17
+
18
+ The load-bearing constraint is the prompt cache: a block that changes per turn sits at the
19
+ front of the prefix and would make every turn a full cache miss. So a block is **pinned per
20
+ conversation** and refreshed only when the prefix is already cold (first turn, model switch,
21
+ retry, or staleness TTL). Version is a **content hash**, not agentdox's `assembledAt`, so an
22
+ unchanged re-assembly keeps the same bytes and the cache survives.
23
+
24
+ ## 2. Where the code is
25
+
26
+ | Path | Role |
27
+ | --- | --- |
28
+ | `src/context/types.ts` | Contracts (`ContextBridge`, `ContextPin`, `TurnRecord`) |
29
+ | `src/context/agentdox.ts` | REST client. Total: every method returns a value or null, never throws |
30
+ | `src/context/bridge.ts` | **The refresh policy.** `shouldRefresh` is the heart of it |
31
+ | `src/context/store.ts` | `context_blocks` (content-addressed) + `agentdox_sessions` |
32
+ | `src/context/index.ts` | `createBridgeFromConfig` — returns an inert bridge when unconfigured |
33
+ | `src/server/turn.ts` | Resolve → inject → pin → record. Two `log.debug("agentdox …")` lines |
34
+ | `src/wire/openai/request.ts` | `injectContextBlock` + `x-agentdox-scope` header parsing |
35
+ | `src/util/sqlite.ts` | `USER_VERSION` 11, `MIGRATE_V11` |
36
+ | `test/context-bridge.test.ts` | 14 tests: every refresh trigger, restart survival, degradation |
37
+ | `tools/agentdox-e2e.ts` | Live check against a running agentdox server |
38
+
39
+ Injection appends to the **last system message** rather than inserting one — inserting would
40
+ shift every `cacheBreakpointMessageIndices` entry the core computed, and appending lands the
41
+ block inside the prefix `planCacheBreakpoints` already marks.
42
+
43
+ ## 3. Running it
44
+
45
+ ```bash
46
+ export AGENTDOX_URL=http://localhost:3003
47
+ export AGENTDOX_TOKEN=<PAT with write on the scope> # see .env.agentdox
48
+ export AGENTDOX_SCOPE=omp-router
49
+ bun src/index.ts serve --port 8799 # standalone
50
+ curl -s http://127.0.0.1:8799/health # confirms the bridge block
51
+ bun tools/agentdox-e2e.ts # live end-to-end
52
+ ```
53
+
54
+ `AUTO_MODEL_ROUTER_LOG=debug` surfaces two lines per turn:
55
+
56
+ ```
57
+ DEBUG agentdox context active=true scope=omp-router injected=true chars=1683
58
+ DEBUG agentdox record turn userChars=109 assistantChars=4 messages=2 roles=system,user
59
+ ```
60
+
61
+ Use `AUTO_MODEL_ROUTER_DB=<scratch>.db` to avoid touching the live `~/.auto-model-router/router.db`.
62
+
63
+ ## 4. Testing through omp — the gotcha that cost an hour
64
+
65
+ `omp -p` (headless) **never binds its own router**. Per `omp-extension/router-embed.ts`, only
66
+ a session with `ctx.hasUI` binds; headless sessions read `$AUTO_MODEL_ROUTER_HOME/embed.port`
67
+ and register the provider against whatever port is in that file. So a headless run silently
68
+ routes to whatever router is already running — including a stale one with old code.
69
+
70
+ To test *your* build through omp:
71
+
72
+ ```bash
73
+ cp ~/.auto-model-router/embed.port ~/.auto-model-router/embed.port.bak
74
+ printf '8799' > ~/.auto-model-router/embed.port # point at your standalone router
75
+ AGENTDOX_URL=… AGENTDOX_TOKEN=… AGENTDOX_SCOPE=omp-router \
76
+ omp -p --no-tools --no-session "…"
77
+ cp ~/.auto-model-router/embed.port.bak ~/.auto-model-router/embed.port # restore
78
+ ```
79
+
80
+ Two more traps hit during this work:
81
+
82
+ - `pkill -f "src/index.ts serve"` does **not** work in Git Bash on Windows. The old process
83
+ keeps the port, the new one prints `Failed to start server. Is port 8799 in use?` to its
84
+ log, and you spend a while testing stale code. Kill via
85
+ `Get-NetTCPConnection -LocalPort 8799 -State Listen` → `Stop-Process -Force`.
86
+ - Long-running interactive omp sessions hold their own embedded routers from whenever they
87
+ started. Check `Get-Process omp` before trusting a result.
88
+
89
+ ## 5. OPEN BUG — assistant text is under-captured on the omp path
90
+
91
+ **Verified working:** injection reaches the model through omp. The dispatched system message
92
+ was confirmed to contain the block (`containsBlock=true`), and on a direct
93
+ `/v1/chat/completions` dispatch the model answered *from* the injected memory, verbatim:
94
+
95
+ > "The router pins one agentdox context block per conversation, refreshing it only on model
96
+ > switches, retries, or TTL."
97
+
98
+ **Broken:** through omp, the recorded assistant turn is near-empty — `assistantChars=4`
99
+ (literally `" high"`) while omp displayed several paragraphs. The session and the model
100
+ attribution (`refs: ["model:…", "tier:…"]`) are written correctly; only the assistant
101
+ *content* is wrong.
102
+
103
+ `assistantText` is accumulated in `src/server/turn.ts` from `ev.type === "text"` deltas
104
+ inside the chunk loop. Leads, roughly in order of suspicion:
105
+
106
+ 1. **omp issues more than one upstream request per visible turn** (e.g. a title/summary call
107
+ on the `smol` role, which also resolves to `auto` → the router). The 4-char record may be
108
+ an auxiliary request, with the real answer on a different conversation key. Check by
109
+ logging `conversationKey` alongside the record line and counting turns per omp invocation.
110
+ 2. **Content arrives as `reasoning` deltas, not `text`**, for reasoning-capable models — the
111
+ accumulator deliberately ignores `reasoning`. If so, decide whether the transcript should
112
+ capture reasoning (probably not) or whether `text` is arriving under a chunk shape the
113
+ interpreter is not mapping to a `text` event.
114
+ 3. **Escalation resets the buffer.** `assistantText` is declared per attempt; if a turn
115
+ commits on a later attempt the earlier text is correctly dropped, but verify the committed
116
+ attempt is the one being recorded.
117
+
118
+ Start by adding `conversationKey` and `attempt` to the `agentdox record turn` debug line and
119
+ running one omp invocation — that distinguishes lead 1 from the others immediately.
120
+
121
+ ## 6. Also worth doing
122
+
123
+ - **Context pollution.** `context_assemble` includes recent session messages, so recorded
124
+ test turns feed back into the next block (already observed: the block contained
125
+ `assistant:: high` from a prior run). Real usage is fine, but noisy test turns compound.
126
+ Consider a `sessionLimit` override for the bridge, or excluding router-authored sessions.
127
+ - **`context.timeoutMs` is 3000ms** and failures degrade silently at `debug` level by design.
128
+ If agentdox is cold this can no-op invisibly. Consider logging the first failure at `warn`.
129
+ - **Four copies of this project exist** on this machine: this repo, the research checkout,
130
+ `~/.omp/plugins/cache/marketplaces/auto-model-router` (v0.2.1, marketplace cache — not
131
+ installed, `installed_plugins.json` is empty), and a global npm `auto-model-router@0.2.4`.
132
+ Confirm which one a given run is exercising before trusting an e2e result.