@lawaty/lacode 1.0.3 → 1.2.0

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.
@@ -23,12 +23,22 @@ allowlist anywhere in the package, and no cost check of any kind: only the
23
23
  structure is validated. The bundled preset is built from free models because
24
24
  that is a sensible default, not because the pool requires it.
25
25
 
26
+ A slot's `weight` (default 1) is both its concurrency budget and its cap. The
27
+ pool fills slots in declaration order in one pass; there is no slot cap, and a
28
+ model may appear on more than one line — each line is its own slot with its own
29
+ weight. A `tier` key is the deprecated spelling of `weight`.
30
+
31
+ A slot may also declare a shared quota bucket with `b:<name>` on the line (or a
32
+ `bucket` field in JSON). A limit on any member benches the whole bucket, because
33
+ the sibling would spend the same exhausted window; without one, the model is its
34
+ own bucket. `pool_status` shows each shared bucket's effective capacity.
35
+
26
36
 
27
37
  ## Which models run which agent
28
38
 
29
39
  Only the **pool** writes a model onto a pooled agent, from whichever source wins
30
40
  above. The pooled bases (`explore`, `implement`, `context`)
31
- and their hidden `-1`..`-4` variants therefore carry no model of their own: a
41
+ and their hidden `<base>-<slot>` variants therefore carry no model of their own: a
32
42
  model written next to an agent would be a second source of truth that silently
33
43
  wins whenever routing falls through. Spawn a base and let the pool route it.
34
44
 
@@ -39,7 +49,7 @@ wins whenever routing falls through. Spawn a base and let the pool route it.
39
49
  work around by configuring it.** If you keep a global `"tools"` block in your
40
50
  config — for example `"tools": { "playwright_*": false }` — then the pooled
41
51
  exploration and implementation bases (`explore` and `implement`, plus their
42
- hidden `-1`..`-4` pool variants) lose browser access, and **nothing tells them why**: the tool call is simply
52
+ hidden `<base>-<slot>` pool variants, one per resolved slot) lose browser access, and **nothing tells them why**: the tool call is simply
43
53
  refused mid-task. `context` gets no grant either, which is correct — it
44
54
  writes the context map and has no business in a browser.
45
55
 
@@ -89,11 +99,11 @@ mention/direct-session case cannot be detected before it runs.
89
99
 
90
100
  ## What gets injected
91
101
 
92
- **18 agent keys**: the 5 bases `build`, `plan`, `explore`, `implement`,
93
- `context`, plus one hidden
94
- `<base>-1`..`-4` pool variant for each of the 3 pooled bases, plus the hidden
95
- `instructions` agent — the one writer of the project's root `AGENTS.md`,
96
- reachable only from the `/agent-instructions` command.
102
+ **One key per base, plus a variant per slot**: the 5 bases `build`, `plan`,
103
+ `explore`, `implement`, `context`, one hidden `<base>-<slot>` pool variant per
104
+ resolved slot for each of the 3 pooled bases, and the hidden `instructions`
105
+ agent — the one writer of the project's root `AGENTS.md`, reachable only from
106
+ the `/agent-instructions` command.
97
107
 
98
108
  `build` and `plan` exist in opencode as built-ins, so on a stock config those
99
109
  two keys are already taken and merge-without-clobber keeps the built-in — the
package/docs/CONTEXT.md CHANGED
@@ -57,7 +57,7 @@ a work list rather than a map:
57
57
  | `contexts.md` | The major subsystems. For each: what it is for, where to start reading it, what it depends on, what must not change while you are in there. Read this for a subsystem question. |
58
58
  | `conventions.md` | Repository-specific rules: naming, module layout, error handling, testing, persistence. Read this before writing code, not after. |
59
59
  | `workflows.md` | The few runtime flows worth tracing end to end — how a request becomes a mutation, how a plugin hook reaches the thing it hooks. Read this when you need to know what actually happens at runtime. |
