@lawaty/lacode 0.10.0 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -6,7 +6,238 @@ All notable changes to LaCode are recorded here. The format follows
6
6
 
7
7
  Versions come from git release commits; the npm version is bumped in the same
8
8
  commit as the changelog entry it describes.
9
- ## [Unreleased]
9
+
10
+ ## [1.0.0] — images, ordered slot fill, and a limit that fails over
11
+
12
+ ### Added
13
+
14
+ - **Image attachments from the clipboard or from files.** `/image-in-cb` reads
15
+ the system clipboard through `wl-paste`/`xclip` on Linux, `pngpaste` on macOS,
16
+ or PowerShell on Windows, and `/image` attaches one or more image files. Paths
17
+ honour quotes, `~` and globs; identical bytes are deduped within a message;
18
+ each file is capped at 4 MiB, at 10 images, and at 20 MiB total. Images are
19
+ archived content-addressed under `~/.local/share/opencode/lacode/images` and
20
+ delivered to the model as `FilePart`s. Configure with `imagePaste`
21
+ (`enabled`, `maxBytes`, `maxImages`, `dir`) or `LACODE_IMAGE_DIR`.
22
+ - **A system-prompt guard against slot-variant names.** Every system string is
23
+ scrubbed by `scrubSlotNames()` before the model sees it, so a stale prompt
24
+ cannot teach the model `explore-1` and friends. The transform logs once when
25
+ it has to strip a literal.
26
+
27
+ ### Changed
28
+
29
+ - **Slot routing fills slots in declaration order in up to two passes.** A
30
+ slot's `weight` is its active-session budget for one pass. Pass 1 fills each
31
+ usable slot to its weight in declaration order; once every usable slot has
32
+ reached its weight, pass 2 fills each to twice its weight. Live claims decide
33
+ the next slot, a released claim immediately reopens its slot, and overflow
34
+ goes to the least-loaded usable slot. There is no stored position.
35
+ - **A hand-named slot variant is coerced to its base, not refused.** The task
36
+ hook rewrites `explore-1` to `explore` before routing, so a caller cannot pin
37
+ a provider or skip ordered fill. Injected agent prompts no longer name
38
+ variants, and `expert` is explicitly opt-in.
39
+ - **Rate-limit failover uses the authenticated plugin client.** Aborting a
40
+ limited child goes through `client.session.abort`, not a bare `fetch`, so an
41
+ `opencode serve` protected by `OPENCODE_SERVER_USERNAME/PASSWORD` no longer
42
+ answers 401 and leaves the child running. A failed abort holds the claim for
43
+ the reaper to retry instead of releasing the slot while the session lives.
44
+ - **A limit on a pooled child with no local claim is identified and handled.**
45
+ The failover path reads the session's `parentID` and user message to recognise
46
+ a pooled variant (`implement-1`, `explore-2`, `context-N`), cools that slot,
47
+ and aborts the child. Root, non-pooled, and unidentifiable sessions are left
48
+ alone.
49
+
50
+ ### Fixed
51
+
52
+ - **A limited or hung session no longer survives its slot.** The rate-limit path
53
+ aborts it immediately, including the transient-outage path in
54
+ `message.updated`, and a failed abort no longer frees the slot.
55
+ - **`pool_status` says a cooling slot is unavailable.** A cooling slot is marked
56
+ `UNAVAILABLE (COOLING …)`, and the summary reads `unavailable/cooling: …`.
57
+ - **A `done` popup is raised once per idle period, and an `error` suppresses the
58
+ `done` that follows it in the same turn.** The per-session completion marker
59
+ is cleared when the session goes busy again or is deleted.
60
+
61
+ ### Documented
62
+
63
+ - README documents `/image`, `/image-in-cb`, `imagePaste`, the `latest` dist-tag
64
+ for 1.0.0, and the ordered two-pass fill. `OPERATIONS.md`, `presets/README.md`
65
+ and the preset comments describe the same routing rule.
66
+
67
+ ## [0.11.0] — the old sessions and the old event rows are swept away on their own, and a pool edit takes effect without a restart
68
+
69
+ ### Added
70
+
71
+ - **A pool edit is picked up live, and a model the provider has withdrawn no
72
+ longer takes its slot down with it.** The slot table used to be read once per
73
+ process, so editing `pool.txt`, `pool.json` or the bundled preset did nothing
74
+ until a restart — and a slot whose model id had been retired failed every
75
+ spawn routed to it. The plugin now watches the three file sources (`watchMs`,
76
+ default 1s; `watchDebounceMs`, default 250ms; both configurable, `watchMs: 0`
77
+ disables). When the resolved table actually changes it reassigns `s.slots`,
78
+ re-binds the variant agents and provider whitelists from the same config
79
+ object, and validates every slot with one real request — cooling or not. A
80
+ provider answering "Model not found" is now a first-class outcome:
81
+ `pool_recheck` benches that model for the quota cap (`model-missing`) so
82
+ routing stops choosing it, and names it in the report. Given `models`,
83
+ `pool_recheck` validates those even when they are not cooling; omitting
84
+ `models` keeps the old, cheap "probe what is cooling" contract. The watcher is
85
+ closed in `dispose`, so a torn-down plugin stops polling.
86
+
87
+ ### Fixed
88
+
89
+ - **The bundled preset and the built-in fallback no longer name a withdrawn
90
+ model.** `opencode-go/minimax-m3.1-flash-free` is `deprecated` on models.dev
91
+ and is hidden from `opencode models`; it is replaced by
92
+ `opencode-go/beam-free`, which is active, free and in a different model family.
93
+ `tests/models-snapshot.json` was refreshed for the new slot.
94
+
95
+ - **A build no longer unloads the pool from a process that starts during it.**
96
+ `npm run build` deleted `dist/` before rebuilding, so an opencode process that
97
+ imported `file://…/dist/index.js` in that window failed to load LaCode at all
98
+ and ran every `task` spawn on the caller's own model — `explore` appeared to
99
+ ignore the pool. Each entry is now staged and renamed into place, and stale
100
+ files are removed only after the live ones are in place.
101
+
102
+ - **A task spawn with no `subagent_type` is reported instead of silently
103
+ bypassing the pool.** opencode runs the default subagent, which carries no
104
+ pool model, so it inherits the caller's model; the hook cannot route it after
105
+ the fact, but it now logs a warning naming the fix (`subagent_type: "explore"`
106
+ or `"implement"`). An `@explore` mention or a direct session prompt has the
107
+ same limitation: the plugin API exposes the model read-only, so it cannot be
108
+ re-routed there either.
109
+
110
+ - **A provider limit the pool did not recognise left the task hanging until the
111
+ watchdog found it.** opencode reports a limit in two shapes: an `action` block
112
+ naming the provider and a reason, or — for every provider whose error body
113
+ opencode does not know — a retry status carrying nothing but the provider's
114
+ own words. The pool only read the first shape, and only when the reason
115
+ happened to be one of its own, so a limit that said "limit reached" was not a
116
+ limit to it: nothing was cooled, the session kept retrying the refused model,
117
+ and the task waited out the watchdog's minutes before anything killed it.
118
+ Worse, the retry went back to the same slot, because nothing had been put on
119
+ cooldown for the pool to route around.
120
+
121
+ The limit vocabulary now reads the provider's own phrasing — "limit reached",
122
+ "daily limit reached", "usage limit exceeded" — and a retry status is read for
123
+ its message as well as for its reason. The context family is deliberately
124
+ excluded: "context limit reached" is a prompt that is too long, not a
125
+ provider that has run out of allowance, and benching a model for it would take
126
+ a healthy provider out of rotation over the user's own prompt.
127
+
128
+ A limit that names its provider still benches the whole provider. A limit that
129
+ does not name a provider cools only the model the session was running on — the
130
+ one the refusal is evidence about — and either way the stalled session is
131
+ aborted at once, so the caller retries onto a slot that answers instead of
132
+ waiting for the watchdog.
133
+
134
+ - **A transient outage no longer benches a model as if it had run out of
135
+ allowance.** "Endpoint is unavailable" is a provider that is briefly down, not
136
+ one that has spent its quota: it lifts on its own. Reported in a retry
137
+ status, it took the quota path — fifteen minutes off the rotation, filed
138
+ under "rate-limited" — and it counted as a strike, so the next real limit on
139
+ that model started at strike two and got a thirty-minute window. An outage now
140
+ takes the flat one-minute window the assistant-error path has always given
141
+ it, and it is never counted as a strike: the model is benched, the session is
142
+ still killed, and the quota window that follows starts where it should.
143
+
144
+ - **Two processes could raise the same notification.** A notification is claimed
145
+ with an exclusive create, so only the process that wins the claim raises it,
146
+ and a claim older than its window may be taken over so a holder that died
147
+ mid-popup cannot silence every later one. The takeover deleted the stale claim
148
+ and then created its own — but the delete was unconditional and never
149
+ re-checked that the file it removed was still the stale claim it had judged a
150
+ moment earlier. Two processes that both saw the same stale claim both reached
151
+ the takeover: the first replaced it with a fresh claim, the second deleted
152
+ that fresh claim and made its own, and both returned "won", so both notified.
153
+ The takeover is now a compare-and-swap: an exclusive takeover lock, an
154
+ immediate refusal for whoever did not get it, and a re-read of the claim
155
+ inside the lock, so the loser leaves a live holder's fresh claim untouched.
156
+ The staleness rule itself is unchanged — a fresh claim is never stolen and a
157
+ stale one is still reclaimed.
158
+
159
+ - **A `busy` session counted as you answering a prompt.** The five-second floor
160
+ before a notification is raised exists so it does not land while you are
161
+ already at the keyboard, and it is meant to be reset by real interaction.
162
+ `session.status: busy` was counted as that interaction — but `busy` is raised
163
+ whenever the session starts working, including when opencode resumes on its
164
+ own, so the floor was refreshed without you touching anything and the popup
165
+ you were owed could be suppressed. The signal is now a `message.updated` with
166
+ the user's own role on it, plus the reply events for permissions and
167
+ questions; `busy` no longer counts.
168
+
169
+ - **A tunnel unit could be installed with a preflight it cannot run.** The unit
170
+ writer put `ExecStartPre=<preflight>` in unconditionally, so on a host where
171
+ the preflight was never installed systemd failed the unit and the tunnel never
172
+ came up — and the up-to-date check kept the broken unit because it demanded
173
+ the line. The line is now written only when the preflight is installed, which
174
+ degrades to "no preflight" rather than "no tunnel", and an already-broken unit
175
+ is repaired on the next run. The up-to-date check no longer requires the line.
176
+
177
+ - **A strike for an unavailable provider is filed where it belongs.** An outage
178
+ reported through the strike path could still be counted as a quota strike and
179
+ handed the long window, because only one of the two call sites delegated to
180
+ the outage handler. Both now do, and a strike never counts an outage. A limit
181
+ that names a provider is also no longer overridden by the model the session
182
+ happened to be running on.
183
+
184
+ - **Two acceptance criteria were corrected to match the shipped behaviour.**
185
+ US-6 criterion 4 stated the "a full slot is not the next one" rule without its
186
+ exception, so it contradicted the overflow path; it now says a full slot is
187
+ next only when no other slot is available and under its ceiling. US-19
188
+ criterion 3 stated a standing rule as if the product injected it, when it now
189
+ lives in the repo-level `AGENTS.md`.
190
+
191
+ ### Added
192
+
193
+ - **The limit vocabulary is read from one place.** `pool.limitReason()` names
194
+ the limit a text reports, and the pool, the prober and the fail-over path all
195
+ take the reason from it, so a log line and a cooldown file never spell the
196
+ same fault two ways.
197
+
198
+ - **US-31 — a limit that hangs is killed, and the retry lands on a slot that
199
+ answers.** The contract is written down as a user story with acceptance
200
+ criteria in `docs/USER-STORIES.md`, and every criterion has a test in
201
+ `tests/stories/`.
202
+
203
+ - **Two retention sweeps take the old rows away before they become the whole
204
+ database.** `scripts/retire-old-sessions.mjs` archives then removes sessions
205
+ past a window (30 days, 24 hours of grace) and
206
+ `scripts/prune-old-events.mjs` deletes old transcript rows from `event`
207
+ (7 days, 1 hour of grace), in chunks, ordering by id — the id carries the
208
+ timestamp, so the primary-key index turns "older than" into a range scan
209
+ instead of a full table walk. Both refuse to touch a row that something still
210
+ depends on: a session an `event_sequence` cursor owns, a session bound to a
211
+ workspace, and anything inside the grace window. Neither ever removes or
212
+ resets a cursor. A session is written to a JSON archive before it is deleted,
213
+ and the archive is the recovery path if the window was wrong.
214
+
215
+ Both run by hand with `--dry-run`, support `--limit`, and the event prune also
216
+ takes `--vacuum`; a window larger than the id's own era is clamped, so a
217
+ mistyped number cannot be read as "delete everything". Each ships a systemd
218
+ service and timer (daily; `Persistent=true`, with a randomized delay on the
219
+ event prune so it does not run inside the session sweep's write window) and
220
+ appends one JSONL line per real run to a log under the state directory.
221
+
222
+ - **US-32 and US-33 document the sweeps, and the coverage guard reads the root
223
+ suites.** Both have acceptance criteria in `docs/USER-STORIES.md` and story
224
+ markers on their tests. The guard now scans the root-level `tests/*-test.mjs`
225
+ suites as well as `tests/stories/`, so a story whose tests legitimately live
226
+ outside the story directory is counted instead of reported uncovered.
227
+
228
+ - **Every acceptance criterion that could not be falsified now has a test that
229
+ fails when the behaviour regresses.** The audit found criteria whose tests
230
+ would have passed for the opposite behaviour: the least-loaded slot under a
231
+ full cooldown, a ceiling turn being skipped rather than consumed, a full slot
232
+ left unmarked, a macOS prompt carrying no buttons, a question carrying none,
233
+ a missing sound player costing only the sound, the bus path coming from the
234
+ local user rather than the remote, the sound playing where the notice is
235
+ rendered and never on the headless host, the self-update CLI, its argv and its
236
+ state file, cost control with no price information, the cartographer carrying
237
+ no model of its own, the map being reported rather than edited, an OmniRoute
238
+ gateway leaving a user's pool alone, and the notification doc leading with the
239
+ shipped path. Each new test was shown to fail by breaking the behaviour it
240
+ covers.
10
241
 
11
242
  ## [0.10.0] — a notification waits for you instead of timing out, and a permission can be answered from the popup
12
243
 
@@ -530,7 +761,9 @@ experimental; `latest` remains 0.4.2.
530
761
 
531
762
  - First published release: pooled subagents over a shared set of models.
532
763
 
533
- [Unreleased]: https://github.com/lawaty/lacode/compare/v0.10.0...HEAD
764
+ [Unreleased]: https://github.com/lawaty/lacode/compare/v1.0.0...HEAD
765
+ [1.0.0]: https://github.com/lawaty/lacode/compare/v0.11.0...v1.0.0
766
+ [0.11.0]: https://github.com/lawaty/lacode/compare/v0.10.0...v0.11.0
534
767
  [0.10.0]: https://github.com/lawaty/lacode/compare/v0.9.0...v0.10.0
535
768
  [0.9.0]: https://github.com/lawaty/lacode/compare/v0.8.0...v0.9.0
536
769
  [0.8.0]: https://github.com/lawaty/lacode/compare/v0.7.1...v0.8.0
package/README.md CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  > Ultimate opencode setup for Go users
4
4
 
5
- > **Beta — experimental, changing fast.** LaCode ships on the npm **`beta`**
6
- > dist-tag rather than `latest`, so nothing pulls it by accident: install
7
- > `@lawaty/lacode@beta`, or pin an exact version. Interfaces here can change
8
- > between releases without a major bump.
5
+ > LaCode 1.0.0 ships on the npm **`latest`** dist-tag. Prereleases use
6
+ > `beta`: install `@lawaty/lacode@beta`, or pin an exact version.
9
7
 
10
8
  LaCode is an opencode plugin. It routes delegated work onto a weighted pool of
11
9
  cheap models instead of the expensive one, reaps subagents that hang without
@@ -29,8 +27,13 @@ is also on disk at `node_modules/@lawaty/lacode/CHANGELOG.md`.
29
27
  | Per-model cooldowns and failover | A provider rate-limits mid-session and the whole thing hard-fails, while other providers sit idle. |
30
28
  | Hang reaper | A wedged subagent holds its slot forever, so the session stalls with no error and no timeout. |
31
29
  | Desktop notifications | With several sessions open you cannot see which one finished, or which is blocked on a question. |
30
+ | Permission answers on the popup | An approval blocks a background session, and finding that session just to click Allow is the interruption. |
31
+ | Distinct notification sounds | A popup says something happened, but not at a glance whether it is a finish, a question or a block. |
32
32
  | SSH notification tunnel | Work on a remote host raises its notification on that host, which has no desktop. |
33
- | Repository context map | Every task starts by exploring your codebase cold, and every agent re-discovers the same layout, the same entry points and the same boundaries — paid for again on every run. |
33
+ | Repository context map | Every task starts by exploring your codebase cold, re-deriving the same layout, entry points and boundaries. |
34
+ | Active backlog | What is half-done, and what can be dropped, lives in chat history the next session cannot see. |
35
+ | Standing instructions | The same direction gets repeated every session, or buried in config that is not per-clone. |
36
+ | Image attachments anywhere | A TUI with no image support locks vision models out entirely. |
34
37
 
35
38
  ## Getting started
36
39
 
@@ -58,9 +61,11 @@ opencode plugin @lawaty/lacode -g
58
61
 
59
62
  ### 2. Restart opencode
60
63
 
61
- **Nothing here hot-reloads.** The `plugin` array is read at startup, so without a
62
- restart the change does not take effect and the agent set will look stale for no
63
- visible reason.
64
+ **Plugin code is not hot-reloaded.** The `plugin` array is read at startup, so
65
+ without a restart the change does not take effect and the agent set will look
66
+ stale for no visible reason. (The pool's slot files are the exception: editing
67
+ `pool.txt`, `pool.json` or the bundled preset is picked up live — see
68
+ [Configure](#configure).)
64
69
 
65
70
  ### 3. Check it loaded
66
71
 
@@ -91,6 +96,7 @@ To see what the pool is doing underneath:
91
96
 
92
97
  ```bash
93
98
  /lacode-doctor # slots, model resolution, OmniRoute — report only, changes nothing
99
+ /pool-recheck # probe the benched models now; re-admit the ones that answer
94
100
  lacode version # which LaCode is actually running, and whether opencode loads that one
95
101
  ```
96
102
 
@@ -129,6 +135,9 @@ option is optional:
129
135
  | `selfUpdate` | `true` | Check for a newer release at most once per ~6h and install it in the background; it applies on the next opencode start. `false` — or `LACODE_NO_SELFUPDATE=1` — turns the check off. |
130
136
  | `sound` | `true` | Play a short tone with each notification, one per outcome, so you can tell a finished session from a question or a permission prompt without reading the popup. It is opt-out: `false` — or `LACODE_SOUND=0` (`false`, `off`, `no` work too, and beat the option) — turns the sound off, and anything else, a typo included, leaves it on. |
131
137
  | `omniroute` | — | Optional [OmniRoute](docs/OMNIROUTE.md) gateway integration. Absent or `{ enabled: false }` changes nothing. |
138
+ | `watchMs` | `1000` | How often the pool files are checked for changes. `0` disables live reload. |
139
+ | `watchDebounceMs` | `250` | Collapse a burst of editor writes into one reload. |
140
+ | `imagePaste` | — | Image attachments: `maxBytes` (4 MiB), `maxImages` (10) and `dir`. |
132
141
 
133
142
  ### Staying up to date
134
143
 
@@ -143,6 +152,12 @@ The running session keeps its loaded code; the new version takes effect on the
143
152
  next start, and the log says so. Run `lacode update` to do it on demand, or
144
153
  `lacode update --check` to just report.
145
154
 
155
+ 1.0.0 is published to the `latest` dist-tag; prereleases use `beta`. This check
156
+ follows `latest`. A beta install is therefore reported as current even when a
157
+ newer beta exists,
158
+ and `lacode update` will not move it: pin the version you want in the `plugin`
159
+ entry, because a new specifier is what forces a fresh resolve.
160
+
146
161
  ### Which LaCode is running
147
162
 
148
163
  `lacode version` answers it — and the answer is often not the version number you
@@ -153,7 +168,7 @@ lacode version
153
168
  ```
154
169
 
155
170
  ```
156
- version 0.4.1
171
+ version 1.0.0
157
172
  root /home/you/lacode
158
173
  source checkout
159
174
  commit <hash> (dirty)
@@ -179,45 +194,60 @@ When it reports `stale`, rebuild with `npm run build`. No network is touched;
179
194
 
180
195
  ## What you get
181
196
 
182
- Three agents, pooled and rotated: `explore`, `implement` and `context`. You spawn
183
- the base name and the pool picks a cheap model slot for it. Behind each base sits
184
- up to four hidden variants — one per slot — and a spawn of the bare name is the
185
- only thing that works: naming a variant lets the caller choose the provider
186
- directly, which is exactly what the rotation and the per-slot ceiling exist to
187
- prevent, so it is refused.
197
+ Three pooled agents: `explore`, `implement` and `context`. You spawn the base
198
+ name and the pool picks a cheap model slot for it. Behind each base sits up to
199
+ four hidden variants — one per slot — and a spawn of the bare name is the only
200
+ thing that works: a hand-named variant such as `explore-1` is coerced to its
201
+ base and routed by the pool, so naming one cannot pin a provider or skip
202
+ ordered slot filling.
188
203
 
189
- A model that rate-limits or runs out of quota is benched and taken out of the
190
- rotation until its window ends, so one sick provider does not take the pool with
191
- it. Those windows are computed backoff rather than a promise from the provider,
204
+ A model that rate-limits or runs out of quota is benched until its window ends,
205
+ so one sick provider does not take the pool with it.
206
+ Those windows are computed backoff rather than a promise from the provider,
192
207
  and a quota-shaped one can run for hours — long enough for a provider that has
193
208
  since recovered, or whose quota you reset in its own console, to stay excluded
194
209
  while every delegated spawn queues behind the survivors. So LaCode asks instead
195
- of guessing: every fifteen minutes, and on demand via the `pool_recheck` tool, it
196
- probes each benched model with one real request on that exact model. A model that
197
- answers is re-admitted immediately — nothing restarts, nothing reloads — and a
198
- model that is still limited keeps exactly the window it had, because a check must
199
- never be the thing that extends the wait. A change is reported as a toast and a
200
- log line; a pass that finds nothing changed says nothing at all.
201
-
202
- Six commands: `/context-init`, `/context-update`, `/context-review`,
203
- `/lacode-doctor`, `/agent-instructions` and `/pool`. Everything is added **only
204
- when your config does not already
210
+ of guessing: every fifteen minutes, on demand via the `pool_recheck` tool, and
211
+ the moment a pool file changes (where it validates every slot, cooling or not),
212
+ it probes each benched model with one real request on that exact model. A model
213
+ that answers is re-admitted immediately — nothing restarts, nothing reloads — and
214
+ a model that is still limited keeps exactly the window it had, because a check
215
+ must never be the thing that extends the wait. A model the provider does not
216
+ serve at all is benched for the quota cap and named in the report.
217
+ A change is reported as a toast and a log line; a pass that finds nothing changed says
218
+ nothing at all.
219
+
220
+ Nine commands: `/context-init`, `/context-update`, `/context-review`,
221
+ `/lacode-doctor`, `/agent-instructions`, `/pool`, `/pool-recheck`, `/image-in-cb`
222
+ and `/image`. Everything is added **only when your config does not already
205
223
  define it** — an agent or command you wrote yourself is left byte-identical,
206
224
  never patched or partially merged. `{"enabled": false}` turns the whole thing off
207
225
  without leaving a trace. The exact injection list, and the five keys that are
208
226
  deliberately *not* injected, are in
209
227
  [docs/CONFIGURATION.md](docs/CONFIGURATION.md#what-gets-injected).
210
228
 
229
+ Two more surfaces that need no configuration:
230
+
231
+ - **`pool_status`** — the live view `/lacode-doctor` uses: per-slot load against
232
+ its ceiling, which slot the next spawn takes, cooldowns, hung claims and
233
+ whether the watcher is on. Ask for it when you want to know what the pool is
234
+ doing; it never changes slot selection.
235
+ - **Notifications that wait** — a notification is never auto-dismissed; it stays
236
+ until you close it. On Linux a permission request carries **Allow once**,
237
+ **Always allow** and **Deny** buttons, and LaCode waits up to ten minutes for
238
+ the click.
239
+
211
240
  ## Context map
212
241
 
213
242
  Agents are stateless between tasks, and exploring your repository is the most
214
243
  expensive part of any task — so every agent pays for it again, re-deriving the
215
244
  same layout, entry points and boundaries from scratch. LaCode keeps a short,
216
- maintained map of the repository in `.opencode/context/` (five navigation files:
217
- `architecture`, `contexts`, `conventions`, `workflows`, `decisions`, plus
218
- `active.md` — the active backlog) and injects
219
- a standing rule that every agent reads the relevant file before exploring and
220
- starts from the canonical entry points it lists. The map holds conclusions, not
245
+ maintained map of the repository in `.opencode/context/` — five navigation files
246
+ (`architecture`, `contexts`, `conventions`, `workflows`, `decisions`) plus
247
+ `active.md`, the active backlog, where a finished task stays a week and an open
248
+ one stays until done — and injects a standing rule that every agent reads the
249
+ relevant file before exploring and starts from the canonical entry points it
250
+ lists. The map holds conclusions, not
221
251
  inventories: it answers "what should I look at first?" and nothing else, which
222
252
  is what makes reading it cheaper than the exploration it replaces.
223
253
 
@@ -236,7 +266,10 @@ are reading:
236
266
  /context-review # audit it for staleness, contradictions, duplication and bloat
237
267
  /lacode-doctor # check the pool, model resolution and the OmniRoute gateway. Report only
238
268
  /agent-instructions # apply a client instruction to the project's AGENTS.md — client-only, see below
239
- /pool # rewrite the pool slot table in ~/.config/lacode/pool.txt; restart to apply
269
+ /pool # rewrite the pool slot table in ~/.config/lacode/pool.txt; picked up automatically
270
+ /pool-recheck # probe the pool now; accepts an optional provider/model list
271
+ /image-in-cb # attach the clipboard image to this message; macOS needs pngpaste, Linux needs wl-paste or xclip
272
+ /image # attach one or more image files to this message; paths honour quotes, ~ and globs
240
273
  ```
241
274
 
242
275
  The map is local and untracked — it describes your checkout on your machine, so
@@ -245,11 +278,16 @@ nothing and carry on without it. What this buys is tokens not spent
245
278
  re-discovering structure; as with everything else here, no savings percentage is
246
279
  published, because none has been independently measured. The mechanism, the
247
280
  permission rule that makes one writer possible, and what the log lines mean are
248
- in [docs/CONTEXT.md](docs/CONTEXT.md).
281
+ in [docs/CONTEXT.md](docs/CONTEXT.md). Automatic map runs also clean up after
282
+ themselves: each run's session is deleted once its transcript is written, and
283
+ the last 20 transcripts are kept under
284
+ `~/.local/share/opencode/lacode/cartographer` (`LACODE_TRANSCRIPT_DIR` overrides
285
+ the directory).
249
286
 
250
287
  ## Standing instructions
251
288
 
252
- The project's root `AGENTS.md` is your own statement of intent: it records
289
+ The project's root `AGENTS.md` — created on first use by `/agent-instructions` —
290
+ is your own statement of intent: it records
253
291
  what you want worked on and how you want it approached. It says nothing about
254
292
  where code lives, so it can never contradict the code, and within that domain
255
293
  it outranks the generated context files and any agent's inference about what
@@ -338,7 +376,8 @@ which explains the harness and deliberately contains no results.
338
376
  ## Remote hosts
339
377
 
340
378
  Notifications raised on a remote host need a way back to your desktop. The
341
- reverse SSH forward is what carries them:
379
+ default hop is a direct `ssh` to your desktop over one warm `ControlMaster`
380
+ connection; a reverse forward is the fallback used when `OC_LOCAL_HOST` is unset:
342
381
 
343
382
  ```bash
344
383
  lacode notify "build finished" "4 files changed" # local desktop, or forwarded if remote
@@ -349,6 +388,8 @@ lacode doctor # what the environment reso
349
388
  must never be able to fail the process that tried to notify you. The one
350
389
  exception is an actionable notification — one carrying a permission request id
351
390
  — which waits for the click, because the answer is the whole point of it.
391
+ That click path is Linux-only: the popup carries **Allow once**, **Always allow**
392
+ and **Deny**, and macOS gets a plain notice without buttons.
352
393
  `lacode forward HOST -- COMMAND` runs a command on another host over ssh and
353
394
  exits with its status.
354
395
 
package/dist/agents.js CHANGED
@@ -78,9 +78,12 @@ Every question of the form "what does this file do", "where is X handled", or
78
78
  that proves it. Never one whole refactor handed over in a single piece \u2014
79
79
  see \`# Task size\` below.
80
80
 
81
- Spawn the BARE pooled base \u2014 \`explore\`, \`implement\` or \`context\`. The pool
82
- refuses a \`-N\` variant, and a refusal means spawn the bare base, never a
83
- different number.
81
+ Spawn the pooled base \u2014 \`explore\`, \`implement\` or \`context\`. The plugin decides
82
+ which model runs the work; do not try to choose one yourself.
83
+
84
+ \`expert\` is not part of the pool and is expensive: spawn it only when the user
85
+ explicitly asks for it. Never escalate to it on your own, and never as a
86
+ fallback when a pooled spawn fails.
84
87
 
85
88
  # When the pool is unavailable
86
89
 
@@ -89,12 +92,12 @@ pool cannot serve a spawn \u2014 every slot cooling or exhausted, spawns repeate
89
92
  failing, or a runtime message saying no slot is available \u2014 that is not a reason
90
93
  to do the work yourself, and not a reason to stop and ask.
91
94
 
92
- **Retry.** A refused or cooling slot is transient and costs you nothing but a
93
- turn. Spawn the bare base again. Do not escalate to a numbered variant after a
94
- refusal \u2014 the refusal means the pool has a routing problem, not that you
95
- picked the wrong number.
95
+ **Retry.** A cooling model is transient and costs you nothing but a turn. Spawn
96
+ the same base again. If a spawn fails, do not switch to a different name \u2014 a
97
+ failure means the pool has a routing problem, not that you picked the wrong one.
96
98
 
97
- Retry is always available and never wrong, which is why it is the default. But
99
+ Retry is always available
100
+ and never wrong, which is why it is the default. But
98
101
  "never wrong" is not "sufficient": looping silently against a pool with every
99
102
  model cooling goes nowhere and burns the slot doing it. So separate the two
100
103
  failures. **Transient** \u2014 a cooldown, a rate limit, one slot at its ceiling \u2014
@@ -102,7 +105,8 @@ means retry. **Persistent or structural** \u2014 every slot cooling, the same sp
102
105
  failing repeatedly with the same error, a malformed model id \u2014 means stop
103
106
  retrying and *report* it: say what failed (slot, model, error) and continue
104
107
  with whatever else the task needs, or ask the user. Surface the failure; do not
105
- spin on it and do not let it end the whole task.
108
+ spin on it and do not let it end the whole task. Do not escalate to \`expert\` as
109
+ a workaround; it is opt-in by the user.
106
110
 
107
111
  **Never fall back to reading, grepping, exploring or implementing on your own.**
108
112
  Do not "just quickly" inspect a file because a spawn failed. A failed spawn is
@@ -219,8 +223,11 @@ When you need to understand the codebase:
219
223
  2. Invoke @explore with specific, focused instructions (e.g. "read src/auth.ts", "grep for 'migration' in *.py files", "list the src/ directory")
220
224
  3. @explore will return its findings \u2014 you process them and plan accordingly
221
225
 
222
- Spawn the BARE pooled base (\`explore\`, \`implement\`, \`context\`); the pool
223
- refuses \`-N\` variants, and a refusal means spawn the bare base, not another number.
226
+ Spawn the pooled base (\`explore\`, \`implement\`, \`context\`); the plugin decides
227
+ which model runs the work. Do not try to choose one yourself.
228
+
229
+ \`expert\` is not part of the pool and is expensive: spawn it only when the user
230
+ explicitly asks for it.
224
231
 
225
232
  If the pool cannot serve a spawn \u2014 every slot cooling or exhausted, or repeated
226
233
  failures \u2014 do NOT fall back to reading or planning from your own inspection.
@@ -548,7 +555,7 @@ Trigger: \`.opencode/context/\` exists and work has just been done here.
548
555
  - an architectural decision
549
556
  - a new dependency between subsystems
550
557
  - a renamed or relocated subsystem
551
- - a previously undocumented invariant
558
+ - an invariant not yet recorded in the map
552
559
 
553
560
  Do not update documentation merely because source files changed. Do not
554
561
  rewrite files wholesale. Do not make timestamp-only edits. Do not add a fact
@@ -854,7 +861,7 @@ function buildAgentSet() {
854
861
  for (let slot = 1; slot <= MAX_SLOTS; slot++) {
855
862
  const variant = {
856
863
  ...def,
857
- description: `Hidden pool variant ${base}-${slot}: slot ${slot}. Its model is NOT set here -- LaCode injects it from the resolved pool preset at config time, so the preset is the only place a pool model is written down. Spawn '${base}' and let the pool route.`,
864
+ description: `Hidden pool slot ${slot} for ${base}. Its model is NOT set here -- LaCode injects it from the resolved pool preset at config time, so the preset is the only place a pool model is written down. Spawn '${base}' and let the pool route.`,
858
865
  hidden: true,
859
866
  // `mode: "primary"` is what keeps a variant OUT of the model's system
860
867
  // prompt. `hidden` cannot do it.
@@ -866,9 +873,10 @@ function buildAgentSet() {
866
873
  // and by default-agent resolution. A variant left as `mode: "subagent"`
867
874
  // is therefore listed to the model under its own name -- "explore-1:
868
875
  // Hidden pool variant explore-1" -- and the model copies that name
869
- // straight into its next spawn. The pool then refuses it, because
870
- // naming a variant is how a caller would pick the provider and skip the
871
- // rotation. The refusal is right; the leak that provokes it is not.
876
+ // straight into its next spawn. The task hook coerces a hand-named
877
+ // variant back to its base, because naming one is how a caller would
878
+ // pick the provider and skip ordered slot fill and the slot ceiling. The coercion is right; the
879
+ // leak that provokes it is not.
872
880
  //
873
881
  // `mode` is the only field that filter respects, and setting it here is
874
882
  // safe: the task tool never reads an agent's mode -- it resolves the
package/dist/commands.js CHANGED
@@ -93,7 +93,7 @@ context file only if this work introduced or revealed information that will
93
93
  materially help future agents \u2014 a new boundary or module, a new or moved
94
94
  canonical entry point, a changed workflow, a new convention, an architectural
95
95
  decision, a new dependency between subsystems, a renamed or moved subsystem,
96
- or a previously undocumented invariant.
96
+ or an invariant not yet recorded.
97
97
 
98
98
  Do not edit documentation just because source files changed. No
99
99
  timestamp-only edits. If nothing material changed, make no edits and report
@@ -168,7 +168,7 @@ Print one PASS/FAIL line per check below, with the exact fix for any FAIL.
168
168
  Change nothing. Print a final one-line summary of failures.`
169
169
  },
170
170
  "pool": {
171
- description: "Modify this machine's LaCode pool from a short instruction \u2014 the shared free-model slot table in ~/.config/lacode/pool.txt. Restarts opencode afterwards.",
171
+ description: "Modify this machine's LaCode pool from a short instruction \u2014 the shared free-model slot table in ~/.config/lacode/pool.txt. Picked up live; no restart.",
172
172
  template: `Apply this change to the LaCode pool on this machine:
173
173
 
174
174
  $ARGUMENTS
@@ -180,22 +180,41 @@ The pool is ONE global slot table shared by every pooled base (explore, implemen
180
180
  2. Resolve the instruction above into a target slot table. These are the rules the loader enforces \u2014 never write a file that breaks one:
181
181
  - one slot per line: \`<provider>/<model-id> [weight]\`
182
182
  - at most ${MAX_SLOTS} slots, and a model may appear only once
183
- - weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's share of the rotation \u2014 how many turns it takes per cycle \u2014 and its concurrency ceiling is twice that. NOT a priority; pick order follows line order.
183
+ - weight is optional (default 1) and must be a number in (0, ${MAX_WEIGHT}]. It is the slot's active-session budget for one pass \u2014 how many turns it takes per cycle \u2014 and its concurrency ceiling is twice that. NOT a priority; pick order follows line order.
184
184
  - \`#\` starts a comment (whole-line or trailing); blank lines are ignored.
185
185
  If the instruction names a model ambiguously, or would need more than ${MAX_SLOTS} slots, stop and ask rather than guessing.
186
186
 
187
- 3. Write the WHOLE file, not a patch. This file is an override, not an edit target: any slot you drop disappears, and slot numbers are line positions, so a partial write silently renumbers the rotation. Keep the existing comment header. If the write fails on a missing directory, create \`~/.config/lacode\` first and retry.
187
+ 3. Write the WHOLE file, not a patch. This file is an override, not an edit target: any slot you drop disappears, and slot numbers are line positions, so a partial write silently renumbers slots. Keep the existing comment header. If the write fails on a missing directory, create \`~/.config/lacode\` first and retry.
188
188
 
189
- 4. Report the before \u2192 after slot table, the resolved path, and this reminder in substance: nothing hot-reloads, so every running opencode process must be restarted before the new pool takes effect.
189
+ 4. Report the before \u2192 after slot table and the resolved path. The running process watches the pool files: it reloads the table, re-binds the variant agents and validates every slot, so no restart is needed. If a slot names a model the provider does not serve, it will be benched and named in the log and toast \u2014 report that instead of claiming the swap succeeded.
190
190
 
191
191
  Two traps that fail silently, so check and state them explicitly:
192
192
  - A slot's provider must already exist in the user's opencode config, or that slot cannot resolve at all. If the instruction adds a model on a provider that is not configured, say so.
193
193
  - If a slot model is \`omniroute/<id>\`, that id must also appear in the plugin's \`omniroute.models\` allowlist (or be the \`combo\` id, which needs no entry) \u2014 otherwise the pool resolves and every spawn fails with \`ProviderModelNotFoundError\`.
194
194
  - A provider \`whitelist\` is an allowlist, and the plugin appends your model to it at config time. Nothing for you to do there.
195
195
 
196
- If the instruction is to RESET or REVERT rather than to change models, delete \`${USER_MODELS_TXT}\` instead, and report that the pool falls back to the bundled preset \u2014 with the same restart reminder.
196
+ If the instruction is to RESET or REVERT rather than to change models, delete \`${USER_MODELS_TXT}\` instead, and report that the pool falls back to the bundled preset \u2014 the running process picks that up automatically too.
197
197
 
198
198
  Change nothing else. In particular do not touch \`presets/free-tier.json\`, \`~/.config/lacode/pool.json\`, \`FALLBACK_SLOTS\` in \`src/lib/pool.ts\`, or \`tests/models-snapshot.json\`: this command owns the per-machine override and nothing else.`
199
+ },
200
+ "pool-recheck": {
201
+ description: "Probe the pool now \u2014 re-admit benched models that answer, keep the ones still limited, and bench a model the provider no longer serves.",
202
+ template: `Probe the LaCode pool now and report the result. Run inline; do not spawn a subagent and do not edit the pool.
203
+
204
+ $ARGUMENTS
205
+
206
+ - If the text above names one or more models, call the \`pool_recheck\` tool with those ids in \`models\` (provider/model-id).
207
+ - Otherwise call \`pool_recheck\` with no arguments; it probes every model that is currently cooling.
208
+
209
+ Report the tool's output as it came back, then add one short line naming what changed: re-admitted, still limited, or benched because the provider does not serve the model. If a model was benched as \`model-missing\`, say so and name the replacement in \`~/.config/lacode/pool.txt\` \u2014 but change nothing yourself.`
210
+ },
211
+ "image-in-cb": {
212
+ description: "Attach the clipboard image to this message \u2014 macOS needs pngpaste, Linux needs wl-paste or xclip.",
213
+ template: "$ARGUMENTS"
214
+ },
215
+ "image": {
216
+ description: "Attach one or more image files to this message",
217
+ template: "$ARGUMENTS"
199
218
  }
200
219
  };
201
220
  function applyCommands(cfg, commands = COMMANDS) {