@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 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 you actually spawn, or drop the global
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
- The agents you spawn are the pooled \`explore\` / \`implement\` / \`context\`
77
- bases. **Always spawn the BARE base name and let the plugin choose the slot;
78
- never name a \`-N\` variant** (\`explore-1\`..\`explore-4\`, \`implement-1\`..,
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
- Always spawn the BARE pooled base (\`explore\`, \`implement\`, \`context\`) and let
205
- the plugin choose the slot. Never name a \`-N\` variant (\`explore-1\`..\`explore-4\`);
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
- The agents you spawn are the pooled \`explore\` / \`implement\` / \`context\`
630
- bases. **Always spawn the BARE base name and let the plugin choose the slot;
631
- never name a \`-N\` variant** (\`explore-1\`..\`explore-4\`, \`implement-1\`..,
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
- Always spawn the BARE pooled base (\`explore\`, \`implement\`, \`context\`) and let
758
- the plugin choose the slot. Never name a \`-N\` variant (\`explore-1\`..\`explore-4\`);
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.0",
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
  ],