@aron98/load-lock 1.2.0 → 1.3.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 +3 -2
- package/dist/adapters/opencode/tui.js +5 -1
- package/dist/cli/copies.d.ts +28 -0
- package/dist/cli/copies.js +207 -0
- package/dist/cli/install.js +218 -1
- package/dist/cli/main.js +264 -16
- package/dist/core/daemon.d.ts +18 -0
- package/dist/core/daemon.js +84 -3
- package/dist/core/persist.js +5 -3
- package/dist/core/state.d.ts +7 -4
- package/dist/core/state.js +64 -10
- package/dist/dashboard/frame.js +5 -1
- package/dist/dashboard/view.d.ts +4 -0
- package/dist/dashboard/view.js +5 -0
- package/dist/shared/client.js +4 -1
- package/dist/shared/protocol.d.ts +40 -2
- package/dist/shared/protocol.js +5 -0
- package/dist/shared/version.d.ts +10 -0
- package/dist/shared/version.js +57 -0
- package/dist/test/install.test.js +181 -1
- package/dist/test/update.test.d.ts +1 -0
- package/dist/test/update.test.js +455 -0
- package/docs/ARCHITECTURE.md +10 -6
- package/docs/COMPATIBILITY.md +79 -4
- package/docs/OPERATIONS.md +171 -56
- package/package.json +1 -1
package/docs/OPERATIONS.md
CHANGED
|
@@ -95,15 +95,62 @@ uninstall — see issue #19); not live-verified inside the real CLIs.
|
|
|
95
95
|
[Sandboxes](#sandboxes)).
|
|
96
96
|
- Also honored by the shared client: `LOAD_LOCK_LABEL_PREFIX` (prefix for
|
|
97
97
|
holder labels; see Sandboxes).
|
|
98
|
-
- Files: `config.json` (settings + history), `journal.json` (held permits
|
|
99
|
-
`daemon.json` (pid), `spawn.lock` (bootstrap race guard).
|
|
98
|
+
- Files: `config.json` (settings + history), `journal.json` (held permits +
|
|
99
|
+
queued requests), `daemon.json` (pid), `spawn.lock` (bootstrap race guard).
|
|
100
100
|
|
|
101
101
|
```sh
|
|
102
|
-
load-lock service status # hello + pid
|
|
103
|
-
load-lock service start # ensure running (idempotent)
|
|
104
|
-
load-lock service stop # SIGTERM;
|
|
102
|
+
load-lock service status # hello + pid + daemon version
|
|
103
|
+
load-lock service start # ensure running (idempotent); refusals name the conflicting pid
|
|
104
|
+
load-lock service stop # SIGTERM → bounded wait → SIGKILL; verifies the socket is free
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
`service stop` is honest, not fire-and-forget: it signals the daemon
|
|
108
|
+
(`--timeout <ms>` bounds the wait, default 5s), escalates to SIGKILL if the
|
|
109
|
+
socket still answers, and only reports success once the endpoint is
|
|
110
|
+
verifiably free (stale `daemon.json`/socket files are cleaned up either way).
|
|
111
|
+
`service start` refuses loudly when something else owns the endpoint —
|
|
112
|
+
naming the conflicting pid — instead of a bare "did not come up within 10s".
|
|
113
|
+
|
|
114
|
+
## Update lifecycle
|
|
115
|
+
|
|
116
|
+
The daemon is long-lived, so an update (`npm i -g @aron98/load-lock@latest`,
|
|
117
|
+
or opencode refreshing its plugin copy) leaves the running daemon on stale
|
|
118
|
+
code. The **version handshake** closes that gap (v1.3.0+):
|
|
119
|
+
|
|
120
|
+
- Every client envelope carries `clientVersion`; `hello`/`status` replies
|
|
121
|
+
carry `daemonVersion` (the package version of the code the daemon runs).
|
|
122
|
+
- When a **newer** client contacts an older daemon, the daemon finishes
|
|
123
|
+
answering, then **self-restarts**: it persists state + journal, drains all
|
|
124
|
+
connections, respawns itself with the same argv (`process.execPath` +
|
|
125
|
+
original arguments, so it runs whatever is now on disk at the same path),
|
|
126
|
+
and exits. The respawn inherits a `LOAD_LOCK_RESTARTED_FOR` marker so
|
|
127
|
+
repeated contact with the same version cannot cause a restart loop — one
|
|
128
|
+
respawn attempt per version.
|
|
129
|
+
- Clients that were connected see a disconnect and simply reconnect to the
|
|
130
|
+
fresh daemon.
|
|
131
|
+
- **What survives a restart:** held permits are journaled and reconciled
|
|
132
|
+
conservatively (tracked jobs re-checked for liveness, same permit tokens
|
|
133
|
+
valid; dead jobs released); queued *and offered* requests are journaled and
|
|
134
|
+
re-queued FIFO with the same request ids (idempotent re-acquire keeps
|
|
135
|
+
working). Offered entries restart as queued and are re-offered with fresh
|
|
136
|
+
tokens — pre-restart offer tokens are invalid, exactly as for any restart.
|
|
137
|
+
- Skew is visible before it heals: `load-lock status`, `service status`,
|
|
138
|
+
`doctor`, and the dashboards show "daemon vX, installed vY" and note the
|
|
139
|
+
pending self-restart; `doctor` actively waits a few seconds to confirm the
|
|
140
|
+
daemon came back on the new version.
|
|
141
|
+
|
|
142
|
+
Limits, stated honestly: the respawn re-runs the **same argv** — it picks up
|
|
143
|
+
an updated copy only if the daemon's launching copy was updated *in place*
|
|
144
|
+
(the normal `npm i -g` case). A daemon spawned from a different copy (e.g.
|
|
145
|
+
opencode's private cache copy) keeps running that copy's code after a
|
|
146
|
+
restart; see the multi-copy notes in
|
|
147
|
+
[COMPATIBILITY.md](COMPATIBILITY.md#multiple-copies-of-load-lock-opencode).
|
|
148
|
+
Pre-1.3.0 daemons do not answer the handshake and never self-restart —
|
|
149
|
+
`service stop` followed by any load-lock command moves them onto current
|
|
150
|
+
code. The restart behavior itself is live-verified on Linux by the test
|
|
151
|
+
suite (`src/test/update.test.ts`); the in-place-update path beyond
|
|
152
|
+
same-argv respawn is inspected, not live-tested.
|
|
153
|
+
|
|
107
154
|
## Configuration
|
|
108
155
|
|
|
109
156
|
All configuration is agent-editable through tools (no file editing for
|
|
@@ -607,56 +654,122 @@ Caveats observed (verified):
|
|
|
607
654
|
inside the sandbox and split the budget. The tests in
|
|
608
655
|
`src/test/sandbox.test.ts` pin the loud failure instead.
|
|
609
656
|
|
|
610
|
-
### omac —
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
omac
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
657
|
+
### omac — live-verified
|
|
658
|
+
|
|
659
|
+
Verified 2026-10-20 against **omac 0.11.0** (extracted package binary, no
|
|
660
|
+
root needed) on Linux, driving the load-lock CLI from the v1.2.0 worktree
|
|
661
|
+
build end-to-end from inside `omac sandbox run`. omac wraps bubblewrap +
|
|
662
|
+
Landlock on Linux (seatbelt on macOS with the same env-stripping) and
|
|
663
|
+
**strips the harness environment**, allowing through only variables
|
|
664
|
+
listed in the user-global sandbox profile —
|
|
665
|
+
`~/.config/omac/sandbox-profiles/default.json`. The profile is the whole
|
|
666
|
+
recipe, and `load-lock install --harness omac` writes it for you
|
|
667
|
+
(merge-only; idempotent; `load-lock uninstall --harness omac` removes
|
|
668
|
+
exactly the entries it added):
|
|
669
|
+
|
|
670
|
+
```json
|
|
671
|
+
{
|
|
672
|
+
"filesystem": {
|
|
673
|
+
"allow_unix_dir": ["<load-lock state dir>"],
|
|
674
|
+
"read": ["<installed package tree>"]
|
|
675
|
+
},
|
|
676
|
+
"environment": { "allow_vars": ["LOAD_LOCK_*"] }
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
- `filesystem.allow_unix_dir` += the state dir: AF_UNIX connect to the
|
|
681
|
+
host daemon socket plus read/write on the state dir — this is the
|
|
682
|
+
sandbox→host bridge (omac has no separate "bind" step; the grant is the
|
|
683
|
+
bridge).
|
|
684
|
+
- `environment.allow_vars` += `"LOAD_LOCK_*"` — one trailing-`*` prefix
|
|
685
|
+
entry (verified: omac 0.11.0 accepts prefix patterns) covering all four
|
|
686
|
+
client vars (`LOAD_LOCK_SOCKET`, `LOAD_LOCK_STATE_DIR`,
|
|
687
|
+
`LOAD_LOCK_NO_SPAWN`, `LOAD_LOCK_LABEL_PREFIX`).
|
|
688
|
+
- `filesystem.read` += the installed package tree — **only when
|
|
689
|
+
load-lock lives outside an already-granted workdir** (see gotcha 1).
|
|
690
|
+
|
|
691
|
+
The exact procedure that was verified (run it with a throwaway `HOME`
|
|
692
|
+
unless you want the grants in your real profile):
|
|
693
|
+
|
|
694
|
+
```sh
|
|
695
|
+
# 0. scratch dirs + a current daemon built from the v1.2.0+ tree.
|
|
696
|
+
# GOTCHA 2: the machine's real daemon was still v1.1.0 — a daemon
|
|
697
|
+
# started before v1.2.0 silently strips the unknown labelPrefix field
|
|
698
|
+
# (zod), so prefixed labels disappear without any error. Upgrade and
|
|
699
|
+
# restart: load-lock service stop && load-lock service start. This
|
|
700
|
+
# verification deliberately
|
|
701
|
+
# used its own daemon on a temp state dir.
|
|
702
|
+
LOAD_LOCK_STATE_DIR=/tmp/ll-32-test node dist/cli/main.js service start
|
|
703
|
+
|
|
704
|
+
# 1. install the grants (merge-only; prints exactly what it merged)
|
|
705
|
+
HOME=/tmp/ll-32-home LOAD_LOCK_STATE_DIR=/tmp/ll-32-test \
|
|
706
|
+
node dist/cli/main.js install --harness omac
|
|
707
|
+
|
|
708
|
+
# 2. omac's strict parser accepts the profile (unknown fields are errors)
|
|
709
|
+
HOME=/tmp/ll-32-home omac sandbox run -- true # exit 0, no ERROR lines
|
|
710
|
+
|
|
711
|
+
# 3. sandboxed client -> host daemon round-trip.
|
|
712
|
+
# NOTE: standalone `omac sandbox run` starts at cwd "/" and grants NO
|
|
713
|
+
# workdir by default — `--workdir-access read` binds the invocation
|
|
714
|
+
# dir read-only, modeling what a real omac agent session gets for its
|
|
715
|
+
# project workdir (which is why a repo checkout needs no read grant).
|
|
716
|
+
# The env values are set inside the sandboxed command with `env`
|
|
717
|
+
# (`-- env VAR=... node ...`); with the LOAD_LOCK_* allow_vars entry
|
|
718
|
+
# in place they could equally be inherited from the outer shell.
|
|
719
|
+
HOME=/tmp/ll-32-home omac sandbox run --workdir-access read -- \
|
|
720
|
+
env LOAD_LOCK_SOCKET=/tmp/ll-32-test/daemon.sock LOAD_LOCK_NO_SPAWN=1 \
|
|
721
|
+
LOAD_LOCK_LABEL_PREFIX=omac \
|
|
722
|
+
node dist/cli/main.js acquire --op-key issue-32-live \
|
|
723
|
+
--reason "issue-32 live verification build" --label "issue-32 build" --json
|
|
724
|
+
# -> permit prm_…; host status shows:
|
|
725
|
+
# held: omac:issue-32 build — issue-32 live verification build [claimed, awaiting start]
|
|
726
|
+
|
|
727
|
+
# 4. host-side release (or any later step from inside the sandbox):
|
|
728
|
+
node dist/cli/main.js release --permit prm_… # -> releasedNow: true
|
|
729
|
+
```
|
|
644
730
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
731
|
+
Also verified live:
|
|
732
|
+
|
|
733
|
+
- **`LOAD_LOCK_NO_SPAWN=1` error contract**: with the client pointed at a
|
|
734
|
+
missing socket inside the sandbox, the CLI exits **70** with the loud
|
|
735
|
+
"not reachable and auto-spawn is disabled … bridge its socket into this
|
|
736
|
+
sandbox" error instead of silently spawning a sandbox-local daemon.
|
|
737
|
+
- **Lease-mode lifecycle across the PID namespace** (same as the
|
|
738
|
+
bubblewrap case): `acquire` → `mark-started` (held, lease 900s) →
|
|
739
|
+
`release` (`releasedNow: true`), all issued from inside the sandbox;
|
|
740
|
+
long holds need lease mode because the host daemon pid is invisible to
|
|
741
|
+
the sandbox.
|
|
742
|
+
- **Install/uninstall round-trips against the real omac**: a populated
|
|
743
|
+
user profile (workdir/network/environment entries) survives install
|
|
744
|
+
byte-faithfully with our grants appended, omac's strict parser accepts
|
|
745
|
+
the merged file, and uninstall leaves the user entries intact; a
|
|
746
|
+
from-scratch profile is removed entirely on uninstall (with the omac
|
|
747
|
+
config tree pruned) and omac keeps running on its defaults.
|
|
748
|
+
|
|
749
|
+
The two gotchas that shaped the installer:
|
|
750
|
+
|
|
751
|
+
1. **A global install needs a read grant for the package tree, not just
|
|
752
|
+
the bin dir.** omac's builtin grants cover `~/.bun/bin`, but the
|
|
753
|
+
launcher resolves the dist tree under
|
|
754
|
+
`~/.bun/install/global/node_modules/@aron98/load-lock/dist/…` —
|
|
755
|
+
ungranted, the CLI dies with `Cannot find module …` (exit 1) before
|
|
756
|
+
ever reaching the socket. Proven live both ways: the sandboxed CLI
|
|
757
|
+
from a global-style tree failed with exactly that error before the
|
|
758
|
+
grant, and completed the full acquire→mark-started→release round-trip
|
|
759
|
+
after `filesystem.read` covered the tree. A repo checkout needs no
|
|
760
|
+
read grant (the agent session's workdir already covers it) and the
|
|
761
|
+
installer says so honestly in its output.
|
|
762
|
+
2. **Label prefixes need a current daemon on the host.** A daemon
|
|
763
|
+
started before v1.2.0 strips the unknown `labelPrefix` field silently
|
|
764
|
+
(zod); the dashboard shows the bare label with no error anywhere.
|
|
765
|
+
`load-lock service stop && load-lock service start` after upgrading.
|
|
766
|
+
|
|
767
|
+
What was NOT verified: omac's macOS seatbelt path (same profile schema,
|
|
768
|
+
per omac's docs, but the verification machine is Linux) and omac's agent
|
|
769
|
+
orchestration commands (`omac start`/`continue`/…) driving a real agent —
|
|
770
|
+
the round-trip was verified with the documented `omac sandbox run`
|
|
771
|
+
primitive those commands build on. The installer's contract tests cover
|
|
772
|
+
the merge/uninstall edge cases (`src/test/install.test.ts`).
|
|
660
773
|
|
|
661
774
|
### Docker — contract-inspected
|
|
662
775
|
|
|
@@ -730,8 +843,10 @@ path-literal strict, or re-generate the profile. Expected outcome
|
|
|
730
843
|
`LOAD_LOCK_LABEL_PREFIX=…`) passed through to the agent, a seatbelt-sandboxed
|
|
731
844
|
client talks to the host daemon exactly as in the verified bubblewrap
|
|
732
845
|
case. omac's macOS sandboxing uses seatbelt under the hood with the same
|
|
733
|
-
`environment.allow_vars` env-stripping as on Linux, so the omac
|
|
734
|
-
|
|
846
|
+
`environment.allow_vars` env-stripping as on Linux, so the omac installer
|
|
847
|
+
(`load-lock install --harness omac`, live-verified on Linux against omac
|
|
848
|
+
0.11.0 — see above) applies unchanged; only the socket path and the
|
|
849
|
+
seatbelt file rules differ.
|
|
735
850
|
|
|
736
851
|
### WSL2 — honest state
|
|
737
852
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aron98/load-lock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|