@lawaty/lacode 0.4.0 → 0.4.1
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 +168 -0
- package/README.md +7 -1
- package/dist/agents.js +5 -13
- package/dist/index.js +22 -20
- package/dist/pool.js +6 -0
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to LaCode are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and releases follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
Versions come from git release commits; the npm version is bumped in the same
|
|
8
|
+
commit as the changelog entry it describes.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.4.1] — refuse hand-named slot variants, and ship a changelog
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `CHANGELOG.md`, reconstructing all 13 published releases from their commits. It
|
|
17
|
+
ships with the package, so an install can read what changed without the repo.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- A pool slot variant named by hand (`explore-1`, `implement-2`, …) is now
|
|
22
|
+
**refused** rather than counted. Naming one skipped both the round-robin
|
|
23
|
+
rotation and the slot's ceiling, so six spawns of `explore-1` put six
|
|
24
|
+
concurrent claims on a slot bounded at three. The refusal names the bare base
|
|
25
|
+
to spawn instead, is logged, and escapes the routing catch — swallowed into
|
|
26
|
+
"spawn unchanged" it would have admitted the spawn it forbids.
|
|
27
|
+
- Agent prompts dropped the long explanation of why a variant must not be named.
|
|
28
|
+
It is enforced now, and the refusal carries the reason; the prompts keep only
|
|
29
|
+
the bare-base rule and what to do on refusal. 1,012 characters removed across
|
|
30
|
+
the `build`, `plan` and `explore` prompts.
|
|
31
|
+
|
|
32
|
+
### Documented
|
|
33
|
+
|
|
34
|
+
- `isAvailable` and `pickSlot` now say, in place, that the pool is deliberately
|
|
35
|
+
fail-open in two places: a model absent from the catalog is not treated as
|
|
36
|
+
unavailable, and when every slot is cooling they are re-admitted rather than
|
|
37
|
+
the work refused. Both were previously only discoverable by reading the code.
|
|
38
|
+
|
|
39
|
+
## [0.4.0] — order-first weighted round-robin, and a reaper that can kill
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **Slot selection is now order-first weighted round-robin.** Weight was both
|
|
44
|
+
the priority and the ceiling, so the heaviest slot was re-prioritised on every
|
|
45
|
+
spawn and refilled the moment a claim completed, while the others sat idle.
|
|
46
|
+
Selection walks the slot list in order, each slot taking as many turns as its
|
|
47
|
+
weight says, from a cursor persisted in the pool directory so the rotation is
|
|
48
|
+
shared by every opencode process rather than restarting in each one. Weights
|
|
49
|
+
`[3,2,1,1]` now produce the run `1,1,1,2,2,3,4`.
|
|
50
|
+
- The concurrency ceiling is unchanged: a slot still holds at most `weight`
|
|
51
|
+
concurrent claims, and overflow still goes to the least-loaded slot.
|
|
52
|
+
- `pool_status` previews the next slot without advancing the rotation, so
|
|
53
|
+
looking at the pool no longer shifts every spawn after it.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **A hung subagent could not be killed.** The reaper detected the wedge, freed
|
|
58
|
+
the claim, and then failed to abort the session — because the claim was
|
|
59
|
+
released *before* the abort, so a failed kill orphaned the session with
|
|
60
|
+
nothing left to retry it. Kills are verified against the response, retried
|
|
61
|
+
three times, and only a confirmed kill releases the claim; a give-up names
|
|
62
|
+
the session to kill by hand.
|
|
63
|
+
- A kill reported success for any HTTP response, including the gateway's
|
|
64
|
+
`200` with body `true` for a session that does not exist.
|
|
65
|
+
- A claim that never bound to a child session was logged as `Infinitys of
|
|
66
|
+
silence` and cooled the provider, though an unbound claim is no evidence that
|
|
67
|
+
anything hung. It is now released without cooling, and a child created before
|
|
68
|
+
its claim is bound at reap time instead of being lost.
|
|
69
|
+
- **A live task lost its slot past the TTL.** Claims now carry a heartbeat
|
|
70
|
+
refreshed by message activity, and abandonment is measured from it. A task
|
|
71
|
+
that ran past the 10-minute TTL previously stopped being counted while it
|
|
72
|
+
kept working, so the slot was handed out again — a ceiling-3 slot ran five.
|
|
73
|
+
|
|
74
|
+
## [0.3.0] — a sound per notification outcome (US-29)
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- Notification outcomes are per-notification and sound, rather than one silent
|
|
79
|
+
aggregate.
|
|
80
|
+
|
|
81
|
+
## [0.2.2] — stop capping gateway traffic at 5s
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- `timeoutMs` no longer caps real provider traffic at 5s. It was documented as
|
|
86
|
+
a probe budget only, but it was being applied to live completions too.
|
|
87
|
+
|
|
88
|
+
## [0.2.1] — OmniRoute registers the provider only
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- The OmniRoute integration registers the provider and nothing else; it writes
|
|
93
|
+
no pool file and no variants.
|
|
94
|
+
|
|
95
|
+
## [0.2.0] — OmniRoute as the pool's primary slot
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- OmniRoute, a local OpenAI-compatible gateway, as the pool's primary slot, with
|
|
100
|
+
combo routing, sticky sessions and cooldown on usage limits.
|
|
101
|
+
|
|
102
|
+
## [0.1.10] — observable self-update outcome
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
|
|
106
|
+
- The self-update reports its outcome instead of failing silently.
|
|
107
|
+
|
|
108
|
+
## [0.1.9] — reclaim update locks orphaned by a dead process
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- An update lock left behind by a dead process is reclaimed rather than blocking
|
|
113
|
+
every later update.
|
|
114
|
+
|
|
115
|
+
## [0.1.7] — self-update, hermetic tests
|
|
116
|
+
|
|
117
|
+
### Added
|
|
118
|
+
|
|
119
|
+
- Self-update on restart, and a hermetic test suite that does not read the
|
|
120
|
+
developer's own `~/.config`.
|
|
121
|
+
|
|
122
|
+
## [0.1.6] — small implement asks, Step 5 Preview pool
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
|
|
126
|
+
- Implementation asks are split into smaller units.
|
|
127
|
+
- `opencode-go/step-5-preview-free` added to the bundled pool preset.
|
|
128
|
+
|
|
129
|
+
## [0.1.5] — build/plan may spawn basic and expert
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
|
|
133
|
+
- `build` and `plan` may spawn `basic` and `expert`, which are not pooled.
|
|
134
|
+
|
|
135
|
+
## [0.1.4] — flat pool.txt
|
|
136
|
+
|
|
137
|
+
### Changed
|
|
138
|
+
|
|
139
|
+
- The pool's user config is a flat `~/.config/lacode/pool.txt`, one model per
|
|
140
|
+
line with an optional weight.
|
|
141
|
+
|
|
142
|
+
## [0.1.3] — ship the updated notification/remote docs
|
|
143
|
+
|
|
144
|
+
### Changed
|
|
145
|
+
|
|
146
|
+
- Notification and remote-control documentation brought up to date.
|
|
147
|
+
|
|
148
|
+
## [0.1.1]
|
|
149
|
+
|
|
150
|
+
### Added
|
|
151
|
+
|
|
152
|
+
- First published release: pooled subagents over a shared set of models.
|
|
153
|
+
|
|
154
|
+
[Unreleased]: https://github.com/lawaty/lacode/compare/v0.4.1...HEAD
|
|
155
|
+
[0.4.1]: https://github.com/lawaty/lacode/compare/v0.4.0...v0.4.1
|
|
156
|
+
[0.4.0]: https://github.com/lawaty/lacode/compare/v0.3.0...v0.4.0
|
|
157
|
+
[0.3.0]: https://github.com/lawaty/lacode/compare/v0.2.2...v0.3.0
|
|
158
|
+
[0.2.2]: https://github.com/lawaty/lacode/compare/v0.2.1...v0.2.2
|
|
159
|
+
[0.2.1]: https://github.com/lawaty/lacode/compare/v0.2.0...v0.2.1
|
|
160
|
+
[0.2.0]: https://github.com/lawaty/lacode/compare/v0.1.10...v0.2.0
|
|
161
|
+
[0.1.10]: https://github.com/lawaty/lacode/compare/v0.1.9...v0.1.10
|
|
162
|
+
[0.1.9]: https://github.com/lawaty/lacode/compare/v0.1.7...v0.1.9
|
|
163
|
+
[0.1.7]: https://github.com/lawaty/lacode/compare/v0.1.6...v0.1.7
|
|
164
|
+
[0.1.6]: https://github.com/lawaty/lacode/compare/v0.1.5...v0.1.6
|
|
165
|
+
[0.1.5]: https://github.com/lawaty/lacode/compare/v0.1.4...v0.1.5
|
|
166
|
+
[0.1.4]: https://github.com/lawaty/lacode/compare/v0.1.3...v0.1.4
|
|
167
|
+
[0.1.3]: https://github.com/lawaty/lacode/compare/v0.1.1...v0.1.3
|
|
168
|
+
[0.1.1]: https://github.com/lawaty/lacode/releases/tag/v0.1.1
|
package/README.md
CHANGED
|
@@ -13,6 +13,9 @@ desktop.
|
|
|
13
13
|
Install it with one config line. It adds agents and commands it finds missing,
|
|
14
14
|
and it never overwrites one you already defined.
|
|
15
15
|
|
|
16
|
+
Upgrading? See [CHANGELOG.md](CHANGELOG.md) — it ships with the package, so it
|
|
17
|
+
is also on disk at `node_modules/@lawaty/lacode/CHANGELOG.md`.
|
|
18
|
+
|
|
16
19
|
## Why
|
|
17
20
|
|
|
18
21
|
| Capability | The problem it solves |
|
|
@@ -586,8 +589,11 @@ fields are not affected; `permission`, `prompt`, `model`, `description`,
|
|
|
586
589
|
```
|
|
587
590
|
|
|
588
591
|
The hidden pool variants (`explore-1`, `implement-2`, …) inherit
|
|
589
|
-
nothing here — add a line per variant
|
|
592
|
+
nothing here — add a line per variant, or drop the global
|
|
590
593
|
deny and scope it to the agents that should not have browser access instead.
|
|
594
|
+
Note that you cannot *spawn* a variant by name: the pool refuses
|
|
595
|
+
`explore-1` and friends and points you at the bare base, so per-variant
|
|
596
|
+
permissions only ever apply to a spawn the plugin routed itself.
|
|
591
597
|
|
|
592
598
|
Nothing hot-reloads: after editing your config, restart every running opencode
|
|
593
599
|
process.
|
package/dist/agents.js
CHANGED
|
@@ -73,15 +73,9 @@ Every question of the form "what does this file do", "where is X handled", or
|
|
|
73
73
|
that proves it. Never one whole refactor handed over in a single piece \u2014
|
|
74
74
|
see \`# Task size\` below.
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
\`context-1\`..). The plugin owns which model every pooled spawn runs on: it
|
|
80
|
-
picks the next slot from the pool's weights, current load and cooldowns, and
|
|
81
|
-
rewrites your spawn to that variant for you. Naming a variant bypasses that
|
|
82
|
-
decision, defeats failover, and can pin work to a cooling or unhealthy model.
|
|
83
|
-
If you catch yourself choosing a slot number, you are doing the plugin's job \u2014
|
|
84
|
-
spawn \`explore\` and let it decide.
|
|
76
|
+
Spawn the BARE pooled base \u2014 \`explore\`, \`implement\` or \`context\`. The pool
|
|
77
|
+
refuses a \`-N\` variant, and a refusal means spawn the bare base, never a
|
|
78
|
+
different number.
|
|
85
79
|
|
|
86
80
|
# When the pool is unavailable
|
|
87
81
|
|
|
@@ -201,10 +195,8 @@ When you need to understand the codebase:
|
|
|
201
195
|
2. Invoke @explore with specific, focused instructions (e.g. "read src/auth.ts", "grep for 'migration' in *.py files", "list the src/ directory")
|
|
202
196
|
3. @explore will return its findings \u2014 you process them and plan accordingly
|
|
203
197
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
doing so bypasses the pool's weights, load and cooldowns, which is the one thing
|
|
207
|
-
the plugin exists to decide.
|
|
198
|
+
Spawn the BARE pooled base (\`explore\`, \`implement\`, \`context\`); the pool
|
|
199
|
+
refuses \`-N\` variants, and a refusal means spawn the bare base, not another number.
|
|
208
200
|
|
|
209
201
|
If the pool cannot serve a spawn \u2014 every slot cooling or exhausted, or repeated
|
|
210
202
|
failures \u2014 do NOT fall back to reading or planning from your own inspection.
|
package/dist/index.js
CHANGED
|
@@ -510,6 +510,11 @@ function pickSlot(s, snap, options) {
|
|
|
510
510
|
function agentFor(base, slot) {
|
|
511
511
|
return `${base}-${slot}`;
|
|
512
512
|
}
|
|
513
|
+
function manualVariant(name) {
|
|
514
|
+
const direct = /^(.*)-([1-9][0-9]*)$/.exec(name);
|
|
515
|
+
if (!direct) return void 0;
|
|
516
|
+
return BASES.includes(direct[1]) ? direct[1] : void 0;
|
|
517
|
+
}
|
|
513
518
|
function acquire(s, base, slot, ownerSession, key, target) {
|
|
514
519
|
const now = Date.now();
|
|
515
520
|
s.own.set(key, { v: base, k: slot.index, m: slot.model, t: now, hb: now, s: ownerSession, g: target });
|
|
@@ -626,15 +631,9 @@ Every question of the form "what does this file do", "where is X handled", or
|
|
|
626
631
|
that proves it. Never one whole refactor handed over in a single piece \u2014
|
|
627
632
|
see \`# Task size\` below.
|
|
628
633
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
\`context-1\`..). The plugin owns which model every pooled spawn runs on: it
|
|
633
|
-
picks the next slot from the pool's weights, current load and cooldowns, and
|
|
634
|
-
rewrites your spawn to that variant for you. Naming a variant bypasses that
|
|
635
|
-
decision, defeats failover, and can pin work to a cooling or unhealthy model.
|
|
636
|
-
If you catch yourself choosing a slot number, you are doing the plugin's job \u2014
|
|
637
|
-
spawn \`explore\` and let it decide.
|
|
634
|
+
Spawn the BARE pooled base \u2014 \`explore\`, \`implement\` or \`context\`. The pool
|
|
635
|
+
refuses a \`-N\` variant, and a refusal means spawn the bare base, never a
|
|
636
|
+
different number.
|
|
638
637
|
|
|
639
638
|
# When the pool is unavailable
|
|
640
639
|
|
|
@@ -754,10 +753,8 @@ When you need to understand the codebase:
|
|
|
754
753
|
2. Invoke @explore with specific, focused instructions (e.g. "read src/auth.ts", "grep for 'migration' in *.py files", "list the src/ directory")
|
|
755
754
|
3. @explore will return its findings \u2014 you process them and plan accordingly
|
|
756
755
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
doing so bypasses the pool's weights, load and cooldowns, which is the one thing
|
|
760
|
-
the plugin exists to decide.
|
|
756
|
+
Spawn the BARE pooled base (\`explore\`, \`implement\`, \`context\`); the pool
|
|
757
|
+
refuses \`-N\` variants, and a refusal means spawn the bare base, not another number.
|
|
761
758
|
|
|
762
759
|
If the pool cannot serve a spawn \u2014 every slot cooling or exhausted, or repeated
|
|
763
760
|
failures \u2014 do NOT fall back to reading or planning from your own inspection.
|
|
@@ -1609,9 +1606,19 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1609
1606
|
},
|
|
1610
1607
|
"tool.execute.before": async (input, output) => {
|
|
1611
1608
|
if (input.tool !== "task") return;
|
|
1609
|
+
const requested = output.args?.subagent_type;
|
|
1610
|
+
if (typeof requested !== "string") return;
|
|
1611
|
+
const manual = manualVariant(requested);
|
|
1612
|
+
if (manual) {
|
|
1613
|
+
await log("warn", `refused manual spawn of slot variant ${requested}; spawn "${manual}" instead`, {
|
|
1614
|
+
variant: requested,
|
|
1615
|
+
base: manual
|
|
1616
|
+
});
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
`${requested} is a pool slot variant and cannot be spawned by name. Naming it lets the caller pick the provider: it skips the round-robin rotation and the slot's ceiling, so one provider takes every concurrent claim. Spawn "${manual}" and let the pool choose the slot.`
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1612
1621
|
try {
|
|
1613
|
-
const requested = output.args?.subagent_type;
|
|
1614
|
-
if (typeof requested !== "string") return;
|
|
1615
1622
|
await reapStuck();
|
|
1616
1623
|
refreshCatalog(s, serverUrl);
|
|
1617
1624
|
const snap = snapshot(s);
|
|
@@ -1633,11 +1640,6 @@ var agentPoolHooks = (async ({ client, serverUrl }, options) => {
|
|
|
1633
1640
|
});
|
|
1634
1641
|
return;
|
|
1635
1642
|
}
|
|
1636
|
-
const direct = /^(.*)-([1-9][0-9]*)$/.exec(requested);
|
|
1637
|
-
if (direct && BASES.includes(direct[1])) {
|
|
1638
|
-
const slot = s.slots.find((sl) => sl.index === Number(direct[2]));
|
|
1639
|
-
if (slot) acquire(s, direct[1], slot, input.sessionID, claimKey(input.sessionID, input.callID));
|
|
1640
|
-
}
|
|
1641
1643
|
} catch (error) {
|
|
1642
1644
|
await log("error", `routing failed, spawn unchanged: ${String(error)}`);
|
|
1643
1645
|
}
|
package/dist/pool.js
CHANGED
|
@@ -510,6 +510,11 @@ function pickSlot(s, snap, options) {
|
|
|
510
510
|
function agentFor(base, slot) {
|
|
511
511
|
return `${base}-${slot}`;
|
|
512
512
|
}
|
|
513
|
+
function manualVariant(name) {
|
|
514
|
+
const direct = /^(.*)-([1-9][0-9]*)$/.exec(name);
|
|
515
|
+
if (!direct) return void 0;
|
|
516
|
+
return BASES.includes(direct[1]) ? direct[1] : void 0;
|
|
517
|
+
}
|
|
513
518
|
function acquire(s, base, slot, ownerSession, key, target) {
|
|
514
519
|
const now = Date.now();
|
|
515
520
|
s.own.set(key, { v: base, k: slot.index, m: slot.model, t: now, hb: now, s: ownerSession, g: target });
|
|
@@ -637,6 +642,7 @@ export {
|
|
|
637
642
|
isLimitText,
|
|
638
643
|
isQuotaReason,
|
|
639
644
|
isUnavailableText,
|
|
645
|
+
manualVariant,
|
|
640
646
|
noteActivity,
|
|
641
647
|
parseSlotText,
|
|
642
648
|
parseSlots,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lawaty/lacode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"presets",
|
|
46
46
|
"sounds",
|
|
47
47
|
"LICENSE",
|
|
48
|
+
"CHANGELOG.md",
|
|
48
49
|
"README.md",
|
|
49
50
|
"docs"
|
|
50
51
|
],
|