@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 +235 -2
- package/README.md +78 -37
- package/dist/agents.js +24 -16
- package/dist/commands.js +25 -6
- package/dist/index.js +1550 -566
- package/dist/lacode.js +211 -48
- package/dist/pool.js +55 -44
- package/dist/tunnel.js +185 -13
- package/docs/CONFIGURATION.md +23 -9
- package/docs/OMNIROUTE.md +1 -1
- package/docs/USER-STORIES.md +99 -21
- package/package.json +3 -3
- package/presets/README.md +26 -27
- package/presets/free-tier.json +26 -42
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
|
-
|
|
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/
|
|
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
|
-
>
|
|
6
|
-
>
|
|
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,
|
|
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
|
-
**
|
|
62
|
-
restart the change does not take effect and the agent set will look
|
|
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.
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
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,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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/`
|
|
217
|
-
`architecture`, `contexts`, `conventions`, `workflows`, `decisions
|
|
218
|
-
`active.md
|
|
219
|
-
a standing rule that every agent reads the
|
|
220
|
-
starts from the canonical entry points it
|
|
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;
|
|
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`
|
|
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
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
|
223
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
870
|
-
// naming
|
|
871
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|