@lawaty/lacode 0.2.2 → 0.4.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.
@@ -96,13 +96,13 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
96
96
  - [ ] Given every model is cooling, When a spawn is routed, Then the least-loaded slot is chosen anyway (degrade, never block).
97
97
  - [ ] Given a limit error names a reset time, When the cooldown is recorded, Then the stated reset is used instead of a guessed backoff.
98
98
 
99
- ### US-5: Weighted preference drains preferred providers first
100
- **As a** user, **I want** to express preference between models via weights, **so that** the pool prefers the cheapest/freest provider up to its ceiling.
99
+ ### US-5: Weighted round-robin gives every slot a turn per cycle
100
+ **As a** user, **I want** weights to say how many turns each slot takes per rotation cycle, **so that** no provider is drained to its limit while the others sit idle.
101
101
 
102
102
  **Story ID(s) for tests:** US-5
103
103
  **Acceptance criteria:**
104
- - [ ] Given a heavy slot and a light slot, When spawns are routed below both ceilings, Then the heavier slot is filled before the lighter one.
105
- - [ ] Given a slot is at its weight ceiling, When another spawn is routed, Then the pick moves to the next slot under its ceiling.
104
+ - [ ] Given a heavy slot and a light slot, When spawns are routed below both ceilings, Then the rotation walks the list in order with each slot taking as many consecutive turns as its weight says, rather than the heavy slot being filled and refilled first.
105
+ - [ ] Given a slot is at its weight ceiling or cooling, When its turn comes up in the rotation, Then the turn is skipped and the rotation resumes at the following turn.
106
106
  - [ ] Given every slot is at its ceiling, When another spawn is routed, Then it goes to the least-loaded slot as overflow.
107
107
  - [ ] Given all slots carry equal weight, When spawns are routed, Then the picks round-robin in declaration order.
108
108
  - [ ] Given per-slot load is read, When it is asserted, Then a slot's load never exceeds its weight ceiling except by overflow.
@@ -115,7 +115,7 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
115
115
  - [ ] Given claims are held by several processes, When the status view is requested, Then it shows every process's claims and the aggregate load per slot.
116
116
  - [ ] Given a model is cooling with a stated reason, When the status view is requested, Then the model is listed as cooling together with that reason.
117
117
  - [ ] Given a claim file is older than the dead-file threshold, When the status view is computed, Then the stale claim is pruned rather than counted.
118
- - [ ] Given a slot is at its ceiling, When the status view is requested, Then it is marked as the slot the next spawn would take.
118
+ - [ ] Given a slot is at its ceiling, When the status view is requested, Then it is not marked as the slot the next spawn would take, because the rotation's turns are skipped for a full slot until it frees a claim.
119
119
 
120
120
  ### US-7: Non-free models still work
121
121
  **As a** user who wants a paid model in the pool, **I want** to supply any model list, **so that** the pool is not restricted to free tiers.
@@ -222,6 +222,18 @@ does not.
222
222
  - [ ] Given the plugin observes a permission prompt, When it handles the event, Then it is a read-only observer: the payload is not mutated and no `permission.ask` hook is registered, because that hook's `output.status` is the permission decision itself.
223
223
  - [ ] Given a `permission` notification is raised, When it is evaluated against the cooldown, Then it bypasses it, since an unannounced block is the failure this story exists to remove.
224
224
 
