@aron98/load-lock 0.1.2 → 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/README.md +37 -12
- package/demo/demo.sh +1 -1
- package/dist/adapters/opencode/plugin.js +39 -18
- package/dist/adapters/opencode/tui.d.ts +31 -0
- package/dist/adapters/opencode/tui.js +268 -0
- package/dist/cli/hook.js +35 -1
- package/dist/cli/install.js +708 -24
- package/dist/cli/main.js +36 -61
- package/dist/cli/top.d.ts +7 -0
- package/dist/cli/top.js +98 -0
- package/dist/core/daemon.d.ts +25 -4
- package/dist/core/daemon.js +67 -30
- package/dist/core/persist.js +5 -12
- package/dist/core/state.js +7 -0
- package/dist/dashboard/feed.d.ts +74 -0
- package/dist/dashboard/feed.js +256 -0
- package/dist/dashboard/frame.d.ts +12 -0
- package/dist/dashboard/frame.js +94 -0
- package/dist/dashboard/view.d.ts +148 -0
- package/dist/dashboard/view.js +214 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +54 -19
- package/dist/policy/text.d.ts +1 -1
- package/dist/policy/text.js +3 -0
- package/dist/shared/client.d.ts +10 -0
- package/dist/shared/client.js +41 -14
- package/dist/shared/protocol.d.ts +29 -0
- package/dist/shared/transport.d.ts +40 -0
- package/dist/shared/transport.js +136 -0
- package/dist/shared/wait-turn.d.ts +29 -0
- package/dist/shared/wait-turn.js +315 -0
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +405 -0
- package/dist/test/hook.test.js +53 -3
- package/dist/test/install.test.d.ts +1 -0
- package/dist/test/install.test.js +658 -0
- package/dist/test/mcp.test.js +36 -1
- package/dist/test/platform.test.d.ts +1 -0
- package/dist/test/platform.test.js +193 -0
- package/dist/test/testutil.js +4 -1
- package/dist/test/wait-turn.test.d.ts +1 -0
- package/dist/test/wait-turn.test.js +376 -0
- package/docs/ADAPTERS.md +5 -2
- package/docs/AGENT_GUIDE.md +33 -1
- package/docs/ARCHITECTURE.md +25 -5
- package/docs/COMPATIBILITY.md +215 -26
- package/docs/OPERATIONS.md +313 -8
- package/package.json +25 -1
- package/skills/load-lock/SKILL.md +4 -0
package/docs/OPERATIONS.md
CHANGED
|
@@ -3,19 +3,35 @@
|
|
|
3
3
|
## Install
|
|
4
4
|
|
|
5
5
|
Requirements: Node 20+, Linux (macOS works with the limitations noted below;
|
|
6
|
-
Windows
|
|
6
|
+
Windows has a named-pipe transport adapter — implemented and unit-tested on
|
|
7
|
+
Linux via platform injection, not yet verified on a Windows machine; see the
|
|
8
|
+
Windows section at the bottom).
|
|
7
9
|
|
|
8
10
|
```sh
|
|
9
11
|
npm install -g @aron98/load-lock # or: npm install -g ./aron98-load-lock-<ver>.tgz
|
|
10
12
|
load-lock install --harness opencode
|
|
11
13
|
load-lock install --harness claude-code
|
|
12
14
|
load-lock install --harness codex
|
|
15
|
+
load-lock install --harness gemini-cli
|
|
16
|
+
load-lock install --harness copilot-cli
|
|
17
|
+
load-lock install --harness cline
|
|
18
|
+
load-lock install --harness roo-code
|
|
19
|
+
load-lock install --harness goose
|
|
20
|
+
load-lock install --harness cursor --project /path/to/repo
|
|
13
21
|
load-lock install --harness generic # prints MCP snippets for other harnesses
|
|
14
22
|
```
|
|
15
23
|
|
|
16
24
|
What each installer does (all merges are non-destructive; uninstall removes
|
|
17
25
|
exactly the entries it added):
|
|
18
26
|
|
|
27
|
+
Hook-bearing installers (claude-code, codex, cursor) recognize their own
|
|
28
|
+
hook entries by the JSON-quoted CLI path embedded in the hook command, so
|
|
29
|
+
re-running an installer is idempotent and uninstall removes exactly our
|
|
30
|
+
entries, leaving user hooks alone. This is contract-tested via temp-HOME
|
|
31
|
+
round-trips in `src/test/install.test.ts` for all three (an earlier matcher
|
|
32
|
+
never matched its own entries — duplicate installs and hooks that survived
|
|
33
|
+
uninstall — see issue #19); not live-verified inside the real CLIs.
|
|
34
|
+
|
|
19
35
|
- **opencode** — writes a plugin stub to
|
|
20
36
|
`~/.config/opencode/plugins/load-lock/` (npm-installed against the global
|
|
21
37
|
package), registers it in `~/.config/opencode/opencode.json`.
|
|
@@ -27,15 +43,52 @@ exactly the entries it added):
|
|
|
27
43
|
`Stop`), appends `[mcp_servers.load-lock]` to `~/.codex/config.toml` if
|
|
28
44
|
absent, adds a marked section to `~/.codex/AGENTS.md`. **Then run `/hooks`
|
|
29
45
|
inside Codex once to trust the hooks.**
|
|
30
|
-
- **
|
|
31
|
-
|
|
46
|
+
- **gemini-cli** — writes `mcpServers.load-lock` (stdio: `node` + absolute
|
|
47
|
+
path to `dist/mcp/main.js`) into `~/.gemini/settings.json`, adds a marked
|
|
48
|
+
section to `~/.gemini/GEMINI.md`. MCP-fallback tier: Gemini CLI has no
|
|
49
|
+
hooks and no async push, so there is no idle wakeup — offers surface at
|
|
50
|
+
tool boundaries.
|
|
51
|
+
- **copilot-cli** — merges `mcpServers.load-lock` (`type: local`) into
|
|
52
|
+
`~/.copilot/mcp-config.json` (verify with `copilot mcp list`) and adds a
|
|
53
|
+
marked section to `~/.copilot/copilot-instructions.md`. MCP-fallback
|
|
54
|
+
tier: hooks are reported in ecosystem content but not verified in
|
|
55
|
+
official docs, so none are installed — no idle wakeup is claimed; offers
|
|
56
|
+
surface at tool boundaries.
|
|
57
|
+
- **cline** — merges `mcpServers.load-lock` into
|
|
58
|
+
`~/.cline/data/settings/cline_mcp_settings.json` (or the path in
|
|
59
|
+
`CLINE_MCP_SETTINGS_PATH`) and writes a dedicated
|
|
60
|
+
`~/.cline/rules/load-lock.md`. MCP-fallback tier: no hooks, no async
|
|
61
|
+
push — no idle wakeup.
|
|
62
|
+
- **roo-code** — merges `mcpServers.load-lock` into `<project>/.roo/mcp.json`
|
|
63
|
+
(the CLI's cwd unless `--project` is passed) and into every **existing**
|
|
64
|
+
global `mcp_settings.json` (VS Code globalStorage; existing files only —
|
|
65
|
+
never created), plus a dedicated `~/.roo/rules/load-lock.md`. MCP-fallback
|
|
66
|
+
tier: no hooks, no async push — no idle wakeup.
|
|
67
|
+
- **goose** — merges an `extensions.load-lock` stdio block (marker-tagged
|
|
68
|
+
block-mapping YAML; refuses flow-style `extensions:`) into
|
|
69
|
+
`~/.config/goose/config.yaml` (XDG_CONFIG_HOME respected) and adds a
|
|
70
|
+
marked section to `~/.config/goose/.goosehints`. MCP-fallback tier: no
|
|
71
|
+
hooks, no async push — no idle wakeup.
|
|
72
|
+
- **cursor** — project-level (pass `--project`, defaults to cwd): merges
|
|
73
|
+
`mcpServers.load-lock` into `<project>/.cursor/mcp.json`, merges a flat
|
|
74
|
+
hooks-v1 stop entry into `<project>/.cursor/hooks.json` (`version` added
|
|
75
|
+
only if absent; user entries preserved), writes a dedicated Always-apply
|
|
76
|
+
rule `<project>/.cursor/rules/load-lock.mdc`. Hooks tier: the stop hook
|
|
77
|
+
returns `{"followup_message":...}` so Cursor continues the session when an
|
|
78
|
+
offer arrives. Contract-tested against the documented hooks JSON
|
|
79
|
+
(https://cursor.com/docs/hooks); not live-verified.
|
|
80
|
+
- **generic** — prints MCP stdio config snippets (harnesses without a
|
|
81
|
+
dedicated installer) plus a suggested
|
|
82
|
+
instructions-file section.
|
|
32
83
|
|
|
33
84
|
`load-lock uninstall --harness <harness>` reverses any of these.
|
|
34
85
|
|
|
35
86
|
## The daemon
|
|
36
87
|
|
|
37
|
-
- State dir: `~/.local/state/load-lock` (
|
|
38
|
-
|
|
88
|
+
- State dir: `~/.local/state/load-lock` (Windows: `%LOCALAPPDATA%\load-lock`;
|
|
89
|
+
override `LOAD_LOCK_STATE_DIR`).
|
|
90
|
+
- Socket: `<state dir>/daemon.sock` (Windows: named pipe `\\.\pipe\load-lock`;
|
|
91
|
+
override `LOAD_LOCK_SOCKET`).
|
|
39
92
|
- **Auto-spawns** on first client connection; no service unit needed.
|
|
40
93
|
- Files: `config.json` (settings + history), `journal.json` (held permits),
|
|
41
94
|
`daemon.json` (pid), `spawn.lock` (bootstrap race guard).
|
|
@@ -61,8 +114,9 @@ load-lock get-config
|
|
|
61
114
|
```
|
|
62
115
|
|
|
63
116
|
Defaults: one pool `heavy` capacity 1 · offer window 30s · blocking wait 5s ·
|
|
117
|
+
turn-wait cap (`maxTurnWaitMs`, bounds `wait_for_turn`/`load-lock wait`) 10min ·
|
|
64
118
|
start deadline 120s · untracked lease 15min. Valid ranges:
|
|
65
|
-
offer window 1s–10min, wait 0–10min, start deadline 1s–1h, lease 10s–24h.
|
|
119
|
+
offer window 1s–10min, wait 0–10min, turn-wait cap 1s–1h, start deadline 1s–1h, lease 10s–24h.
|
|
66
120
|
|
|
67
121
|
Semantics of changes:
|
|
68
122
|
|
|
@@ -76,6 +130,64 @@ limits (containers respected — we never assume 2 cores means 2 jobs),
|
|
|
76
130
|
available memory, and current load/memory pressure with an advisory
|
|
77
131
|
suggestion. Agents are expected to consult it before proposing capacity.
|
|
78
132
|
|
|
133
|
+
## Dashboard: watching the budget live
|
|
134
|
+
|
|
135
|
+
Two read-only viewers surface the machine-wide state in real time. Both use
|
|
136
|
+
the library's own client + protocol (`./shared/client`, `./shared/protocol`)
|
|
137
|
+
over the daemon's watch event stream — neither scrapes CLI output — and
|
|
138
|
+
both resume cleanly across daemon restarts (event-cursor `events since` +
|
|
139
|
+
journal recovery).
|
|
140
|
+
|
|
141
|
+
**The viewers never acquire a permit.** Watching the lock is light work by
|
|
142
|
+
construction: they only ever call `status` / `events` / `inspect` / `watch`.
|
|
143
|
+
The one admin action they expose — force-release — sits behind an explicit
|
|
144
|
+
select + confirm step and never runs automatically.
|
|
145
|
+
|
|
146
|
+
### `load-lock top` (portable, any terminal)
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
load-lock top # full-screen live dashboard (q quit, r resync)
|
|
150
|
+
load-lock top --once # render one frame and exit (pipes, scripts, CI logs)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Shows pools with capacity vs in-use, holder cards (label, reason, phase,
|
|
154
|
+
pid/pgid, age, start-deadline or lease countdown), offer cards with the
|
|
155
|
+
claim-window countdown, FIFO queue positions, paused requests, the recent
|
|
156
|
+
event feed, and the `inspect_machine` panel. It reconnects with backoff if
|
|
157
|
+
the daemon dies, and a daemon restart is announced and re-synced (durable
|
|
158
|
+
event ids are per daemon lifetime; the cursor resets on restart).
|
|
159
|
+
|
|
160
|
+
Zero extra dependencies: it renders plain text lines over the standard
|
|
161
|
+
alternate screen, so it works anywhere the CLI works.
|
|
162
|
+
|
|
163
|
+
### OpenCode TUI panel
|
|
164
|
+
|
|
165
|
+
The OpenCode installer also ships the package's `./tui` plugin export (a
|
|
166
|
+
companion to the main plugin, which is unchanged). Inside the opencode TUI:
|
|
167
|
+
|
|
168
|
+
- `/load-lock` (or the command palette) opens the **load-lock.dashboard**
|
|
169
|
+
session panel full-screen: same data as `load-lock top`, themed,
|
|
170
|
+
live-updating.
|
|
171
|
+
- The home footer gains a small `lock 1/1·Q2` indicator.
|
|
172
|
+
- `r` resyncs; `x` opens force-release, which requires an explicit
|
|
173
|
+
select + confirm — the panel itself is read-only.
|
|
174
|
+
|
|
175
|
+
Verified live against opencode 2.0.29: the panel renders pools, holder
|
|
176
|
+
cards, queue positions and the event feed against the real daemon, and the
|
|
177
|
+
`x` flow refuses to release anything until the confirm dialog accepts.
|
|
178
|
+
Plugins configured in `opencode.json` that expose `./tui` are loaded
|
|
179
|
+
automatically by the CLI, so the published package needs no extra config.
|
|
180
|
+
For local development against a checkout, opencode also auto-discovers
|
|
181
|
+
plugins laid out like a package under `<project>/.opencode/plugins/<name>/`
|
|
182
|
+
(`index.ts` re-exporting the main plugin, `tui.ts` re-exporting
|
|
183
|
+
`./dist/adapters/opencode/tui.js`).
|
|
184
|
+
|
|
185
|
+
### Notes for other harnesses
|
|
186
|
+
|
|
187
|
+
`load-lock top` is the dashboard for every harness (it only needs the
|
|
188
|
+
daemon, which every integration spawns). Harnesses without an in-app UI
|
|
189
|
+
surface get exactly that — no parity claims; see `docs/COMPATIBILITY.md`.
|
|
190
|
+
|
|
79
191
|
## Diagnostics & recovery
|
|
80
192
|
|
|
81
193
|
```sh
|
|
@@ -117,10 +229,203 @@ Recovery policy summary:
|
|
|
117
229
|
| Config change lost | Configuration is CAS-versioned: fetch `get_config`, retry with the fresh `version` on `version_conflict`. |
|
|
118
230
|
| After OS reboot | Nothing to do: state dir persists config; daemon respawns on first use; journal reconciliation releases permits whose processes died. |
|
|
119
231
|
|
|
232
|
+
## Containers / WSL2
|
|
233
|
+
|
|
234
|
+
### Docker — verified
|
|
235
|
+
|
|
236
|
+
Verified 2026-10-16 with Docker Engine 29.1.3 on Linux (amd64) and
|
|
237
|
+
`node:24-bookworm-slim`, running load-lock 0.1.2 installed from the packed
|
|
238
|
+
tarball (`npm pack`, then `npm install -g --omit=dev <tgz>` inside the
|
|
239
|
+
container). Two runs, both in containers started with `--cpus=1.5
|
|
240
|
+
--memory=2g`:
|
|
241
|
+
|
|
242
|
+
- a sanity-check container (`load-lock doctor`, `load-lock inspect`,
|
|
243
|
+
auto-spawn proof, `attach-pid` round trip), and
|
|
244
|
+
- the full `demo/demo.sh` (which needs bash + git + node; the slim image
|
|
245
|
+
needs `apt-get install -y git` first).
|
|
246
|
+
|
|
247
|
+
**Cgroup v2 quota detection.** `load-lock inspect` inside the constrained
|
|
248
|
+
container reported the *container* limits, not the host's:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
"cpu": { "logicalCores": 2, "cgroupVersion": "v2", "cgroupCpuQuota": 150000,
|
|
252
|
+
"cgroupCpuPeriod": 100000, "effectiveCores": 1 },
|
|
253
|
+
"memory": { "totalBytes": 8195100672, "cgroupMemoryLimitBytes": 2147483648,
|
|
254
|
+
"effectiveLimitBytes": 2147483648 }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`/sys/fs/cgroup/cpu.max` reads `150000 100000` inside the container; the 2
|
|
258
|
+
logical cores are still visible (`os.cpus()`/`nproc` are not namespaced),
|
|
259
|
+
but `effectiveCores` is 1 (floor of the 1.5-CPU quota) and
|
|
260
|
+
`effectiveLimitBytes` is exactly the 2 GiB `--memory=2g` limit while
|
|
261
|
+
`totalBytes` shows the ~8 GiB host view. Pressure and the inspect
|
|
262
|
+
suggestion therefore scale to the container, not the host — exactly the
|
|
263
|
+
"don't assume the host's cores" behavior the inspect output promises.
|
|
264
|
+
|
|
265
|
+
**Auto-spawn + default state dir.** With no `LOAD_LOCK_*` environment
|
|
266
|
+
variables set, the first CLI call auto-spawned the daemon on the
|
|
267
|
+
container-default `~/.local/state/load-lock` (state dir contained
|
|
268
|
+
`daemon.json`, `daemon.sock`, `journal.json`; `service status` reported
|
|
269
|
+
`pid 35, protocol v1, config version 1`). No special env or volumes
|
|
270
|
+
needed.
|
|
271
|
+
|
|
272
|
+
**Process tracking.** `/proc`-based tracking works unchanged in the
|
|
273
|
+
container: `attach-pid` recorded the target with its
|
|
274
|
+
`/proc/<pid>/stat` start time (`pidStartTime`), `recover list` reported
|
|
275
|
+
`tracked process (pid 3367) is running`, and after the process was killed
|
|
276
|
+
the daemon's 5s sweep auto-released the permit (`pool "heavy": 0/1 in
|
|
277
|
+
use`). `run_with_permit` and journal reconciliation use the same mechanism.
|
|
278
|
+
|
|
279
|
+
**demo.sh.** The scripted three-agent demo ran to completion (exit 0)
|
|
280
|
+
inside the same plain node:24 container. Its inventory line reflected
|
|
281
|
+
container limits — `cpu: 2 logical / 1 effective (v2)`, `memory: 7.6 GiB
|
|
282
|
+
total / 2.0 GiB effective limit` — and the full flow played out: A
|
|
283
|
+
granted, B and C queued, strict FIFO hand-off, C's offer expired →
|
|
284
|
+
`paused` (not recycled), explicit `rejoin` → claim → run → release; 21
|
|
285
|
+
durable events.
|
|
286
|
+
|
|
287
|
+
Limitations observed (all inherent to containers, not load-lock bugs):
|
|
288
|
+
|
|
289
|
+
- `/proc/loadavg` and CPU inventory are not namespaced: `loadPerEffectiveCore`
|
|
290
|
+
reflects host load, so a quiet container on a busy host can still show
|
|
291
|
+
"high" load pressure, and `nproc` shows the host CPU set. The cgroup quota
|
|
292
|
+
(`effectiveCores`) is the honest number to reason with.
|
|
293
|
+
- State dir is per-container: with `docker run --rm` the daemon, config
|
|
294
|
+
history and journal vanish with the container; the next use just
|
|
295
|
+
respawns with defaults (fine for ephemeral CI containers; volume-mount
|
|
296
|
+
`~/.local/state/load-lock` if you want persistence).
|
|
297
|
+
|
|
298
|
+
Reproduce from a checkout (needs `dist/` built; tarball from `npm pack`):
|
|
299
|
+
|
|
300
|
+
```sh
|
|
301
|
+
npm ci && npm run build && npm pack --pack-destination /tmp
|
|
302
|
+
docker run --rm --cpus=1.5 --memory=2g \
|
|
303
|
+
-v "$PWD":/src:ro -v /tmp:/pkg:ro \
|
|
304
|
+
node:24 bash -c '
|
|
305
|
+
cp -a /src /work && cd /work
|
|
306
|
+
apt-get update -qq && apt-get install -y -qq git
|
|
307
|
+
npm install -g --omit=dev /pkg/aron98-load-lock-<version>.tgz
|
|
308
|
+
load-lock doctor && load-lock inspect
|
|
309
|
+
bash demo/demo.sh'
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### WSL2 — procedure (NOT verified here)
|
|
313
|
+
|
|
314
|
+
**No WSL2 environment was available on the verification machine** (the
|
|
315
|
+
Docker results above were produced on a plain Linux VM). Nothing in this
|
|
316
|
+
subsection has been executed by us; it is a ready-to-run procedure with
|
|
317
|
+
*expected* outcomes, to be verified and recorded by someone with a WSL2
|
|
318
|
+
setup.
|
|
319
|
+
|
|
320
|
+
1. Use a WSL2 distro with Node 20+ (e.g. Ubuntu 24.04). Check the cgroup
|
|
321
|
+
layout inside the distro:
|
|
322
|
+
|
|
323
|
+
```sh
|
|
324
|
+
stat -fc %T /sys/fs/cgroup # expect cgroup2fs (cgroup v2)
|
|
325
|
+
cat /sys/fs/cgroup/cpu.max /sys/fs/cgroup/memory.max
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
2. Run load-lock on the distro itself (not in Docker):
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
load-lock doctor && load-lock inspect
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Expected: WSL2 presents a full VM, so `inspect` reports the *WSL VM's*
|
|
335
|
+
resources, not the Windows host's. `.wslconfig` caps (`memory=`,
|
|
336
|
+
`processors=`) are enforced by the VM itself (vCPU count / VM RAM), not
|
|
337
|
+
via cgroup quotas — so `cgroupVersion` should read `v2` on a
|
|
338
|
+
systemd-enabled distro, `cgroupCpuQuota` typically `null` (`max`), and
|
|
339
|
+
`effectiveCores` = the `.wslconfig` `processors=` value with
|
|
340
|
+
`memory.totalBytes`/`effectiveLimitBytes` ≈ the `memory=` cap. If the
|
|
341
|
+
distro runs without systemd the cgroup controllers may not be mounted;
|
|
342
|
+
expect `cgroupVersion` to degrade to `none` and the raw VM numbers to be
|
|
343
|
+
reported — either way, no host overstating.
|
|
344
|
+
|
|
345
|
+
3. With Docker inside WSL2 (docker-ce in the distro, or Docker Desktop's
|
|
346
|
+
WSL2 backend), re-run the constrained-container check from the verified
|
|
347
|
+
Docker procedure above. Expected: containers get their own nested
|
|
348
|
+
cgroup, so `inspect` should report the container quota exactly as in the
|
|
349
|
+
verified results above, regardless of the extra nesting level.
|
|
350
|
+
|
|
351
|
+
4. Run `demo/demo.sh` in the distro (needs bash, git, node) and in a plain
|
|
352
|
+
`node:24` container under WSL2's Docker.
|
|
353
|
+
|
|
354
|
+
Caveats to watch for (unverified expectations):
|
|
355
|
+
|
|
356
|
+
- Nested cgroups (Docker Desktop's WSL2 backend adds a level) must not mask
|
|
357
|
+
the container's own `cpu.max`/`memory.max` — that's what `inspect` reads.
|
|
358
|
+
- `/proc/loadavg` reflects the WSL VM, not Windows.
|
|
359
|
+
- Keep the state dir on the Linux filesystem (`~/.local/state/load-lock`),
|
|
360
|
+
not on `/mnt/c` (drvfs/9p is slow and lock semantics differ).
|
|
361
|
+
|
|
120
362
|
## macOS / Windows notes
|
|
121
363
|
|
|
122
364
|
- macOS: no cgroup limits; process-liveness checks fall back from
|
|
123
365
|
`/proc/<pid>/stat` start times to `kill(pid, 0)` (weaker PID-reuse
|
|
124
366
|
defense — document and prefer `run_with_permit`). Everything else works.
|
|
125
|
-
|
|
126
|
-
|
|
367
|
+
|
|
368
|
+
### Windows — procedure (NOT verified here)
|
|
369
|
+
|
|
370
|
+
**No Windows environment was available on the verification machine** (all
|
|
371
|
+
development and testing happened on Linux). The transport adapter is
|
|
372
|
+
implemented in `src/shared/transport.ts` and its logic is covered by
|
|
373
|
+
`src/test/platform.test.ts` (endpoint resolution, pipe naming, probe,
|
|
374
|
+
stale-cleanup, liveness, and job-tree kill paths, exercised via platform
|
|
375
|
+
injection on Linux) — but nothing in this subsection has been executed on
|
|
376
|
+
Windows. It is a ready-to-run procedure with *expected* outcomes, to be
|
|
377
|
+
verified and recorded by someone with a Windows setup.
|
|
378
|
+
|
|
379
|
+
What the adapter does:
|
|
380
|
+
|
|
381
|
+
- Transport: clients and the daemon talk over the named pipe
|
|
382
|
+
`\\.\pipe\load-lock` (Node's `net` module accepts pipe paths in
|
|
383
|
+
`connect`/`createServer` natively). No Unix-domain socket is involved.
|
|
384
|
+
- State dir: `%LOCALAPPDATA%\load-lock` (falls back to
|
|
385
|
+
`%USERPROFILE%\AppData\Local\load-lock`); `LOAD_LOCK_STATE_DIR` and
|
|
386
|
+
`LOAD_LOCK_SOCKET` overrides still win, as on POSIX.
|
|
387
|
+
- Daemon claim: a second daemon probes the pipe before listening and
|
|
388
|
+
exits with "denied" if a live daemon answers (same semantics as the
|
|
389
|
+
POSIX socket claim). Because a named pipe is a kernel object, there is
|
|
390
|
+
no stale-socket file to clean up.
|
|
391
|
+
|
|
392
|
+
1. Install Node 20+ on Windows and install the package:
|
|
393
|
+
|
|
394
|
+
```powershell
|
|
395
|
+
npm install -g @aron98/load-lock
|
|
396
|
+
load-lock install --harness <harness>
|
|
397
|
+
load-lock doctor
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Expected: `doctor` reports the `%LOCALAPPDATA%\load-lock` state dir and
|
|
401
|
+
reaches the daemon over the pipe (it probes by connecting, since a pipe
|
|
402
|
+
is not a filesystem entry).
|
|
403
|
+
|
|
404
|
+
2. Run two `load-lock acquire` in separate shells, then `load-lock top --once`.
|
|
405
|
+
|
|
406
|
+
Expected: the same queue/permit behavior as on Linux — the pipe carries
|
|
407
|
+
the identical NDJSON protocol.
|
|
408
|
+
|
|
409
|
+
3. Start a permit-tracked job (`run_with_permit` / `load-lock run`), kill
|
|
410
|
+
the job, and confirm the permit auto-releases via the sweep.
|
|
411
|
+
|
|
412
|
+
4. Run `load-lock service stop`, then any tool call.
|
|
413
|
+
|
|
414
|
+
Expected: the daemon exits and the next client auto-spawns a fresh one.
|
|
415
|
+
|
|
416
|
+
Caveats to watch for (unverified expectations):
|
|
417
|
+
|
|
418
|
+
- **No PID-reuse defense.** Windows has no `/proc`-style start time, so
|
|
419
|
+
`isProcessAlive` can only use `kill(pid, 0)` semantics. A recycled PID
|
|
420
|
+
could be mistaken for the original job — the same limitation as macOS,
|
|
421
|
+
documented here rather than solved.
|
|
422
|
+
- **No process-group scan.** The `/proc`-based process-group scan does not
|
|
423
|
+
exist on Windows. Tracked permits release when the main PID dies even if
|
|
424
|
+
child processes survive; job stop/kill uses `taskkill /PID <pid> /T`
|
|
425
|
+
(`/F` for SIGKILL) to take down the tree.
|
|
426
|
+
- **Pipe name is machine-wide.** Unlike POSIX, where each user gets their
|
|
427
|
+
own state dir and socket, a named pipe named `\\.\pipe\load-lock` is
|
|
428
|
+
machine-global — two Windows users on one host share one daemon. Point
|
|
429
|
+
`LOAD_LOCK_SOCKET` at a different pipe name if that is a problem.
|
|
430
|
+
- `inspect_machine` cgroup detection reads `/sys/fs/cgroup` and will
|
|
431
|
+
degrade gracefully (`cgroupVersion: "none"`), reporting raw host numbers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aron98/load-lock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Machine-wide counting semaphore coordinating resource-intensive work across parallel coding agents (OpenCode, Codex CLI, Claude Code, ...)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"types": "./dist/core/daemon.d.ts",
|
|
73
73
|
"default": "./dist/core/daemon.js"
|
|
74
74
|
},
|
|
75
|
+
"./tui": {
|
|
76
|
+
"types": "./dist/adapters/opencode/tui.d.ts",
|
|
77
|
+
"default": "./dist/adapters/opencode/tui.js"
|
|
78
|
+
},
|
|
75
79
|
"./dist/*": "./dist/*"
|
|
76
80
|
},
|
|
77
81
|
"files": [
|
|
@@ -97,8 +101,28 @@
|
|
|
97
101
|
"@opencode/plugin": "^2.0.29",
|
|
98
102
|
"zod": "^3.24.0"
|
|
99
103
|
},
|
|
104
|
+
"peerDependencies": {
|
|
105
|
+
"@opentui/core": ">=0.5.8",
|
|
106
|
+
"@opentui/solid": ">=0.5.8",
|
|
107
|
+
"solid-js": ">=1.9.0"
|
|
108
|
+
},
|
|
109
|
+
"peerDependenciesMeta": {
|
|
110
|
+
"@opentui/core": {
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
113
|
+
"@opentui/solid": {
|
|
114
|
+
"optional": true
|
|
115
|
+
},
|
|
116
|
+
"solid-js": {
|
|
117
|
+
"optional": true
|
|
118
|
+
}
|
|
119
|
+
},
|
|
100
120
|
"devDependencies": {
|
|
121
|
+
"@opencode/theme": "2.0.29",
|
|
122
|
+
"@opentui/core": "0.5.17",
|
|
123
|
+
"@opentui/solid": "0.5.17",
|
|
101
124
|
"@types/node": "^24.0.0",
|
|
125
|
+
"solid-js": "1.9.12",
|
|
102
126
|
"typescript": "^5.9.0"
|
|
103
127
|
}
|
|
104
128
|
}
|
|
@@ -23,6 +23,10 @@ Judge by *expected* CPU/memory/IO impact, not by command name. When unsure,
|
|
|
23
23
|
`granted` (you hold a permit; start heavy work now) or `queued`
|
|
24
24
|
(keep doing light work; an offer will arrive).
|
|
25
25
|
2. While queued: **continue useful light work**. Do not idle-poll.
|
|
26
|
+
Out of light work? `wait_for_turn(request_id?, timeout_ms?)` — a
|
|
27
|
+
bounded blocking sleep until your turn that auto-claims the offer and
|
|
28
|
+
wakes you with the permit_token (holds no capacity; cancellation
|
|
29
|
+
leaves nothing behind).
|
|
26
30
|
3. When an offer arrives (harness wakeup or `pending`): `claim(offer_token)`
|
|
27
31
|
inside the claim window (default 30s). Expired offers are rejected; your
|
|
28
32
|
request pauses until you `rejoin`.
|