@lawaty/lacode 0.11.0 → 1.0.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 +88 -1
- package/README.md +63 -29
- package/dist/agents.js +24 -16
- package/dist/commands.js +11 -3
- package/dist/index.js +576 -118
- package/dist/lacode.js +5 -5
- package/dist/pool.js +14 -38
- package/dist/tunnel.js +2 -2
- package/docs/CONFIGURATION.md +2 -2
- package/docs/USER-STORIES.md +15 -16
- package/package.json +2 -2
- package/presets/README.md +22 -24
- package/presets/free-tier.json +24 -40
- package/sounds/done.wav +0 -0
- package/sounds/error.wav +0 -0
- package/sounds/permission.wav +0 -0
- package/sounds/question.wav +0 -0
package/dist/lacode.js
CHANGED
|
@@ -12,9 +12,9 @@ import { homedir as homedir3 } from "node:os";
|
|
|
12
12
|
import { join as join4 } from "node:path";
|
|
13
13
|
|
|
14
14
|
// src/lib/notify.ts
|
|
15
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, statSync as statSync2, unlinkSync as unlinkSync2, writeSync } from "node:fs";
|
|
15
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, writeSync } from "node:fs";
|
|
16
16
|
import { homedir as homedir2 } from "node:os";
|
|
17
|
-
import { join as join3 } from "node:path";
|
|
17
|
+
import { basename, join as join3 } from "node:path";
|
|
18
18
|
|
|
19
19
|
// src/lib/pool.ts
|
|
20
20
|
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -466,7 +466,7 @@ function describe(env = process.env, platform = process.platform) {
|
|
|
466
466
|
|
|
467
467
|
// src/lib/selfupdate.ts
|
|
468
468
|
import { spawn as spawn2 } from "node:child_process";
|
|
469
|
-
import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as
|
|
469
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync3, rmSync, statSync as statSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3, writeSync as writeSync2 } from "node:fs";
|
|
470
470
|
import { homedir as homedir4 } from "node:os";
|
|
471
471
|
import { dirname as dirname2, join as join5, resolve } from "node:path";
|
|
472
472
|
var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
@@ -568,7 +568,7 @@ async function refreshCache(opts) {
|
|
|
568
568
|
const aside = `${cacheRoot}.stale-${Date.now()}`;
|
|
569
569
|
try {
|
|
570
570
|
rmSync(aside, { recursive: true, force: true });
|
|
571
|
-
|
|
571
|
+
renameSync3(cacheRoot, aside);
|
|
572
572
|
} catch (error) {
|
|
573
573
|
return { ok: false, detail: `could not set the old cache aside: ${String(error)}` };
|
|
574
574
|
}
|
|
@@ -603,7 +603,7 @@ async function refreshCache(opts) {
|
|
|
603
603
|
}
|
|
604
604
|
} else {
|
|
605
605
|
try {
|
|
606
|
-
|
|
606
|
+
renameSync3(aside, cacheRoot);
|
|
607
607
|
} catch {
|
|
608
608
|
}
|
|
609
609
|
}
|
package/dist/pool.js
CHANGED
|
@@ -626,48 +626,24 @@ function snapshot(s) {
|
|
|
626
626
|
}
|
|
627
627
|
var loadOf = (counts, model) => counts.get(model) ?? 0;
|
|
628
628
|
var underCeiling = (slot, counts) => loadOf(counts, slot.model) < slotCeiling(slot);
|
|
629
|
-
function
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
function readCursor(s) {
|
|
636
|
-
try {
|
|
637
|
-
const parsed = JSON.parse(readFileSync2(join2(s.dir, CURSOR_FILE), "utf8"));
|
|
638
|
-
if (typeof parsed.seq === "number" && Number.isInteger(parsed.seq) && parsed.seq >= 0) return parsed.seq;
|
|
639
|
-
} catch {
|
|
629
|
+
function pickSlot(s, snap) {
|
|
630
|
+
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
631
|
+
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
632
|
+
const pool = usable.length > 0 ? usable : slots;
|
|
633
|
+
for (const slot of usable) {
|
|
634
|
+
if (loadOf(snap.counts, slot.model) < slot.weight) return slot;
|
|
640
635
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
function writeCursor(s, seq) {
|
|
644
|
-
if (!s.dirReady) return;
|
|
645
|
-
const path = join2(s.dir, CURSOR_FILE);
|
|
646
|
-
try {
|
|
647
|
-
writeFileSync(`${path}.tmp`, JSON.stringify({ seq }));
|
|
648
|
-
renameSync(`${path}.tmp`, path);
|
|
649
|
-
} catch {
|
|
636
|
+
for (const slot of usable) {
|
|
637
|
+
if (loadOf(snap.counts, slot.model) < slotCeiling(slot)) return slot;
|
|
650
638
|
}
|
|
639
|
+
return pool.reduce((best, slot) => loadOf(snap.counts, slot.model) < loadOf(snap.counts, best.model) ? slot : best, pool[0]);
|
|
651
640
|
}
|
|
652
|
-
function
|
|
653
|
-
const advance = options?.advance !== false;
|
|
641
|
+
function poolPass(s, snap) {
|
|
654
642
|
const slots = s.slots.length > 0 ? s.slots : FALLBACK_SLOTS;
|
|
655
|
-
const seq = roundRobinSequence(slots);
|
|
656
|
-
if (seq.length > 0) {
|
|
657
|
-
const byIndex = new Map(slots.map((slot) => [slot.index, slot]));
|
|
658
|
-
const start = readCursor(s) % seq.length;
|
|
659
|
-
for (let step = 0; step < seq.length; step++) {
|
|
660
|
-
const at = (start + step) % seq.length;
|
|
661
|
-
const slot = byIndex.get(seq[at]);
|
|
662
|
-
if (slot && isAvailable(s, slot.model, snap.cooling) && underCeiling(slot, snap.counts)) {
|
|
663
|
-
if (advance) writeCursor(s, at + 1);
|
|
664
|
-
return slot;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
643
|
const usable = slots.filter((slot) => isAvailable(s, slot.model, snap.cooling));
|
|
669
|
-
|
|
670
|
-
|
|
644
|
+
if (usable.some((slot) => loadOf(snap.counts, slot.model) < slot.weight)) return 1;
|
|
645
|
+
if (usable.some((slot) => loadOf(snap.counts, slot.model) < slotCeiling(slot))) return 2;
|
|
646
|
+
return "overflow";
|
|
671
647
|
}
|
|
672
648
|
function agentFor(base, slot) {
|
|
673
649
|
return `${base}-${slot}`;
|
|
@@ -829,6 +805,7 @@ export {
|
|
|
829
805
|
parseSlots,
|
|
830
806
|
parseTextSlots,
|
|
831
807
|
pickSlot,
|
|
808
|
+
poolPass,
|
|
832
809
|
providerOf,
|
|
833
810
|
pruneOwn,
|
|
834
811
|
pruneSessions,
|
|
@@ -843,7 +820,6 @@ export {
|
|
|
843
820
|
reloadSlots,
|
|
844
821
|
resolveModels,
|
|
845
822
|
retryAfterMs,
|
|
846
|
-
roundRobinSequence,
|
|
847
823
|
selfFile,
|
|
848
824
|
slotCeiling,
|
|
849
825
|
slotFor,
|
package/dist/tunnel.js
CHANGED
|
@@ -5,9 +5,9 @@ import { homedir as homedir3 } from "node:os";
|
|
|
5
5
|
import { join as join4 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/lib/notify.ts
|
|
8
|
-
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, statSync as statSync2, unlinkSync as unlinkSync2, writeSync } from "node:fs";
|
|
8
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, writeSync } from "node:fs";
|
|
9
9
|
import { homedir as homedir2 } from "node:os";
|
|
10
|
-
import { join as join3 } from "node:path";
|
|
10
|
+
import { basename, join as join3 } from "node:path";
|
|
11
11
|
|
|
12
12
|
// src/lib/pool.ts
|
|
13
13
|
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -68,8 +68,8 @@ fields are not affected; `permission`, `prompt`, `model`, `description`,
|
|
|
68
68
|
The hidden pool variants (`explore-1`, `implement-2`, …) inherit
|
|
69
69
|
nothing here — add a line per variant, or drop the global
|
|
70
70
|
deny and scope it to the agents that should not have browser access instead.
|
|
71
|
-
Note that you cannot *spawn* a variant by name: the pool
|
|
72
|
-
`explore-1` and friends
|
|
71
|
+
Note that you cannot *spawn* a variant by name: the pool rewrites
|
|
72
|
+
`explore-1` and friends to their base and routes them, so per-variant
|
|
73
73
|
permissions only ever apply to a spawn the plugin routed itself.
|
|
74
74
|
|
|
75
75
|
The pool files are the exception to the restart rule: editing `pool.txt`,
|
package/docs/USER-STORIES.md
CHANGED
|
@@ -15,11 +15,7 @@ line. Criteria that cannot be observed without a running server are not in this
|
|
|
15
15
|
document; everything here is testable offline with a stub client and a
|
|
16
16
|
`mkdtempSync` state directory.
|
|
17
17
|
|
|
18
|
-
**Terminology.** A *slot* is one entry in `presets/free-tier.json`, carrying a
|
|
19
|
-
model and a `weight`. A *base* is a pooled agent type (`explore`), and its slot
|
|
20
|
-
holders are `<base>-<slot>`, hidden in `opencode.jsonc`. A *claim* is a running
|
|
21
|
-
spawn recorded in `claims.<pid>.json`. A *context map* is the five-file
|
|
22
|
-
`.opencode/context/` codemap described in Epic G.
|
|
18
|
+
**Terminology.** A *slot* is one entry in `presets/free-tier.json`, carrying a model and a `weight`. A slot's `weight` is its active-session budget for one pass: the number of concurrent claims it may hold before the next slot is preferred. A *pass* is one declaration-order sweep of the slot list; the pool runs at most two passes, and a slot's absolute cap is twice its weight. Preference is by declaration order, never by weight. A *base* is a pooled agent type (`explore`), and its slot holders are `<base>-<slot>`, hidden in `opencode.jsonc`. A *claim* is a running spawn recorded in `claims.<pid>.json`. A *context map* is the five-file `.opencode/context/` codemap described in Epic G.
|
|
23
19
|
|
|
24
20
|
---
|
|
25
21
|
|
|
@@ -96,16 +92,18 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
|
|
|
96
92
|
- [ ] Given every model is cooling, When a spawn is routed, Then the least-loaded slot is chosen anyway (degrade, never block).
|
|
97
93
|
- [ ] 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
94
|
|
|
99
|
-
### US-5:
|
|
100
|
-
**As a** user, **I want**
|
|
95
|
+
### US-5: Slots fill in declaration order, in up to two passes
|
|
96
|
+
**As a** user, **I want** work to prefer the first slot and spill to later slots only when the earlier ones are full, **so that** a healthy preferred model is used before fallbacks and no slot is used while an earlier usable slot has room.
|
|
101
97
|
|
|
102
98
|
**Story ID(s) for tests:** US-5
|
|
103
99
|
**Acceptance criteria:**
|
|
104
|
-
- [ ] Given
|
|
105
|
-
- [ ] Given
|
|
106
|
-
- [ ] Given
|
|
107
|
-
- [ ] Given
|
|
108
|
-
- [ ] Given
|
|
100
|
+
- [ ] Given live claims, When the pool has room in the first pass, Then the lowest-index usable slot whose load is below its weight is chosen, and a slot declared later is not used until every earlier usable slot has reached its weight.
|
|
101
|
+
- [ ] Given every usable slot has reached its weight, When another spawn is routed, Then the second pass begins at the lowest-index slot and fills each usable slot up to twice its weight.
|
|
102
|
+
- [ ] Given a slot is cooling or unavailable, When its position in the pass comes up, Then it is skipped and the pass continues to the next slot.
|
|
103
|
+
- [ ] Given a claim is released, When the next spawn is routed, Then the slot that freed capacity is preferred again in declaration order; no stored position keeps it waiting.
|
|
104
|
+
- [ ] Given every usable slot is at its absolute cap, When another spawn is routed, Then it goes to the least-loaded usable slot as overflow, and the pool never refuses work.
|
|
105
|
+
- [ ] Given all slots carry equal weight, When spawns are routed, Then the same ordered two-pass fill applies.
|
|
106
|
+
- [ ] Given per-slot load is read, When it is asserted, Then a slot's load never exceeds twice its weight except through overflow.
|
|
109
107
|
|
|
110
108
|
### US-6: Live pool visibility
|
|
111
109
|
**As a** user, **I want** to see per-slot load and which model is cooling and why, **so that** I understand where my subagents are going.
|
|
@@ -115,7 +113,8 @@ The reconciliation with US-7 is that **acceptance and endorsement are different
|
|
|
115
113
|
- [ ] 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
114
|
- [ ] 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
115
|
- [ ] 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
|
|
116
|
+
- [ ] Given a slot is at its absolute cap, When the status view is requested, Then it is not marked as the slot the next spawn would take while any usable slot is below its current pass target; when every usable slot is at its absolute cap, the least-loaded slot is marked next.
|
|
117
|
+
- [ ] Given the pool is filling, When the status view is requested, Then it shows whether the next spawn belongs to the first pass, the second pass, or overflow.
|
|
119
118
|
|
|
120
119
|
### US-7: Non-free models still work
|
|
121
120
|
**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.
|
|
@@ -320,7 +319,7 @@ The popup belongs to its session: working in one chat closes that chat's popup a
|
|
|
320
319
|
- [ ] Given a body containing `"`, `'`, `;`, `$(…)` or backticks, When the macOS send runs, Then it reaches `display notification` byte-for-byte as data and nothing is executed.
|
|
321
320
|
- [ ] Given a machine has no notifier for its platform, When a notification is sent, Then the result is a failed delivery with a reason and no exception, and `lacode doctor` names the channel that would run — or reports that there is none — instead of reporting a binary that is not used there.
|
|
322
321
|
- [ ] Given the CLI runs on macOS, When the send does not complete, Then it is still bounded by a timeout, exactly as on Linux.
|
|
323
|
-
- [ ] Given the notification hop, When it is documented, Then the doc leads with the shipped path — the npm-installed plugin, a direct ssh hop (`ControlMaster`) set up by `lacode tunnel setup`, and the portable `lacode receive` forced command that delivers through the platform channel (`notify-send` or `osascript`) — and marks the reverse-forward scripts (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`, `bin/oc-sync`) as the author's
|
|
322
|
+
- [ ] Given the notification hop, When it is documented, Then the doc leads with the shipped path — the npm-installed plugin, a direct ssh hop (`ControlMaster`) set up by `lacode tunnel setup`, and the portable `lacode receive` forced command that delivers through the platform channel (`notify-send` or `osascript`) — and marks the reverse-forward scripts (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`, `bin/oc-sync`) as the author's own scripts, so a Mac user is not sent to debug a script that was never meant to run on their machine.
|
|
324
323
|
- [ ] Given a machine without GNU `timeout` or with bash older than 4.0, When `bin/oc-sync` runs, Then it refuses **before** touching anything, names each missing prerequisite, and names the `lacode` subcommand to use instead. It must never report a misleading cause: the host list is read with `mapfile`, so without it the run dies at "no hosts configured" — a confident wrong answer that sends the reader to edit `.env`.
|
|
325
324
|
- [ ] Given `bin/oc-notify-receiver` runs where `notify-send` does not exist, When it is invoked, Then it exits non-zero and names both the missing channel and the portable path, because its stderr reaches whoever opened the ssh connection and nobody else, and a bare "command not found" reads as a desktop that quietly stopped notifying.
|
|
326
325
|
- [ ] Given the payload is parsed, When the receiver splits `$SSH_ORIGINAL_COMMAND`, Then it splits on newlines only using constructs that exist in bash 3.2 as well as bash 5, and the suite **proves the split by running the receiver** rather than by finding the word `mapfile` in the file. Under bash 3.2 — the version macOS ships — a bash-4-only builtin leaves every field at its default, so the popup says "LaCode" with an empty body and no error anywhere: the same silent blank notification the argv bug causes, reached through a different door.
|
|
@@ -511,8 +510,8 @@ table was filled in.
|
|
|
511
510
|
| US-3 | A | `tests/stories/a-cost-control.test.mjs` (harness: `tests/benchmark/run.mjs`) |
|
|
512
511
|
| US-23 | A | `tests/stories/a-cost-control.test.mjs` |
|
|
513
512
|
| US-4 | B | `tests/stories/b-provider-resilience.test.mjs`, `tests/pool-limits-test.mjs` |
|
|
514
|
-
| US-5 | B | `tests/stories/b-provider-resilience.test.mjs`, `tests/pool-test.mjs`, `tests/pool-models-test.mjs` |
|
|
515
|
-
| US-6 | B | `tests/stories/b-provider-resilience.test.mjs` |
|
|
513
|
+
| US-5 | B | `tests/stories/b-slot-fill.test.mjs`, `tests/stories/b-provider-resilience.test.mjs`, `tests/pool-test.mjs`, `tests/pool-models-test.mjs` |
|
|
514
|
+
| US-6 | B | `tests/stories/b-provider-resilience.test.mjs`, `tests/stories/b-slot-fill.test.mjs` |
|
|
516
515
|
| US-7 | B | `tests/stories/b-provider-resilience.test.mjs`, `tests/pool-models-test.mjs` |
|
|
517
516
|
| US-28 | B | `tests/stories/i-omniroute.test.mjs` |
|
|
518
517
|
| US-31 | B | `tests/stories/b-provider-resilience.test.mjs`, `tests/stories/c-hang-recovery.test.mjs` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lawaty/lacode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -62,6 +62,6 @@
|
|
|
62
62
|
"@opencode-ai/plugin": "1.18.34"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
65
|
-
"tag": "
|
|
65
|
+
"tag": "latest"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/presets/README.md
CHANGED
|
@@ -63,7 +63,7 @@ else means the flat form.
|
|
|
63
63
|
| Field | Required | Meaning |
|
|
64
64
|
|-------|----------|---------|
|
|
65
65
|
| `model` | yes | `provider/model-id`, split on the **first** slash only |
|
|
66
|
-
| `weight` | no |
|
|
66
|
+
| `weight` | no | active-session budget for one pass; its absolute cap is twice that. Absent = 1 |
|
|
67
67
|
|
|
68
68
|
`$comment` is a JSON-legal array of strings and is ignored by the loader; the
|
|
69
69
|
bundled preset uses it to explain itself in place.
|
|
@@ -81,7 +81,7 @@ The loader checks shape and nothing else:
|
|
|
81
81
|
- at most `MAX_SLOTS` (4) slots; extra entries are **dropped with a warning**,
|
|
82
82
|
because one hidden variant agent per base per slot exists and a fifth slot
|
|
83
83
|
would route at an agent that does not exist
|
|
84
|
-
- `tier` is accepted as
|
|
84
|
+
- `tier` is accepted as an alias for `weight` (`primary` → 2,
|
|
85
85
|
`overflow` → 1) and warns
|
|
86
86
|
|
|
87
87
|
**Cost is never validated.** Any provider/model-id string is accepted, including
|
|
@@ -92,28 +92,26 @@ because the pool requires it.
|
|
|
92
92
|
|
|
93
93
|
## Weight, precisely
|
|
94
94
|
|
|
95
|
-
A weight is
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
2.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
rotation itself, so put the models you want taking the earlier turns of a
|
|
116
|
-
cycle at the top and the ones you want taking the later turns at the bottom.
|
|
95
|
+
A weight is a slot's active-session budget for one pass: the number of
|
|
96
|
+
concurrent claims it may hold before the next slot is preferred. Its absolute
|
|
97
|
+
cap is **twice that weight** — a weight-3 slot may hold 6 concurrent sessions.
|
|
98
|
+
Slots are filled in declaration order; preference is order, never weight.
|
|
99
|
+
|
|
100
|
+
Routing runs in two passes:
|
|
101
|
+
|
|
102
|
+
1. pass 1 fills each usable slot up to its weight, in declaration order
|
|
103
|
+
2. once every usable slot has reached its weight, pass 2 repeats the order,
|
|
104
|
+
filling each usable slot up to twice its weight
|
|
105
|
+
|
|
106
|
+
A slot is skipped when it is cooling, unavailable, or at its absolute cap.
|
|
107
|
+
When every usable slot is at its absolute cap, overflow goes to the least
|
|
108
|
+
loaded usable slot; the pool never refuses work.
|
|
109
|
+
|
|
110
|
+
Live claims decide the next slot: a released claim immediately reopens its
|
|
111
|
+
slot, and there is no stored position.
|
|
112
|
+
|
|
113
|
+
Equal weights give a plain pass in list order. List order is preference, so
|
|
114
|
+
put the slots you want preferred first at the top of the list.
|
|
117
115
|
|
|
118
116
|
Spreading at least two providers across the slots is worth doing deliberately: a
|
|
119
117
|
provider-wide rate limit takes out every slot on that provider at once.
|
package/presets/free-tier.json
CHANGED
|
@@ -13,52 +13,36 @@
|
|
|
13
13
|
"AT MOST 4 SLOTS: one hidden variant per base per slot is declared, so a fifth entry would",
|
|
14
14
|
"route at an agent that does not exist. Extra entries are dropped with a warning.",
|
|
15
15
|
"",
|
|
16
|
-
"WEIGHT IS THE
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
16
|
+
"WEIGHT IS THE ACTIVE-SESSION BUDGET FOR ONE PASS: a slot may hold up to its weight in",
|
|
17
|
+
"concurrent claims before the next slot is preferred. Slots are filled in declaration",
|
|
18
|
+
"order; preference is order, never weight.",
|
|
19
|
+
"Routing runs in two passes:",
|
|
20
|
+
" 1. pass 1 fills each usable slot up to its weight, in declaration order",
|
|
21
|
+
" 2. once every usable slot has reached its weight, pass 2 repeats the order, filling",
|
|
22
|
+
" each usable slot up to twice its weight",
|
|
23
|
+
"A slot is skipped when it is cooling, unavailable, or at its absolute cap (twice its",
|
|
24
|
+
"weight). When every usable slot is at its absolute cap, overflow goes to the least",
|
|
25
|
+
"loaded usable slot; the pool never refuses work.",
|
|
26
|
+
"Live claims decide the next slot: a released claim immediately reopens its slot, and",
|
|
27
|
+
"there is no stored position.",
|
|
28
28
|
"",
|
|
29
|
-
"With this file:
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"Equal weights give a plain round robin in list order. 2,2,1,1 reproduces the old",
|
|
33
|
-
"primary/overflow cycle exactly, which is why the old tier values still work.",
|
|
29
|
+
"With this file: weights 3,2,1,1 give 7 active sessions in pass 1 and 14 total across",
|
|
30
|
+
"both passes.",
|
|
31
|
+
"Equal weights give a plain pass in list order.",
|
|
34
32
|
"",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"land on the heaviest slot that is LEFT. A heavier slot declared after a lighter one",
|
|
38
|
-
"is unreachable in the way the pool means.",
|
|
33
|
+
"DECLARATION ORDER IS PREFERENCE. Slots are filled in declaration order, so the slot",
|
|
34
|
+
"you want preferred comes first; weight sets its per-pass budget, not its priority.",
|
|
39
35
|
"",
|
|
40
36
|
"EVERY SLOT IS A MODEL THAT IS PROBABLY STILL THERE NEXT WEEK, AND NO TWO SLOTS",
|
|
41
|
-
"SHARE A FAMILY. A free model can be withdrawn by its provider without warning
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"pool where every slot is one family is one withdrawal away from being empty. If a",
|
|
47
|
-
"slot does go away, the plugin warns at startup and the rest keep working; the fix",
|
|
48
|
-
"is a one-line edit here, not a redesign.",
|
|
49
|
-
"",
|
|
50
|
-
"Round-robin rather than heaviest-first because re-prioritising the heaviest slot on",
|
|
51
|
-
"EVERY spawn drained the fastest provider's free-tier window early: one provider",
|
|
52
|
-
"absorbed all work up to its ceiling and kept being refilled the moment a claim",
|
|
53
|
-
"completed, while the other slots sat idle -- until the fast provider hit its limit and",
|
|
54
|
-
"its 429s displaced every agent type at once. Every slot a turn per cycle puts a fast",
|
|
55
|
-
"slot with a large usage window to work early in the cycle while it is still fast, and",
|
|
56
|
-
"no provider's quota sits unused while another's is drained.",
|
|
37
|
+
"SHARE A FAMILY. A free model can be withdrawn by its provider without warning, and",
|
|
38
|
+
"a pool where every slot is one family is one withdrawal away from being empty. The",
|
|
39
|
+
"slots here are long-lived free ones spread over two providers and four model",
|
|
40
|
+
"families. If a slot does go away, the plugin warns at startup and the rest keep",
|
|
41
|
+
"working; the fix is a one-line edit here, not a redesign.",
|
|
57
42
|
"",
|
|
58
43
|
"Absent means weight 1. Must be a number in (0, 100]; anything else warns and uses 1.",
|
|
59
|
-
"List order sets
|
|
60
|
-
"
|
|
61
|
-
"top of the list.",
|
|
44
|
+
"List order sets preference: earlier slots are preferred first, so put the slots you",
|
|
45
|
+
"want worked first at the top of the list.",
|
|
62
46
|
"",
|
|
63
47
|
"COST IS THE DEFAULT, NOT A CONSTRAINT. These four slots are free because that is what",
|
|
64
48
|
"the pool is FOR: mechanical work -- reading files, grepping, mechanical edits, cartography.",
|
package/sounds/done.wav
CHANGED
|
Binary file
|
package/sounds/error.wav
CHANGED
|
Binary file
|
package/sounds/permission.wav
CHANGED
|
Binary file
|
package/sounds/question.wav
CHANGED
|
Binary file
|