225
+ ### US-29: Notification sounds distinguish outcomes and can be switched off
226
+ **As a** user running many parallel sessions, **I want** each notification to arrive with its own sound, **so that** I can tell a finished session from a question or a permission prompt without looking at the popup.
227
+
228
+ **Story ID(s) for tests:** US-29
229
+ **Acceptance criteria:**
230
+ - [ ] Given any of the four outcomes, When its notification is delivered, Then it carries that outcome's own sound name — `done`, `question`, `permission` or `error` — and no two share one, so the four are distinguishable by ear alone.
231
+ - [ ] Given a notification raised on a remote host, When it reaches the desktop, Then the sound plays where the notification is delivered and never on the opencode host, because a headless host has no speakers for you to hear.
232
+ - [ ] Given no sound player is installed, or the player cannot start, When the notification is delivered, Then the notification is delivered unchanged and only the sound is skipped, because a missing audio tool must never cost you the alert itself.
233
+ - [ ] Given a machine with no notification channel at all, When a notification is raised, Then its sound still plays, because on such a desktop the tone is the only alert there is.
234
+ - [ ] Given the plugin option `sound` is `false`, or `LACODE_SOUND` is `0`, `false`, `off` or `no`, When a notification is delivered, Then its sound is `none`; when both are set the environment wins over the option, and any other value — a typo included — leaves sound on, because a misspelt variable must not be how you get silence.
235
+ - [ ] Given a sound name that is not one of `none`, `done`, `question`, `permission` or `error`, When it arrives, Then it is treated as `none` and never becomes a path or a command, because the hop carries data only (see US-18).
236
+
225
237
  ---
226
238
 
227
239
  ## Epic E — Remote hosts over SSH
@@ -431,6 +443,7 @@ table was filled in.
431
443
  | US-11 | D | `tests/stories/d-notifications.test.mjs`, `tests/notify-test.mjs` |
432
444
  | US-12 | D | `tests/stories/d-notifications.test.mjs`, `tests/notify-test.mjs` |
433
445
  | US-25 | D | `tests/stories/d-notifications.test.mjs` |
446
+ | US-29 | D | `tests/stories/d-notifications.test.mjs` |
434
447
  | US-13 | E | `tests/stories/e-remote-ssh.test.mjs`, `tests/notify-test.mjs`, `tests/oc-test.mjs` |
435
448
  | US-14 | E | `tests/stories/e-remote-ssh.test.mjs`, `tests/oc-test.mjs` |