60
- | `decisions.md` | Architectural decisions that the repository's own decision log does not already record. Concise, variable length. |
60
+ | `decisions.md` | Architectural constraints that bind future work and that the repository's own decision log does not already record. Each entry states the constraint and points at the page or path that owns it. Capped at 120 non-blank lines; decision records and deliberation are not copied here. |
61
61
  | `active.md` | The active backlog: work in flight, grouped by the ISO week each task was opened in. A task that is not marked `[x]` is open and is never removed; a task that is finished and dated is kept for a week after it was finished, then dropped. The plugin enforces that window — see [How it stays current](#how-it-stays-current). Not a navigation file: there is no question it is the answer to. |
62
62
 
63
63
  The project's root `AGENTS.md` is the standing-instructions home: the client's
@@ -67,8 +67,51 @@ no prescribed structure. It has exactly one writer — the hidden `instructions`
67
67
  agent, reachable only from the `/agent-instructions` command.
68
68
 
69
69
  Files the user or the plan explicitly names always win over the map. The map is
70
- a shortcut to the right starting point, not an authority: when it disagrees with
71
- the source, the source is right and the map is wrong.
70
+ a shortcut to the right starting point, not an authority. When it disagrees with
71
+ **code**, the code is right and the map is wrong. When it disagrees with a
72
+ **project page**, neither side is automatically right: the map may be current
73
+ and the page stale (that has happened). The disagreement is reported and marked
74
+ `[disputed]`; the page's owner resolves it. The cartographer never silently
75
+ overwrites either side.
76
+
77
+ ## The entry contract
78
+
79
+ Every navigation entry has one shape, in every file:
80
+
81
+ ```markdown
82
+ - [binding] The operative rule, in at most three non-blank lines. — See `docs/DESIGN.md` §The two halves (verified 2026-10-26)
83
+ ```
84
+
85
+ - **A provenance marker.** `[binding]` is a constraint established against the
86
+ source and exempt from age expiry. `[provisional]` is an inference or an
87
+ unverified claim and expires 30 days after `verified`. `[disputed]` records a
88
+ map/page disagreement and is kept, reported, and exempt from age until the
89
+ owner resolves it. There is no second confidence dialect.
90
+ - **The rule, not the deliberation.** No Context / Decision / Consequences /
91
+ Alternatives blocks, status lines, or decision records. A reader needs the
92
+ constraint, not the meeting that produced it.
93
+ - **A pointer.** A fact whose home is a page cites the page **and** its section
94
+ (`docs/DESIGN.md §Retention`). A fact whose home is code cites the path, with
95
+ an optional line or symbol. No pointer means the content has no source in the
96
+ tree — it is reported as unarchived, never emitted as though it were derived.
97
+ - **A verification date.** `(verified YYYY-MM-DD)` drives both the lifecycle and
98
+ the drift check.
99
+
100
+ Every file has a hard cap, counted in **non-blank lines**: architecture 150,
101
+ contexts 200, conventions 150, workflows 150, decisions 120, active 60. Lines
102
+ are the unit because they are what a reader pays for, they bound an entry that
103
+ wraps to three lines, and the validator can count them exactly; an entry count
104
+ does not bound cost, and a token count needs a tokenizer and is model-specific.
105
+ The cap is enforced by a post-generation validator, not only by the prompt.
106
+
107
+ The lifecycle generalises the window `active.md` already had. A finished backlog
108
+ task is dropped after 7 days. A `[provisional]` entry in any navigation file is
109
+ dropped 30 days after its `verified` date unless it is re-verified. `[binding]`
110
+ and `[disputed]` never expire by age. An entry whose cited source no longer
111
+ exists is **never** auto-dropped: it may be the only copy, so it is reported as
112
+ unarchived and kept. Only provisional entries whose source still resolves are
113
+ eligible for the sweep. The arithmetic lives in `src/lib/map-schema.ts`; the
114
+ cartographer writes the shape, code decides what has aged out.
72
115
 
73
116
  The map is **local and untracked**. It describes *your* checkout on *this*
74
117
  machine, so committing it would churn on every session that edits a file, and
@@ -110,6 +153,18 @@ a new or moved entry point, a changed workflow, a new convention, an
110
153
  architectural decision, an undocumented invariant — and to report
111
154
  "no update needed" otherwise.
112
155
 
156
+ After the turn ends, the plugin applies the per-file lifecycle and then audits
157
+ the map. The audit is **report-only**: it never rewrites a claim and never
158
+ deletes an entry whose source is gone. It checks the schema, each file's line
159
+ budget, dangling paths and `§sections`, a numeric claim absent from its cited
160
+ source, a possible negation conflict, a source modified after its `verified`
161
+ date, tables and fenced blocks (restatements), duplicate rules across files,
162
+ archive-of-record language, and whether each file still answers its charter. A
163
+ failure is a loud `error` in the log; it is what turns the prompt's rules into a
164
+ gate. The deterministic half is `src/lib/map-schema.ts`; the semantic drift pass
165
+ is the cartographer's, and it reports a page disagreement instead of picking a
166
+ side.
167
+
113
168
  If the map is missing entirely, nothing breaks: agents run the check, find no
114
169
  `.opencode/context/`, and explore from scratch. What tells them to run the check
115
170
  is an entry appended to your config's `instructions` array: your own entries are
@@ -138,17 +193,25 @@ your source.
138
193
  If you have defined a command of the same name, yours wins and is left exactly
139
194
  as you wrote it — the same merge-without-clobber rule the agents follow.
140
195
 
141
- `active.md` is the one file in the directory the plugin maintains rather than
142
- the cartographer. The cartographer records what happened — work in flight, and a
196
+ `active.md` is the file the plugin maintains as a backlog rather than as a
197
+ navigation map. The cartographer records what happened — work in flight, and a
143
198
  task marked finished with the date it finished — and the plugin decides what is
144
199
  still worth keeping: the file is regrouped by week and a task that was finished
145
200
  more than a week ago is dropped. The arithmetic lives in
146
201
  `src/lib/active-backlog.ts`, not in the cartographer's prompt, because a rule a
147
202
  model applies is a rule the model applies when it feels like it, and the failure
148
- is silent — the backlog just grows. The plugin normalizes the file on every
149
- edit of it and once when the session starts, so a hand edit is grouped and
150
- pruned whether the cartographer ran or not, and the file is rewritten only when
151
- its canonical form differs from what is on disk, so the write cannot loop.
203
+ is silent — the backlog just grows. The plugin normalizes the file on every edit
204
+ of it and once when the session starts, so a hand edit is grouped and pruned
205
+ whether the cartographer ran or not, and the file is rewritten only when its
206
+ canonical form differs from what is on disk, so the write cannot loop.
207
+
208
+ The same code owns the navigation files' lifecycle, but it may only **drop** an
209
+ entry that is `[provisional]`, past its 30-day window, and whose cited source
210
+ still exists. It never drops a binding constraint or disputed entry, and it
211
+ never drops an entry with no source — that is the one case where the map may be
212
+ the only copy. The plugin is not a second writer of the map in the permission
213
+ sense: `context` is still the only **agent** that writes it. The lifecycle is
214
+ housekeeping by code, the same category as the backlog window.
152
215
 
153
216
  ## One writer
154
217
 
@@ -226,14 +289,21 @@ to check is that permission rule, not the map's content.
226
289
  | `auto update finished` | The run completed and changed at least one map file. |
227
290
  | `auto update finished without changing the map` | Nothing landed. Either nothing needed saying, or the cartographer could not write. |
228
291
  | `auto update failed: …` | The run errored. The paths it was given are queued again for the next window, so the work is not lost. |
292
+ | `map lifecycle: dropped N expired provisional entr… from <file>` | The code sweep dropped an entry past its 30-day window. Binding and disputed entries are never dropped. |
293
+ | `context map validation passed` | Every file is in schema and within budget, every pointer resolves, and the drift checks found nothing. |
294
+ | `context map validation failed: N issue(s)` | The run's map is off schema or over budget, cites a missing path/section, restates a page, or has a possible contradiction. The map is **not** rewritten; the log names up to 20 issues and the paths to fix. |
229
295
  | `context map is not writable: …` | Your config's one-writer rule does not match the path opencode evaluates. Nothing will update until you fix it. |
230
296
  | `context map has more than one writer: …` | An agent other than `context` would be granted the map. The offending agent and the winning rule are named. |
231
297
 
232
298
  ## Reference
233
299
 
234
300
  - The agent and command definitions, as injected: `src/agents.ts`, `src/commands.ts`.
235
- - The idle/edit trigger: `src/plugins/context-autoupdate.ts`.
301
+ - The prompt the model actually runs, in one place: `src/lib/cartographer-prompt.ts`; the tracked copy is `.opencode/prompts/context.txt`, and a test pins equality.
302
+ - The idle/edit trigger and the post-run audit wiring: `src/plugins/context-autoupdate.ts`.
303
+ - The schema, budget, lifecycle and deterministic drift check: `src/lib/map-schema.ts`.
304
+ - The active backlog's window: `src/lib/active-backlog.ts`.
236
305
  - The one-writer rule check: `src/lib/writer-rule.ts`.
306
+ - Fixtures and their report: `tests/fixtures/context-map/`, `scripts/context-map-fixture.mjs`.
237
307
  - The behavioural contracts, with acceptance criteria a test asserts:
238
308
  [USER-STORIES.md](USER-STORIES.md), US-19 to US-22.
239
309
  - Tests: `tests/stories/g-context-management.test.mjs`.
package/docs/INSTALL.md CHANGED
@@ -181,8 +181,8 @@ or no GNU `timeout` — rather than passing silently or failing confusingly.
181
181
 
182
182
  ## The `lacode` CLI
183
183
 
184
- The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `version`,
185
- `update`, `receive`, `tunnel setup`) — see
184
+ The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `feedback`,
185
+ `version`, `update`, `receive`, `tunnel setup`) — see
186
186
  [Remote hosts](../README.md#remote-hosts). The plugin install does **not** put it on your
187
187
  `PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
188
188
  globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
@@ -15,7 +15,7 @@ line. Criteria that cannot be observed without a running server are not in this
15
15
  document; everything here is testable offline with a stub client and a
16
16
  `mkdtempSync` state directory.
17
17
 
18
- **Terminology.** A *slot* is one entry in `presets/free-tier.json`, carrying a model and a `weight`. A slot's `weight` is its active-session budget for one pass: the number of concurrent claims it may hold before the next slot is preferred. A *pass* is one declaration-order sweep of the slot list; the pool runs at most two passes, and a slot's absolute cap is twice its weight. Preference is by declaration order, never by weight. A *base* is a pooled agent type (`explore`), and its slot holders are `<base>-<slot>`, hidden in `opencode.jsonc`. A *claim* is a running spawn recorded in `claims.<pid>.json`. A *context map* is the five-file `.opencode/context/` codemap described in Epic G.
18
+ **Terminology.** A *slot* is one entry in `presets/free-tier.json`, carrying a model and a `weight`. A slot's `weight` is its active-session budget for one pass: the number of concurrent claims it may hold before the next slot is preferred. A *pass* is one declaration-order sweep of the slot list; the pool runs one pass, and a slot's cap is its weight. There is no slot cap, and a model may appear on more than one line — each line is its own slot with its own weight. Preference is by declaration order, never by weight. A *base* is a pooled agent type (`explore`), and its slot holders are `<base>-<slot>`, hidden in `opencode.jsonc`. A *claim* is a running spawn recorded in `claims.<pid>.json`. A *context map* is the five-file `.opencode/context/` codemap described in Epic G.
19
19
 
20
20
  ---
21
21
 
@@ -92,18 +92,18 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
92
92
  - [ ] Given every model is cooling, When a spawn is routed, Then the least-loaded slot is chosen anyway (degrade, never block).
93
93
  - [ ] Given a limit error names a reset time, When the cooldown is recorded, Then the stated reset is used instead of a guessed backoff.
94
94
 
95
- ### US-5: Slots fill in declaration order, in up to two passes
95
+ ### US-5: Slots fill in declaration order in one pass, with repeats
96
96
  **As a** user, **I want** work to prefer the first slot and spill to later slots only when the earlier ones are full, **so that** a healthy preferred model is used before fallbacks and no slot is used while an earlier usable slot has room.
97
97
 
98
98
  **Story ID(s) for tests:** US-5
99
99
  **Acceptance criteria:**
100
- - [ ] Given live claims, When the pool has room in the first pass, Then the lowest-index usable slot whose load is below its weight is chosen, and a slot declared later is not used until every earlier usable slot has reached its weight.
101
- - [ ] Given every usable slot has reached its weight, When another spawn is routed, Then the second pass begins at the lowest-index slot and fills each usable slot up to twice its weight.
102
- - [ ] Given a slot is cooling or unavailable, When its position in the pass comes up, Then it is skipped and the pass continues to the next slot.
100
+ - [ ] Given live claims, When the pool has room, Then the lowest-index usable slot whose load is below its weight is chosen, and a slot declared later is not used until every earlier usable slot has reached its weight.
101
+ - [ ] Given a model appears on more than one line, When the pool loads, Then each line is its own slot with its own weight, and the later line adds seats at its own position.
102
+ - [ ] Given a slot is cooling or unavailable, When its position comes up, Then it is skipped and the pass continues to the next slot.
103
103
  - [ ] Given a claim is released, When the next spawn is routed, Then the slot that freed capacity is preferred again in declaration order; no stored position keeps it waiting.
104
- - [ ] Given every usable slot is at its absolute cap, When another spawn is routed, Then it goes to the least-loaded usable slot as overflow, and the pool never refuses work.
105
- - [ ] Given all slots carry equal weight, When spawns are routed, Then the same ordered two-pass fill applies.
106
- - [ ] Given per-slot load is read, When it is asserted, Then a slot's load never exceeds twice its weight except through overflow.
104
+ - [ ] Given every usable slot is at its weight, When another spawn is routed, Then it goes to the least-loaded usable slot as overflow, and the pool never refuses work.
105
+ - [ ] Given all slots carry equal weight, When spawns are routed, Then the same ordered one-pass fill applies.
106
+ - [ ] Given per-slot load is read, When it is asserted, Then a slot's load never exceeds its weight except through overflow.
107
107
 
108
108
  ### US-6: Live pool visibility
109
109
  **As a** user, **I want** to see per-slot load and which model is cooling and why, **so that** I understand where my subagents are going.
@@ -113,8 +113,8 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
113
113
  - [ ] Given claims are held by several processes, When the status view is requested, Then it shows every process's claims and the aggregate load per slot.
114
114
  - [ ] Given a model is cooling with a stated reason, When the status view is requested, Then the model is listed as cooling together with that reason.
115
115
  - [ ] Given a claim file is older than the dead-file threshold, When the status view is computed, Then the stale claim is pruned rather than counted.
116
- - [ ] Given a slot is at its absolute cap, When the status view is requested, Then it is not marked as the slot the next spawn would take while any usable slot is below its current pass target; when every usable slot is at its absolute cap, the least-loaded slot is marked next.
117
- - [ ] Given the pool is filling, When the status view is requested, Then it shows whether the next spawn belongs to the first pass, the second pass, or overflow.
116
+ - [ ] Given a slot is at its weight, When the status view is requested, Then it is not marked as the slot the next spawn would take while any usable slot is below its weight; when every usable slot is at its weight, the least-loaded slot is marked next.
117
+ - [ ] Given the pool is filling, When the status view is requested, Then it shows whether the next spawn is a normal pick or overflow.
118
118
 
119
119
  ### US-7: Non-free models still work
120
120
  **As a** user who wants a paid model in the pool, **I want** to supply any model list, **so that** the pool is not restricted to free tiers.
@@ -123,7 +123,7 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
123
123
  **Acceptance criteria:**
124
124
  - [ ] Given a slot names a paid model, When the pool loads, Then the slot is accepted and routed to without a free-tier filter rejecting it.
125
125
  - [ ] Given a model id contains a vendor prefix with slashes (e.g. `openrouter/nvidia/model:free`), When it is split into provider and model id, Then the split is on the first slash only and the id tail is preserved.
126
- - [ ] Given more slots are declared than the pool allows, When the pool loads, Then the surplus entries are dropped with a warning naming the cap.
126
+ - [ ] Given a pool file declares more slots than the built-in warning threshold, When the pool loads, Then every entry is kept and a warning names the count; there is no slot cap.
127
127
  - [ ] Given a provider configured with **no** `whitelist` key, When the config hook binds a slot on that provider, Then no `whitelist` is written for it, because an absent whitelist already allows every model the provider offers.
128
128
  - [ ] Given a provider the pool never names a slot on, When the config hook runs, Then that provider entry is left exactly as it was, with no `whitelist` key added.
129
129
  - [ ] Given a provider that already carries a `whitelist`, When the config hook binds a slot model on it, Then the model id is appended and every entry the user wrote is preserved in order.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,11 +34,13 @@
34
34
  "./pool": "./dist/pool.js",
35
35
  "./agents": "./dist/agents.js",
36
36
  "./commands": "./dist/commands.js",
37
+ "./tui": "./dist/tui.js",
37
38
  "./tunnel": "./dist/tunnel.js",
38
39
  "./package.json": "./package.json"
39
40
  },
40
41
  "bin": {
41
- "lacode": "./dist/lacode.js"
42
+ "lacode": "dist/lacode.js",
43
+ "lacode-voice": "dist/lacode-voice.js"
42
44
  },
43
45
  "files": [
44
46
  "dist",
@@ -47,14 +49,16 @@
47
49
  "LICENSE",
48
50
  "CHANGELOG.md",
49
51
  "README.md",
50
- "docs"
52
+ "docs",
53
+ "scripts/score-models.mjs"
51
54
  ],
52
55
  "scripts": {
53
56
  "build": "node scripts/build.mjs",
54
57
  "prepack": "npm run build",
55
58
  "postpublish": "node scripts/sync-hosts.mjs",
56
59
  "test": "node --test tests/",
57
- "benchmark": "node tests/benchmark/run.mjs"
60
+ "benchmark": "node tests/benchmark/run.mjs",
61
+ "score": "node scripts/score-models.mjs"
58
62
  },
59
63
  "devDependencies": {
60
64
  "esbuild": "^0.28.2"
@@ -62,6 +66,10 @@
62
66
  "peerDependencies": {
63
67
  "@opencode-ai/plugin": "1.18.34"
64
68
  },
69
+ "optionalDependencies": {
70
+ "@picovoice/porcupine-node": "^4.0.2",
71
+ "vosk": "^0.3.39"
72
+ },
65
73
  "publishConfig": {
66
74
  "tag": "latest"
67
75
  }
package/presets/README.md CHANGED
@@ -45,9 +45,9 @@ opencode/glm-5.3-flash-free 1
45
45
 
46
46
  `#` starts a comment (whole-line or trailing), blank lines are ignored, and the
47
47
  weight is optional (default 1). The rules are exactly the same as the JSON form
48
- below: up to 4 slots, distinct models, split on the first slash only. The loader
49
- accepts either shape in the same file, so a leading `{` means JSON and anything
50
- else means the flat form.
48
+ below: any number of slots, a model may repeat on another line, split on the
49
+ first slash only. The loader accepts either shape in the same file, so a leading
50
+ `{` means JSON and anything else means the flat form.
51
51
 
52
52
  ## Schema
53
53
 
@@ -63,7 +63,7 @@ else means the flat form.
63
63
  | Field | Required | Meaning |
64
64
  |-------|----------|---------|
65
65
  | `model` | yes | `provider/model-id`, split on the **first** slash only |
66
- | `weight` | no | active-session budget for one pass; its absolute cap is twice that. Absent = 1 |
66
+ | `weight` | no | active-session budget; the pool's capacity is the sum of the weights. Absent = 1 |
67
67
 
68
68
  `$comment` is a JSON-legal array of strings and is ignored by the loader; the
69
69
  bundled preset uses it to explain itself in place.
@@ -74,13 +74,13 @@ The loader checks shape and nothing else:
74
74
 
75
75
  - `slots` must be a non-empty array
76
76
  - each `model` must contain a `/`
77
- - models must be distinct across slots
77
+ - a model may appear on more than one line; each line is its own slot with its
78
+ own load counter and weight
78
79
  - `weight` must be a number in `(0, 100]`; a bad one **warns and uses 1** rather
79
80
  than dropping the slot, because losing a model to a typo in a count costs real
80
81
  capacity
81
- - at most `MAX_SLOTS` (4) slots; extra entries are **dropped with a warning**,
82
- because one hidden variant agent per base per slot exists and a fifth slot
83
- would route at an agent that does not exist
82
+ - there is no slot cap: one hidden variant agent is declared per base per
83
+ resolved slot, and the watcher declares a variant if a reload grows the table
84
84
  - `tier` is accepted as an alias for `weight` (`primary` → 2,
85
85
  `overflow` → 1) and warns
86
86
 
@@ -92,20 +92,18 @@ because the pool requires it.
92
92
 
93
93
  ## Weight, precisely
94
94
 
95
- A weight is a slot's active-session budget for one pass: the number of
96
- concurrent claims it may hold before the next slot is preferred. Its absolute
97
- cap is **twice that weight** — a weight-3 slot may hold 6 concurrent sessions.
98
- Slots are filled in declaration order; preference is order, never weight.
95
+ A weight is a slot's active-session budget: the number of concurrent claims it
96
+ may hold before the next slot is preferred. The pool's capacity is the **sum of
97
+ the weights**, so a weight-3 slot may hold 3 concurrent sessions and listing a
98
+ model on two lines gives it two independent budgets.
99
99
 
100
- Routing runs in two passes:
100
+ Routing is one pass:
101
101
 
102
- 1. pass 1 fills each usable slot up to its weight, in declaration order
103
- 2. once every usable slot has reached its weight, pass 2 repeats the order,
104
- filling each usable slot up to twice its weight
102
+ 1. fill each usable slot up to its weight, in declaration order
105
103
 
106
- A slot is skipped when it is cooling, unavailable, or at its absolute cap.
107
- When every usable slot is at its absolute cap, overflow goes to the least
108
- loaded usable slot; the pool never refuses work.
104
+ A slot is skipped when it is cooling or unavailable. When every usable slot is
105
+ at its weight, the pool is at capacity and overflow goes to the least loaded
106
+ usable slot; the pool never refuses work.
109
107
 
110
108
  Live claims decide the next slot: a released claim immediately reopens its
111
109
  slot, and there is no stored position.
@@ -9,25 +9,23 @@
9
9
  "Slot N is the same model for every base agent, which is what makes load shared.",
10
10
  "",
11
11
  "RULES, all validated at startup: models are written provider/model-id, split on the FIRST",
12
- "slash only so openrouter's vendor-prefixed ids (nvidia/x:free) resolve; must be distinct.",
13
- "AT MOST 4 SLOTS: one hidden variant per base per slot is declared, so a fifth entry would",
14
- "route at an agent that does not exist. Extra entries are dropped with a warning.",
12
+ "slash only so openrouter's vendor-prefixed ids (nvidia/x:free) resolve. A model may",
13
+ "appear on more than one line: each line is its own slot with its own budget.",
14
+ "ANY NUMBER OF SLOTS: one hidden variant per base per slot is declared from the resolved",
15
+ "table, and the watcher declares a variant when a reload grows the table. There is no slot cap.",
15
16
  "",
16
17
  "WEIGHT IS THE ACTIVE-SESSION BUDGET FOR ONE PASS: a slot may hold up to its weight in",
17
18
  "concurrent claims before the next slot is preferred. Slots are filled in declaration",
18
19
  "order; preference is order, never weight.",
19
- "Routing runs in two passes:",
20
- " 1. pass 1 fills each usable slot up to its weight, in declaration order",
21
- " 2. once every usable slot has reached its weight, pass 2 repeats the order, filling",
22
- " each usable slot up to twice its weight",
23
- "A slot is skipped when it is cooling, unavailable, or at its absolute cap (twice its",
24
- "weight). When every usable slot is at its absolute cap, overflow goes to the least",
25
- "loaded usable slot; the pool never refuses work.",
20
+ "Routing is ONE ordered pass: each usable slot fills up to its weight, in declaration",
21
+ "order. A slot is skipped when it is cooling or unavailable. When every usable slot is at",
22
+ "its weight, the pool is at capacity (the sum of the weights) and overflow goes to the",
23
+ "least-loaded usable slot; the pool never refuses work.",
26
24
  "Live claims decide the next slot: a released claim immediately reopens its slot, and",
27
25
  "there is no stored position.",
28
26
  "",
29
- "With this file: weights 3,2,1,1 give 7 active sessions in pass 1 and 14 total across",
30
- "both passes.",
27
+ "With this file: weights 3,2,1,1 give 7 active sessions in total. To give a model more",
28
+ "capacity, list it again on another line with its own weight.",
31
29
  "Equal weights give a plain pass in list order.",
32
30
  "",
33
31
  "DECLARATION ORDER IS PREFERENCE. Slots are filled in declaration order, so the slot",
@@ -44,7 +42,7 @@
44
42
  "List order sets preference: earlier slots are preferred first, so put the slots you",
45
43
  "want worked first at the top of the list.",
46
44
  "",
47
- "COST IS THE DEFAULT, NOT A CONSTRAINT. These four slots are free because that is what",
45
+ "COST IS THE DEFAULT, NOT A CONSTRAINT. These slots are free because that is what",
48
46
  "the pool is FOR: mechanical work -- reading files, grepping, mechanical edits, cartography.",
49
47
  "They are not here to be the smartest model in the system. The point of putting that work",
50
48
  "on a near-free model is that the mechanical tokens stop being spent by your expensive",