@aron98/load-lock 0.0.0-stage → 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -2
  3. package/demo/demo.sh +112 -0
  4. package/demo/sim-agent.mjs +149 -0
  5. package/dist/adapters/opencode/plugin.d.ts +18 -0
  6. package/dist/adapters/opencode/plugin.js +301 -0
  7. package/dist/cli/hook.d.ts +1 -0
  8. package/dist/cli/hook.js +141 -0
  9. package/dist/cli/install.d.ts +14 -0
  10. package/dist/cli/install.js +377 -0
  11. package/dist/cli/main.d.ts +2 -0
  12. package/dist/cli/main.js +461 -0
  13. package/dist/core/daemon-main.d.ts +2 -0
  14. package/dist/core/daemon-main.js +27 -0
  15. package/dist/core/daemon.d.ts +79 -0
  16. package/dist/core/daemon.js +526 -0
  17. package/dist/core/persist.d.ts +11 -0
  18. package/dist/core/persist.js +66 -0
  19. package/dist/core/state.d.ts +203 -0
  20. package/dist/core/state.js +909 -0
  21. package/dist/core/sysinfo.d.ts +2 -0
  22. package/dist/core/sysinfo.js +123 -0
  23. package/dist/mcp/main.d.ts +2 -0
  24. package/dist/mcp/main.js +4 -0
  25. package/dist/mcp/server.d.ts +27 -0
  26. package/dist/mcp/server.js +352 -0
  27. package/dist/policy/text.d.ts +30 -0
  28. package/dist/policy/text.js +58 -0
  29. package/dist/shared/client.d.ts +30 -0
  30. package/dist/shared/client.js +227 -0
  31. package/dist/shared/protocol.d.ts +333 -0
  32. package/dist/shared/protocol.js +22 -0
  33. package/dist/test/core.test.d.ts +1 -0
  34. package/dist/test/core.test.js +281 -0
  35. package/dist/test/daemon.test.d.ts +1 -0
  36. package/dist/test/daemon.test.js +384 -0
  37. package/dist/test/hook.test.d.ts +1 -0
  38. package/dist/test/hook.test.js +126 -0
  39. package/dist/test/mcp.test.d.ts +1 -0
  40. package/dist/test/mcp.test.js +139 -0
  41. package/dist/test/run-helper.test.d.ts +1 -0
  42. package/dist/test/run-helper.test.js +84 -0
  43. package/dist/test/testutil.d.ts +27 -0
  44. package/dist/test/testutil.js +111 -0
  45. package/docs/ADAPTERS.md +118 -0
  46. package/docs/AGENT_GUIDE.md +142 -0
  47. package/docs/ARCHITECTURE.md +195 -0
  48. package/docs/COMPATIBILITY.md +116 -0
  49. package/docs/OPERATIONS.md +126 -0
  50. package/package.json +102 -4
  51. package/skills/load-lock/SKILL.md +73 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * load-lock core: a pure, synchronous state machine for the machine-wide