436
449
  | US-18 | E | `tests/stories/e-remote-ssh.test.mjs`, `tests/oc-test.mjs` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  "files": [
44
44
  "dist",
45
45
  "presets",
46
+ "sounds",
46
47
  "LICENSE",
47
48
  "README.md",
48
49
  "docs"
package/presets/README.md CHANGED
@@ -62,7 +62,7 @@ else means the flat form.
62
62
  | Field | Required | Meaning |
63
63
  |-------|----------|---------|
64
64
  | `model` | yes | `provider/model-id`, split on the **first** slash only |
65
- | `weight` | no | soft ceiling on concurrent claims, and priority. Absent = 1 |
65
+ | `weight` | no | soft ceiling on concurrent claims, and turns taken per rotation. Absent = 1 |
66
66
 
67
67
  `$comment` is a JSON-legal array of strings and is ignored by the loader; the
68
68
  bundled preset uses it to explain itself in place.
@@ -91,17 +91,28 @@ because the pool requires it.
91
91
 
92
92
  ## Weight, precisely
93
93
 
94
- A weight is a count of concurrent sessions, used as both a ceiling and a
95
- priority:
94
+ A weight is a count of concurrent sessions — a soft ceiling — and the number
95
+ of turns a slot takes per rotation. Routing is **order-first weighted
96
+ round-robin**:
96
97
 
97
- 1. the **highest-weight** slot still under its own ceiling takes the next spawn
98
- 2. so slot 1 fills to its number before slot 2 sees any work at all
98
+ 1. the slot list is expanded into a run in which each slot appears once per
99
+ unit of its weight — `[3,2,1,1]` becomes `1,1,1,2,2,3,4` — and the next
100
+ spawn takes the turn a persistent cursor (a file in the pool directory,
101
+ shared by every opencode process) points at
102
+ 2. a turn whose slot is cooling or already at its ceiling is **skipped**, and
103
+ the rotation resumes at the following turn
99
104
  3. once **every** slot is at its ceiling, overflow goes to the least loaded slot —
100
105
  a burst past the summed ceilings degrades to even spreading and never blocks
101
106
 
107
+ Weight is no longer a priority: a heavy slot is not drained to its ceiling
108
+ before the others see any work. Every slot gets a turn each cycle, so a fast
109
+ provider with a large usage window is used early in the cycle while it is
110
+ still fast — instead of one provider being refilled until it hits its limit
111
+ and its 429s displace every agent type at once.
112
+
102
113
  Equal weights give a plain round robin in list order. List order is the
103
- tie-break, so put the models you want busiest first and treat the bottom of the
104
- list as reserve capacity.
114
+ rotation itself, so put the models you want taking the earlier turns of a
115
+ cycle at the top and the ones you want taking the later turns at the bottom.
105
116
 
106
117
  Spreading at least two providers across the slots is worth doing deliberately: a
107
118
  provider-wide rate limit takes out every slot on that provider at once.
@@ -14,20 +14,34 @@
14
14
  "route at an agent that does not exist. Extra entries are dropped with a warning.",
15
15
  "",
16
16
  "WEIGHT IS THE ONLY KNOWN KNOB, and it is a SOFT CEILING on how many concurrent claims a",
17
- "slot holds -- not a share of the work. Priority is that same number, so the rule is:",
18
- " 1. the HIGHEST-weight slot that is still under its ceiling takes the spawn",
19
- " 2. slot 1 therefore fills to its number before slot 2 sees any work at all",
17
+ "slot holds -- not a share of the work and not a priority. Routing is ORDER-FIRST",
18
+ "WEIGHTED ROUND-ROBIN: the slot list is walked IN ORDER and each slot takes as many",
19
+ "consecutive turns as its weight says, so weights 3,2,1,1 produce the run 1,1,1,2,2,3,4,",
20
+ "which then repeats. The rule is:",
21
+ " 1. a persistent cursor in the pool directory marks where in that run the next spawn",
22
+ " lands, so every opencode process on the machine shares one rotation",
23
+ " 2. a turn is SKIPPED only when its slot is cooling or already at its ceiling, and the",
24
+ " rotation resumes at the following turn",
20
25
  " 3. once EVERY slot is at its ceiling, the overflow goes to the least loaded slot,",
21
26
  " so a burst past the summed ceilings degrades to even spreading and never blocks",
22
27
  "",
23
- "With this file: space-bunny takes 3 sessions to itself, then step-5-preview",
28
+ "With this file: space-bunny takes 3 consecutive turns, then step-5-preview",
24
29
  "(opencode-go) takes 2, then big-pickle and step-5-preview (Zen) take 1 each -- sequence 1,1,1,2,2,3,4,...",
25
30
  "Equal weights give a plain round robin in list order. 2,2,1,1 reproduces the old",
26
31
  "primary/overflow cycle exactly, which is why the old tier values still work.",
27
32
  "",
33
+ "Round-robin rather than heaviest-first because re-prioritising the heaviest slot on",
34
+ "EVERY spawn drained the fastest provider's free-tier window early: one provider",
35
+ "absorbed all work up to its ceiling and kept being refilled the moment a claim",
36
+ "completed, while the other slots sat idle -- until the fast provider hit its limit and",
37
+ "its 429s displaced every agent type at once. Every slot a turn per cycle puts a fast",
38
+ "slot with a large usage window to work early in the cycle while it is still fast, and",
39
+ "no provider's quota sits unused while another's is drained.",
40
+ "",
28
41
  "Absent means weight 1. Must be a number in (0, 100]; anything else warns and uses 1.",
29
- "List order is the tie-break between slots of equal weight, so put the ones you want",
30
- "first and treat the bottom of the list as the reserve.",
42
+ "List order sets the rotation for every slot, not just a tie-break: earlier positions",
43
+ "take their turns earlier in each cycle, so put the slots you want worked first at the",
44
+ "top of the list.",
31
45
  "",
32
46
  "COST IS THE DEFAULT, NOT A CONSTRAINT. These four slots are free because that is what",
33
47
  "the pool is FOR: mechanical work -- reading files, grepping, mechanical edits, cartography.",
Binary file
Binary file
Binary file
Binary file