3
+ * counting semaphore. No I/O here — the daemon drives timers, process
4
+ * liveness checks, journaling and persistence. All mutations are serialized
5
+ * by construction (single-threaded event loop of the daemon), so every
6
+ * handle() call is atomic with respect to every other call. Notably,
7
+ * execution association (start_job) is atomic with respect to reclamation:
8
+ * a start either observes a live claimed permit and transitions it, or fails.
9
+ *
10
+ * Permit lifecycle (see docs/ARCHITECTURE.md):
11
+ * acquire --grant--> held(phase=claimed, start deadline)
12
+ * offer --claim----> held(phase=claimed, start deadline)
13
+ * claimed --start_job-------> held(phase=running_tracked, pid+pgid tracked)
14
+ * claimed --mark_started----> held(phase=running_untracked, heartbeat lease)
15
+ * running_* --job end-------> released (automatic or explicit, idempotent)
16
+ * claimed past deadline ----> released (reclaimed)
17
+ * untracked lease expired --> released (reclaimed)
18
+ *
19
+ * Invariants (covered by tests):
20
+ * - used(pool) = sum(weights of held) + sum(weights of offered) <= capacity,
21
+ * except temporarily after a capacity decrease (holders always finish).
22
+ * - Queue admission is strict FIFO per pool.
23
+ * - Offers reserve capacity; an expired/declined offer passes capacity to
24
+ * the next waiter and the request becomes `paused` (never auto-recycled).
25
+ * - Tokens (offer/permit) are opaque and single-use; only owners ever see
26
+ * them; terminal states invalidate them (kept briefly for idempotency).
27
+ * - acquire() is idempotent per (agentId, opKey) while a request is live.
28
+ * - A forgotten release cannot permanently block the queue: every held
29
+ * permit has an automatic exit (start deadline, tracked process death, or
30
+ * untracked lease). A known-running tracked job is never revoked on
31
+ * duration alone.
32
+ */
33
+ import { AcquireParams, AcquireResult, CancelParams, CancelResult, ClaimResult, ConfigHistoryEntry, ConfigureParams, ConfigureResult, EntrySnapshot, EntryState, GetConfigResult, HolderDiag, JournalEntry, MachineConfig, MachineEvent, OwnerEntrySnapshot, PermitPhase, RejoinParams, ReleaseResult, StatusResult } from "../shared/protocol.js";
34
+ export declare const LONG_HOLD_MS: number;
35
+ export declare function defaultConfig(): MachineConfig;
36
+ interface Entry {
37
+ id: string;
38
+ pool: string;
39
+ agentId: string;
40
+ agentLabel?: string;
41
+ cwd?: string;
42
+ opKey: string;
43
+ reason: string;
44
+ weight: number;
45
+ state: EntryState;
46
+ since: number;
47
+ offerToken?: string;
48
+ offerExpiresAt?: number;
49
+ permitToken?: string;
50
+ phase?: PermitPhase;
51
+ startDeadline?: number;
52
+ pid?: number;
53
+ pgid?: number;
54
+ pidStartTime?: string;
55
+ startedAt?: number;
56
+ leaseDeadline?: number;
57
+ lastSeen?: number;
58
+ enqueuedAt?: number;
59
+ }
60
+ export declare class Machine {
61
+ config: MachineConfig;
62
+ private entries;
63
+ private byKey;
64
+ private offerTokens;
65
+ private permitTokens;
66
+ private queues;
67
+ private expiredOffers;
68
+ private events;
69
+ private nextEventId;
70
+ private clock;
71
+ startedAt: number;
72
+ constructor(clock?: () => number, config?: MachineConfig);
73
+ now(): number;
74
+ private pushEvent;
75
+ /** Events with id > since, newest last. */
76
+ eventsSince(since: number, agentId?: string, limit?: number): MachineEvent[];
77
+ latestEventId(): number;
78
+ private emit;
79
+ snapshot(e: Entry): EntrySnapshot;
80
+ private ownerSnapshot;
81
+ private position;
82
+ private poolNames;
83
+ private capacity;
84
+ /** Weight currently consuming capacity (held + offered). */
85
+ used(pool: string): number;
86
+ private liveEntryByOpKey;
87
+ /**
88
+ * Fill capacity from the FIFO queue. Strict FIFO: the head of the queue is
89
+ * admitted only if it fits; otherwise we stop (no bypassing heavier
90
+ * requests by lighter ones behind them).
91
+ */
92
+ private admit;
93
+ /** Remove an entry from its pool queue if present. */
94
+ private dequeue;
95
+ /** Remove op-key association and invalidate live tokens (terminal-ish). */
96
+ private retire;
97
+ private newPermit;
98
+ acquire(p: AcquireParams): AcquireResult;
99
+ claim(offerToken: string): ClaimResult;
100
+ /** Daemon-side claim by request id (used for bounded waits). */
101
+ claimByRequestId(requestId: string): {
102
+ entry: OwnerEntrySnapshot;
103
+ };
104
+ /**
105
+ * Associate a claimed permit with a real process. Atomic with respect to
106
+ * reclamation: succeeds only if the permit is held and still in a startable
107
+ * phase; afterwards the permit is protected by process tracking instead of
108
+ * the start deadline. Callers that spawn the job first must kill it if this
109
+ * call fails (see `load-lock run` / run_with_permit helpers).
110
+ */
111
+ startJob(permitToken: string, pid: number, pgid?: number, pidStartTime?: string): OwnerEntrySnapshot;
112
+ /**
113
+ * Explicit "I started the work, but I cannot associate a process".
114
+ * The untracked lease applies: renewed by automatic adapter heartbeats,
115
+ * reclaimed when the owning agent is gone. Documented weaker guarantee.
116
+ */
117
+ markStarted(permitToken: string): OwnerEntrySnapshot;
118
+ /** Adapter heartbeat: extends untracked leases and records agent liveness. */
119
+ heartbeat(agentId: string): void;
120
+ /** Idempotent release: a second release returns the terminal state. */
121
+ release(permitToken: string): ReleaseResult;
122
+ /**
123
+ * Daemon-internal release with positive evidence of job death (tracked).
124
+ * Never expires a permit whose process is merely unknown — callers must
125
+ * only invoke this with confirmed death of pid/pgid.
126
+ */
127
+ autoRelease(entryId: string, cause: string): void;
128
+ /**
129
+ * Reclaim: start deadline exceeded, untracked lease expired, or admin
130
+ * force-release. Frees capacity for the queue.
131
+ */
132
+ private reclaim;
133
+ private finish;
134
+ /** Expire one offered entry (timer-driven or claim-time). */
135
+ private expireOffer;
136
+ /** Nearest future deadline of any kind, or null. The daemon arms timers with this. */
137
+ nextExpiry(): number | null;
138
+ /** Called by the daemon at each tick: expire offers, reclaim stale permits, prune. */
139
+ tick(now: number): void;
140
+ /** Drop terminal entries (and their idempotency token mappings) after 1h. */
141
+ private prune;
142
+ private rememberExpired;
143
+ decline(offerToken: string): void;
144
+ rejoin(p: RejoinParams): OwnerEntrySnapshot;
145
+ cancel(p: CancelParams): CancelResult;
146
+ pending(agentId: string): {
147
+ offers: OwnerEntrySnapshot[];
148
+ queue: OwnerEntrySnapshot[];
149
+ paused: OwnerEntrySnapshot[];
150
+ held: OwnerEntrySnapshot[];
151
+ };
152
+ /** All outstanding offers (owner-scoped snapshots) — for cwd-matched wakeup. */
153
+ allOffers(): OwnerEntrySnapshot[];
154
+ status(pool?: string): StatusResult;
155
+ /** Held permits — input for daemon liveness sweeps and CLI recovery. */
156
+ heldEntries(): (EntrySnapshot & {
157
+ pgid?: number;
158
+ })[];
159
+ entryByPermit(permitToken: string): Entry | undefined;
160
+ private heldByPermit;
161
+ entryById(requestId: string): {
162
+ state: EntryState;
163
+ phase?: PermitPhase;
164
+ } & EntrySnapshot;
165
+ ownerSnapshotById(requestId: string): OwnerEntrySnapshot | undefined;
166
+ offerTokenOf(requestId: string): string | undefined;
167
+ /** Admin (CLI recovery) force-release of a held permit — reclaims capacity. */
168
+ adminRelease(requestId: string): void;
169
+ /**
170
+ * Recovery diagnostics for held permits. `liveness(pid, pgid, pidStartTime)`
171
+ * is supplied by the daemon (reads /proc). Returns holder + job info and
172
+ * which automatic cleanup applies. Long holds are flagged, never revoked
173
+ * here.
174
+ */
175
+ diagnose(liveness: (e: {
176
+ pid?: number;
177
+ pgid?: number;
178
+ pidStartTime?: string;
179
+ }) => boolean | null): HolderDiag[];
180
+ get_config(): GetConfigResult;
181
+ configure(p: ConfigureParams): ConfigureResult;
182
+ history: ConfigHistoryEntry[];
183
+ /** Held permits to persist for conservative restart reconciliation. */
184
+ journalEntries(): JournalEntry[];
185
+ /**
186
+ * Restore held permits after a daemon restart (conservative reconciliation):
187
+ * - tracked: re-check liveness via the supplied probe; dead => release,
188
+ * alive => keep (same token, so a late explicit release still works).
189
+ * - claimed: keep the start deadline (reclaimed by tick if already past).
190
+ * - untracked: keep, with a FRESH lease window from restore time.
191
+ * Queued/offered requests are deliberately NOT restored — queued agents
192
+ * re-acquire (documented restart behavior).
193
+ */
194
+ restore(journal: JournalEntry[], liveness: (e: {
195
+ pid?: number;
196
+ pgid?: number;
197
+ pidStartTime?: string;
198
+ }) => boolean | null): {
199
+ kept: number;
200
+ releasedDead: number;
201
+ };
202
+ }
203
+ export {};