@sentropic/h2a-runtime 0.88.0 → 0.90.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 (49) hide show
  1. package/dist/attach.d.ts +21 -0
  2. package/dist/attach.d.ts.map +1 -1
  3. package/dist/attach.js +69 -1
  4. package/dist/attach.js.map +1 -1
  5. package/dist/file-lock.d.ts +34 -0
  6. package/dist/file-lock.d.ts.map +1 -0
  7. package/dist/file-lock.js +96 -0
  8. package/dist/file-lock.js.map +1 -0
  9. package/dist/index.d.ts +30 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +492 -50
  12. package/dist/index.js.map +1 -1
  13. package/dist/llm-gateway-runtime/accounts.d.ts.map +1 -1
  14. package/dist/llm-gateway-runtime/accounts.js +3 -0
  15. package/dist/llm-gateway-runtime/accounts.js.map +1 -1
  16. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +1 -1
  17. package/dist/llm-gateway-runtime/proxy-anthropic.js +24 -0
  18. package/dist/llm-gateway-runtime/proxy-anthropic.js.map +1 -1
  19. package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +1 -1
  20. package/dist/llm-gateway-runtime/proxy-openai.js +53 -2
  21. package/dist/llm-gateway-runtime/proxy-openai.js.map +1 -1
  22. package/dist/proc-cpu.d.ts +55 -0
  23. package/dist/proc-cpu.d.ts.map +1 -0
  24. package/dist/proc-cpu.js +133 -0
  25. package/dist/proc-cpu.js.map +1 -0
  26. package/dist/prompt-delivery.d.ts +159 -0
  27. package/dist/prompt-delivery.d.ts.map +1 -0
  28. package/dist/prompt-delivery.js +469 -0
  29. package/dist/prompt-delivery.js.map +1 -0
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js +7 -69
  32. package/dist/registry.js.map +1 -1
  33. package/dist/relaunch.d.ts +20 -1
  34. package/dist/relaunch.d.ts.map +1 -1
  35. package/dist/relaunch.js +31 -10
  36. package/dist/relaunch.js.map +1 -1
  37. package/dist/restore.d.ts +20 -4
  38. package/dist/restore.d.ts.map +1 -1
  39. package/dist/restore.js +26 -4
  40. package/dist/restore.js.map +1 -1
  41. package/dist/session-lease.d.ts +266 -0
  42. package/dist/session-lease.d.ts.map +1 -0
  43. package/dist/session-lease.js +342 -0
  44. package/dist/session-lease.js.map +1 -0
  45. package/dist/tmux.d.ts +77 -2
  46. package/dist/tmux.d.ts.map +1 -1
  47. package/dist/tmux.js +269 -23
  48. package/dist/tmux.js.map +1 -1
  49. package/package.json +1 -1
@@ -0,0 +1,266 @@
1
+ /**
2
+ * MACHINE-SCOPED session lease store.
3
+ *
4
+ * WHY THIS EXISTS. Nine subcontractor sessions, verifiably FINISHED, held 4 325
5
+ * minutes of concurrency slot for 61.4 minutes of real work (ratio 1:70). The
6
+ * pool stood at 15 of 12 — over cap, with zero of them working. It was never
7
+ * scarce; it was hoarded.
8
+ *
9
+ * THE DISTINCTION THAT DECIDES THE DESIGN: those sessions were not DEAD, they
10
+ * were FINISHED — a live process that no longer works (the composer back on its
11
+ * placeholder, "Worked for X" on screen). Death and inactivity are DISJOINT
12
+ * modes. A lock released on process death would have freed NONE of them, and
13
+ * neither does `sweepStaleJobs` (delegate.ts), which skips anything `isJobLive`.
14
+ * This store covers INACTIVITY: a finished-but-alive session stops beating, so
15
+ * `now − heartbeatAt > ttlMs` declares it abandoned after 30 minutes instead of
16
+ * 994.
17
+ *
18
+ * THE PATTERN IS `packages/track/src/lease/store.ts`; the LOCATION is not.
19
+ * Track's store is REPO-scoped (`.track/leases.json`). The concurrency pool is
20
+ * MACHINE-scoped: sessions from h2a-Impots, h2a-geo, h2a-immo, h2a-kog and
21
+ * h2a-openerp all consume it, so a lease filed in one repo's `.track/` cannot
22
+ * arbitrate a resource several repos contend for. It also would make pool
23
+ * correctness depend on repo identity, which DERIVES. So the file sits beside
24
+ * `registry.json` in the h2a config dir — the same machine-scoped home the
25
+ * session registry already uses, under NO `.track/` and under no repo.
26
+ *
27
+ * WHAT IS BORROWED FROM TRACK, VERBATIM IN SPIRIT:
28
+ * - abandonment is CLOCKLESS, COMPUTED BY THE READER: the caller injects `now`
29
+ * and a lease is abandoned iff `now − heartbeatAt > ttlMs`. The store holds
30
+ * no clock of its own — `now` is a REQUIRED constructor dependency here
31
+ * (stricter than track, which defaults it), so no code path can stamp a
32
+ * timestamp from an implicit clock;
33
+ * - one ACTIVE lease per subject (here: per session id);
34
+ * - each acquisition mints a fresh, unguessable `token`; only the holder
35
+ * presenting the matching token may heartbeat, observe or release.
36
+ *
37
+ * WHAT THE TOKEN DOES AND DOES NOT GUARANTEE. It is a GENERATION guard, not a
38
+ * secret: it is persisted in the same file, so any local process that can read
39
+ * the store can read it. What it structurally prevents is a stale holder — one
40
+ * that slept through its own abandonment — renewing or releasing the lease that
41
+ * REPLACED its own. That property is enforced by the code and tested; secrecy
42
+ * against a local reader is not claimed.
43
+ */
44
+ /**
45
+ * Default lease TTL. 30 minutes, the same figure track settled on: long enough
46
+ * that a session pausing between two tool calls is never mistaken for finished,
47
+ * short enough that a finished-but-alive session frees its slot within one
48
+ * working bout instead of the 994 minutes measured on the worst hoarder.
49
+ */
50
+ export declare const DEFAULT_SESSION_LEASE_TTL_MS: number;
51
+ /**
52
+ * The CPU rate (ms of CPU per second of wall clock, over a pane's whole process
53
+ * TREE) at or above which a session counts as WORKING and its lease is beaten.
54
+ *
55
+ * MEASURED on this workstation, 2026-07-30: 64 panes across 59 tmux sessions,
56
+ * one 150 s window, same /proc arithmetic as `proc-cpu.ts`, each pane classified
57
+ * by what its TUI showed at BOTH ends of the window:
58
+ *
59
+ * idle-but-alive n=59 median 12 ms/s p95 23 ms/s max 53 ms/s
60
+ * working n=2 142 and 146 ms/s
61
+ *
62
+ * 75 ms/s sits in the empty band between the two: above EVERY idle sample
63
+ * observed, well under the working ones. What the measurement does NOT
64
+ * establish, and this comment will not pretend it does: the working mode rests
65
+ * on two panes, and a session that works while burning little CPU — one long
66
+ * network wait, one tool call every ten minutes — reads as finished-but-alive
67
+ * here. That is survivable only because the decision this constant drives is
68
+ * whether to BEAT, never whether to kill: a session judged idle is merely
69
+ * PROPOSED for reclamation to a human or conductor, who can still see it.
70
+ */
71
+ export declare const DEFAULT_WORKING_CPU_MS_PER_SECOND = 75;
72
+ /** A CPU observation of the session's pane process tree, at a point in time. */
73
+ export interface SessionCpuSample {
74
+ /** Cumulative CPU (ms) of the pane's whole process tree — `paneTreeCpuMs`. */
75
+ cpuMs: number;
76
+ /** ISO-8601 instant the sample was taken (caller-injected clock). */
77
+ sampledAt: string;
78
+ }
79
+ /**
80
+ * A lease on one SESSION's concurrency slot. Persisted verbatim in
81
+ * `session-leases.json`. Abandonment is NOT a stored field: it is computed by
82
+ * the reader against an injected `now`.
83
+ */
84
+ export interface SessionLease {
85
+ leaseId: string;
86
+ /** Registry entry id of the leased session (a job id / session id). */
87
+ sessionId: string;
88
+ /** Who holds the slot — the launching surface or the supervising pass. */
89
+ holder: string;
90
+ /** Informational provenance: the repo/cwd the session was launched from. The
91
+ * lease is NOT scoped by it — the pool is machine-wide. */
92
+ workspace?: string;
93
+ acquiredAt: string;
94
+ heartbeatAt: string;
95
+ expiresAt: string;
96
+ ttlMs: number;
97
+ /** Per-acquisition generation guard (see the module header). */
98
+ token: string;
99
+ /** Last CPU observation — the evidence the beat decision is derived from. */
100
+ sample?: SessionCpuSample;
101
+ }
102
+ /** Rejected lease operation (wrong token, absent lease, subject already held). */
103
+ export declare class SessionLeaseError extends Error {
104
+ constructor(message: string);
105
+ }
106
+ /**
107
+ * Is `lease` abandoned at `now`? PURE and CLOCKLESS: abandoned iff
108
+ * `now − heartbeatAt > ttlMs`. The single abandonment predicate — reused by the
109
+ * acquire guard, by the reclaim report, and by callers deciding what to show.
110
+ */
111
+ export declare function isSessionLeaseAbandoned(lease: SessionLease, now: string): boolean;
112
+ /**
113
+ * CPU rate (ms of CPU per second of wall clock) between the lease's last
114
+ * observation and `cpuMsNow` taken at `now`. Undefined when there is no prior
115
+ * sample, when the elapsed window is not positive, or when the counter went
116
+ * BACKWARDS (the pane's tree was replaced — a relaunch, or PID reuse): a
117
+ * negative delta is not a rate, and must never be read as "idle". PURE.
118
+ */
119
+ export declare function cpuRateMsPerSecond(lease: SessionLease, cpuMsNow: number, now: string): number | undefined;
120
+ /**
121
+ * What a supervising pass should do with one lease, given a fresh CPU reading.
122
+ * PURE — the probe is the caller's effect, the decision is testable here.
123
+ *
124
+ * - `beat` — the tree burned CPU at or above `workingRateMsPerSecond`: the
125
+ * session is WORKING, renew its lease.
126
+ * - `observe` — alive but under the bar (finished-but-alive), or no prior
127
+ * sample to compare against: record the reading WITHOUT renewing,
128
+ * so the TTL keeps running and the next pass has a baseline.
129
+ * - `unknown` — no CPU reading at all (pane gone or unreadable). We do NOT
130
+ * beat: an unreadable pane is not proof of work. We also do not
131
+ * release: this store never decides a session is dead.
132
+ */
133
+ export type SessionBeatAction = "beat" | "observe" | "unknown";
134
+ export interface SessionBeatDecision {
135
+ action: SessionBeatAction;
136
+ /** The computed rate when one could be computed (diagnostics + reporting). */
137
+ rateMsPerSecond?: number;
138
+ reason: "working" | "under-threshold" | "no-baseline" | "no-cpu-reading";
139
+ }
140
+ export declare function decideSessionBeat(input: {
141
+ lease: SessionLease;
142
+ /** Fresh `paneTreeCpuMs` reading, or undefined when the pane cannot be read. */
143
+ cpuMsNow: number | undefined;
144
+ now: string;
145
+ workingRateMsPerSecond?: number;
146
+ }): SessionBeatDecision;
147
+ /** One abandoned lease, described so a caller can decide whether to reclaim it. */
148
+ export interface SessionReclaimProposal {
149
+ sessionId: string;
150
+ holder: string;
151
+ /** How long the lease has gone unbeaten at `now` (ms). */
152
+ idleMs: number;
153
+ ttlMs: number;
154
+ heartbeatAt: string;
155
+ /** CPU rate over the whole unbeaten window, when a sample exists (ms/s). */
156
+ lastRateMsPerSecond?: number;
157
+ }
158
+ /**
159
+ * The abandoned leases at `now`, as PROPOSALS. This function proposes; it never
160
+ * executes and never signals a process. A lease still inside its TTL — i.e. one
161
+ * that still beats — is structurally excluded: the only filter is
162
+ * `isSessionLeaseAbandoned`, so a beating session can never appear here. PURE.
163
+ */
164
+ export declare function reclaimProposals(leases: ReadonlyArray<SessionLease>, now: string): SessionReclaimProposal[];
165
+ /**
166
+ * What a supervising pass must do with each recorded lease, BEFORE any probe.
167
+ * The registry state decides; the CPU probe only happens for `probe` steps.
168
+ *
169
+ * - `release` — the session reached a terminal state, so its slot is already
170
+ * free: the lease must not outlive it.
171
+ * - `probe` — the session is still running: read its pane's CPU and let
172
+ * `decideSessionBeat` rule.
173
+ * - `leave` — the lease names a session the registry does not know. We do NOT
174
+ * release it: a missing or unreadable registry would otherwise wipe the
175
+ * leases of sessions that ARE working. It ages out and gets reported.
176
+ */
177
+ export type LeaseSupervisionStep = {
178
+ sessionId: string;
179
+ action: "release";
180
+ reason: "terminal";
181
+ } | {
182
+ sessionId: string;
183
+ action: "probe";
184
+ tmuxSession?: string;
185
+ } | {
186
+ sessionId: string;
187
+ action: "leave";
188
+ reason: "unregistered";
189
+ };
190
+ /** The registry facts this planner needs — structural, so no registry import. */
191
+ export interface LeasedSessionState {
192
+ id: string;
193
+ jobState?: string;
194
+ tmuxSession?: string;
195
+ }
196
+ export declare function planLeaseSupervision(leases: ReadonlyArray<SessionLease>, sessions: ReadonlyArray<LeasedSessionState>): LeaseSupervisionStep[];
197
+ /** Machine-scoped path of the session-lease side-store (beside `registry.json`). */
198
+ export declare function resolveSessionLeasePath(): string;
199
+ /**
200
+ * The session-lease side-store. Every mutation runs under the store's OWN
201
+ * lockfile (never the registry's) and is written atomically (tmp + rename).
202
+ *
203
+ * `now` is a REQUIRED dependency: the store stamps `acquiredAt`/`heartbeatAt`/
204
+ * `expiresAt` from it, and holds no clock of its own. Abandonment is still the
205
+ * READER's call — `abandoned(now)` takes the instant explicitly, with no default.
206
+ */
207
+ export declare class SessionLeaseStore {
208
+ private readonly leasesPath;
209
+ private readonly clock;
210
+ private readonly newId;
211
+ constructor(leasesPath: string, deps: {
212
+ now: () => string;
213
+ newId?: () => string;
214
+ });
215
+ /** Every recorded lease (live OR abandoned — liveness is the reader's call). */
216
+ readAll(): SessionLease[];
217
+ /** The lease recorded for a session, live or abandoned, if any. */
218
+ forSession(sessionId: string): SessionLease | undefined;
219
+ /** The leases abandoned at `now` (explicit instant — the store adds no clock). */
220
+ abandoned(now: string): SessionLease[];
221
+ /**
222
+ * Take the slot for `sessionId`. Rejects when a LIVE lease already covers it
223
+ * (one active lease per session). An ABANDONED prior lease does not block: the
224
+ * new acquisition replaces it and mints a FRESH token, so the previous holder
225
+ * can never beat or release what replaced its lease. An initial `cpuMs`
226
+ * baseline may be supplied so the first supervising pass can compute a rate.
227
+ */
228
+ acquire(input: {
229
+ sessionId: string;
230
+ holder: string;
231
+ workspace?: string;
232
+ ttlMs?: number;
233
+ cpuMs?: number;
234
+ }): SessionLease;
235
+ /**
236
+ * Renew a lease — the holder's assertion that the session is WORKING. Only the
237
+ * matching token is accepted. Resets the reader's abandonment window and, when
238
+ * `cpuMs` is supplied, records the reading the assertion was based on.
239
+ */
240
+ heartbeat(input: {
241
+ sessionId: string;
242
+ token: string;
243
+ cpuMs?: number;
244
+ }): SessionLease;
245
+ /**
246
+ * Record a CPU reading WITHOUT renewing the lease.
247
+ *
248
+ * This is what keeps the abandonment clock honest. A session that worked hard
249
+ * and then finished would, on the next pass, still show a high AVERAGE rate
250
+ * since its last beat and win itself another renewal it no longer deserves.
251
+ * Observing separates the two acts: the baseline moves, the TTL does not.
252
+ */
253
+ observe(input: {
254
+ sessionId: string;
255
+ token: string;
256
+ cpuMs: number;
257
+ }): SessionLease;
258
+ /** Give the slot back. Only the matching token is accepted. */
259
+ release(input: {
260
+ sessionId: string;
261
+ token: string;
262
+ }): void;
263
+ /** Shared token-guarded read-modify-write for `heartbeat` and `observe`. */
264
+ private mutate;
265
+ }
266
+ //# sourceMappingURL=session-lease.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-lease.d.ts","sourceRoot":"","sources":["../src/session-lease.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,342 @@
1
+ /**
2
+ * MACHINE-SCOPED session lease store.
3
+ *
4
+ * WHY THIS EXISTS. Nine subcontractor sessions, verifiably FINISHED, held 4 325
5
+ * minutes of concurrency slot for 61.4 minutes of real work (ratio 1:70). The
6
+ * pool stood at 15 of 12 — over cap, with zero of them working. It was never
7
+ * scarce; it was hoarded.
8
+ *
9
+ * THE DISTINCTION THAT DECIDES THE DESIGN: those sessions were not DEAD, they
10
+ * were FINISHED — a live process that no longer works (the composer back on its
11
+ * placeholder, "Worked for X" on screen). Death and inactivity are DISJOINT
12
+ * modes. A lock released on process death would have freed NONE of them, and
13
+ * neither does `sweepStaleJobs` (delegate.ts), which skips anything `isJobLive`.
14
+ * This store covers INACTIVITY: a finished-but-alive session stops beating, so
15
+ * `now − heartbeatAt > ttlMs` declares it abandoned after 30 minutes instead of
16
+ * 994.
17
+ *
18
+ * THE PATTERN IS `packages/track/src/lease/store.ts`; the LOCATION is not.
19
+ * Track's store is REPO-scoped (`.track/leases.json`). The concurrency pool is
20
+ * MACHINE-scoped: sessions from h2a-Impots, h2a-geo, h2a-immo, h2a-kog and
21
+ * h2a-openerp all consume it, so a lease filed in one repo's `.track/` cannot
22
+ * arbitrate a resource several repos contend for. It also would make pool
23
+ * correctness depend on repo identity, which DERIVES. So the file sits beside
24
+ * `registry.json` in the h2a config dir — the same machine-scoped home the
25
+ * session registry already uses, under NO `.track/` and under no repo.
26
+ *
27
+ * WHAT IS BORROWED FROM TRACK, VERBATIM IN SPIRIT:
28
+ * - abandonment is CLOCKLESS, COMPUTED BY THE READER: the caller injects `now`
29
+ * and a lease is abandoned iff `now − heartbeatAt > ttlMs`. The store holds
30
+ * no clock of its own — `now` is a REQUIRED constructor dependency here
31
+ * (stricter than track, which defaults it), so no code path can stamp a
32
+ * timestamp from an implicit clock;
33
+ * - one ACTIVE lease per subject (here: per session id);
34
+ * - each acquisition mints a fresh, unguessable `token`; only the holder
35
+ * presenting the matching token may heartbeat, observe or release.
36
+ *
37
+ * WHAT THE TOKEN DOES AND DOES NOT GUARANTEE. It is a GENERATION guard, not a
38
+ * secret: it is persisted in the same file, so any local process that can read
39
+ * the store can read it. What it structurally prevents is a stale holder — one
40
+ * that slept through its own abandonment — renewing or releasing the lease that
41
+ * REPLACED its own. That property is enforced by the code and tested; secrecy
42
+ * against a local reader is not claimed.
43
+ */
44
+ import { randomBytes } from "node:crypto";
45
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
46
+ import { dirname, join } from "node:path";
47
+ import { resolveConfigDir } from "./config.js";
48
+ import { withFileLock } from "./file-lock.js";
49
+ /**
50
+ * Default lease TTL. 30 minutes, the same figure track settled on: long enough
51
+ * that a session pausing between two tool calls is never mistaken for finished,
52
+ * short enough that a finished-but-alive session frees its slot within one
53
+ * working bout instead of the 994 minutes measured on the worst hoarder.
54
+ */
55
+ export const DEFAULT_SESSION_LEASE_TTL_MS = 30 * 60_000;
56
+ /**
57
+ * The CPU rate (ms of CPU per second of wall clock, over a pane's whole process
58
+ * TREE) at or above which a session counts as WORKING and its lease is beaten.
59
+ *
60
+ * MEASURED on this workstation, 2026-07-30: 64 panes across 59 tmux sessions,
61
+ * one 150 s window, same /proc arithmetic as `proc-cpu.ts`, each pane classified
62
+ * by what its TUI showed at BOTH ends of the window:
63
+ *
64
+ * idle-but-alive n=59 median 12 ms/s p95 23 ms/s max 53 ms/s
65
+ * working n=2 142 and 146 ms/s
66
+ *
67
+ * 75 ms/s sits in the empty band between the two: above EVERY idle sample
68
+ * observed, well under the working ones. What the measurement does NOT
69
+ * establish, and this comment will not pretend it does: the working mode rests
70
+ * on two panes, and a session that works while burning little CPU — one long
71
+ * network wait, one tool call every ten minutes — reads as finished-but-alive
72
+ * here. That is survivable only because the decision this constant drives is
73
+ * whether to BEAT, never whether to kill: a session judged idle is merely
74
+ * PROPOSED for reclamation to a human or conductor, who can still see it.
75
+ */
76
+ export const DEFAULT_WORKING_CPU_MS_PER_SECOND = 75;
77
+ /** Rejected lease operation (wrong token, absent lease, subject already held). */
78
+ export class SessionLeaseError extends Error {
79
+ constructor(message) {
80
+ super(message);
81
+ this.name = "SessionLeaseError";
82
+ }
83
+ }
84
+ /**
85
+ * Is `lease` abandoned at `now`? PURE and CLOCKLESS: abandoned iff
86
+ * `now − heartbeatAt > ttlMs`. The single abandonment predicate — reused by the
87
+ * acquire guard, by the reclaim report, and by callers deciding what to show.
88
+ */
89
+ export function isSessionLeaseAbandoned(lease, now) {
90
+ return Date.parse(now) - Date.parse(lease.heartbeatAt) > lease.ttlMs;
91
+ }
92
+ /**
93
+ * CPU rate (ms of CPU per second of wall clock) between the lease's last
94
+ * observation and `cpuMsNow` taken at `now`. Undefined when there is no prior
95
+ * sample, when the elapsed window is not positive, or when the counter went
96
+ * BACKWARDS (the pane's tree was replaced — a relaunch, or PID reuse): a
97
+ * negative delta is not a rate, and must never be read as "idle". PURE.
98
+ */
99
+ export function cpuRateMsPerSecond(lease, cpuMsNow, now) {
100
+ const previous = lease.sample;
101
+ if (previous === undefined)
102
+ return undefined;
103
+ const elapsedMs = Date.parse(now) - Date.parse(previous.sampledAt);
104
+ if (!Number.isFinite(elapsedMs) || elapsedMs <= 0)
105
+ return undefined;
106
+ const deltaMs = cpuMsNow - previous.cpuMs;
107
+ if (deltaMs < 0)
108
+ return undefined;
109
+ return (deltaMs / elapsedMs) * 1000;
110
+ }
111
+ export function decideSessionBeat(input) {
112
+ if (input.cpuMsNow === undefined) {
113
+ return { action: "unknown", reason: "no-cpu-reading" };
114
+ }
115
+ const rate = cpuRateMsPerSecond(input.lease, input.cpuMsNow, input.now);
116
+ if (rate === undefined) {
117
+ return { action: "observe", reason: "no-baseline" };
118
+ }
119
+ const bar = input.workingRateMsPerSecond ?? DEFAULT_WORKING_CPU_MS_PER_SECOND;
120
+ return rate >= bar
121
+ ? { action: "beat", rateMsPerSecond: rate, reason: "working" }
122
+ : { action: "observe", rateMsPerSecond: rate, reason: "under-threshold" };
123
+ }
124
+ /**
125
+ * The abandoned leases at `now`, as PROPOSALS. This function proposes; it never
126
+ * executes and never signals a process. A lease still inside its TTL — i.e. one
127
+ * that still beats — is structurally excluded: the only filter is
128
+ * `isSessionLeaseAbandoned`, so a beating session can never appear here. PURE.
129
+ */
130
+ export function reclaimProposals(leases, now) {
131
+ const nowMs = Date.parse(now);
132
+ return leases
133
+ .filter((lease) => isSessionLeaseAbandoned(lease, now))
134
+ .map((lease) => {
135
+ const idleMs = nowMs - Date.parse(lease.heartbeatAt);
136
+ const proposal = {
137
+ sessionId: lease.sessionId,
138
+ holder: lease.holder,
139
+ idleMs,
140
+ ttlMs: lease.ttlMs,
141
+ heartbeatAt: lease.heartbeatAt,
142
+ };
143
+ return proposal;
144
+ })
145
+ .sort((a, b) => b.idleMs - a.idleMs);
146
+ }
147
+ export function planLeaseSupervision(leases, sessions) {
148
+ const byId = new Map(sessions.map((session) => [session.id, session]));
149
+ return leases.map((lease) => {
150
+ const session = byId.get(lease.sessionId);
151
+ if (session === undefined) {
152
+ return { sessionId: lease.sessionId, action: "leave", reason: "unregistered" };
153
+ }
154
+ if (session.jobState === "done" || session.jobState === "failed") {
155
+ return { sessionId: lease.sessionId, action: "release", reason: "terminal" };
156
+ }
157
+ return {
158
+ sessionId: lease.sessionId,
159
+ action: "probe",
160
+ ...(session.tmuxSession !== undefined ? { tmuxSession: session.tmuxSession } : {}),
161
+ };
162
+ });
163
+ }
164
+ /** Machine-scoped path of the session-lease side-store (beside `registry.json`). */
165
+ export function resolveSessionLeasePath() {
166
+ return join(resolveConfigDir(), "session-leases.json");
167
+ }
168
+ /**
169
+ * The session-lease side-store. Every mutation runs under the store's OWN
170
+ * lockfile (never the registry's) and is written atomically (tmp + rename).
171
+ *
172
+ * `now` is a REQUIRED dependency: the store stamps `acquiredAt`/`heartbeatAt`/
173
+ * `expiresAt` from it, and holds no clock of its own. Abandonment is still the
174
+ * READER's call — `abandoned(now)` takes the instant explicitly, with no default.
175
+ */
176
+ export class SessionLeaseStore {
177
+ leasesPath;
178
+ clock;
179
+ newId;
180
+ constructor(leasesPath, deps) {
181
+ this.leasesPath = leasesPath;
182
+ this.clock = deps.now;
183
+ this.newId = deps.newId ?? (() => randomBytes(12).toString("hex"));
184
+ }
185
+ /** Every recorded lease (live OR abandoned — liveness is the reader's call). */
186
+ readAll() {
187
+ return readLeaseFile(this.leasesPath);
188
+ }
189
+ /** The lease recorded for a session, live or abandoned, if any. */
190
+ forSession(sessionId) {
191
+ return this.readAll().find((lease) => lease.sessionId === sessionId);
192
+ }
193
+ /** The leases abandoned at `now` (explicit instant — the store adds no clock). */
194
+ abandoned(now) {
195
+ return this.readAll().filter((lease) => isSessionLeaseAbandoned(lease, now));
196
+ }
197
+ /**
198
+ * Take the slot for `sessionId`. Rejects when a LIVE lease already covers it
199
+ * (one active lease per session). An ABANDONED prior lease does not block: the
200
+ * new acquisition replaces it and mints a FRESH token, so the previous holder
201
+ * can never beat or release what replaced its lease. An initial `cpuMs`
202
+ * baseline may be supplied so the first supervising pass can compute a rate.
203
+ */
204
+ acquire(input) {
205
+ return withFileLock(this.leasesPath, () => {
206
+ const all = readLeaseFile(this.leasesPath);
207
+ const now = this.clock();
208
+ const existing = all.find((lease) => lease.sessionId === input.sessionId);
209
+ if (existing !== undefined && !isSessionLeaseAbandoned(existing, now)) {
210
+ throw new SessionLeaseError(`session-lease: ${input.sessionId} is already held by ${existing.holder} (one active lease per session)`);
211
+ }
212
+ const ttlMs = input.ttlMs ?? DEFAULT_SESSION_LEASE_TTL_MS;
213
+ const lease = {
214
+ leaseId: this.newId(),
215
+ sessionId: input.sessionId,
216
+ holder: input.holder,
217
+ ...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
218
+ acquiredAt: now,
219
+ heartbeatAt: now,
220
+ expiresAt: new Date(Date.parse(now) + ttlMs).toISOString(),
221
+ ttlMs,
222
+ token: this.newId(),
223
+ ...(input.cpuMs !== undefined
224
+ ? { sample: { cpuMs: input.cpuMs, sampledAt: now } }
225
+ : {}),
226
+ };
227
+ const next = all.filter((l) => l.sessionId !== input.sessionId);
228
+ next.push(lease);
229
+ writeLeaseFile(this.leasesPath, next);
230
+ return lease;
231
+ });
232
+ }
233
+ /**
234
+ * Renew a lease — the holder's assertion that the session is WORKING. Only the
235
+ * matching token is accepted. Resets the reader's abandonment window and, when
236
+ * `cpuMs` is supplied, records the reading the assertion was based on.
237
+ */
238
+ heartbeat(input) {
239
+ return this.mutate(input.sessionId, input.token, "heartbeat", (lease) => {
240
+ const now = this.clock();
241
+ return {
242
+ ...lease,
243
+ heartbeatAt: now,
244
+ expiresAt: new Date(Date.parse(now) + lease.ttlMs).toISOString(),
245
+ ...(input.cpuMs !== undefined
246
+ ? { sample: { cpuMs: input.cpuMs, sampledAt: now } }
247
+ : {}),
248
+ };
249
+ });
250
+ }
251
+ /**
252
+ * Record a CPU reading WITHOUT renewing the lease.
253
+ *
254
+ * This is what keeps the abandonment clock honest. A session that worked hard
255
+ * and then finished would, on the next pass, still show a high AVERAGE rate
256
+ * since its last beat and win itself another renewal it no longer deserves.
257
+ * Observing separates the two acts: the baseline moves, the TTL does not.
258
+ */
259
+ observe(input) {
260
+ return this.mutate(input.sessionId, input.token, "observe", (lease) => ({
261
+ ...lease,
262
+ sample: { cpuMs: input.cpuMs, sampledAt: this.clock() },
263
+ }));
264
+ }
265
+ /** Give the slot back. Only the matching token is accepted. */
266
+ release(input) {
267
+ withFileLock(this.leasesPath, () => {
268
+ const all = readLeaseFile(this.leasesPath);
269
+ const idx = all.findIndex((lease) => lease.sessionId === input.sessionId);
270
+ if (idx === -1) {
271
+ throw new SessionLeaseError(`session-lease: no lease to release for ${input.sessionId}`);
272
+ }
273
+ if (all[idx].token !== input.token) {
274
+ throw new SessionLeaseError(`session-lease: release token mismatch for ${input.sessionId} — only the holder may release`);
275
+ }
276
+ all.splice(idx, 1);
277
+ writeLeaseFile(this.leasesPath, all);
278
+ });
279
+ }
280
+ /** Shared token-guarded read-modify-write for `heartbeat` and `observe`. */
281
+ mutate(sessionId, token, verb, apply) {
282
+ return withFileLock(this.leasesPath, () => {
283
+ const all = readLeaseFile(this.leasesPath);
284
+ const idx = all.findIndex((lease) => lease.sessionId === sessionId);
285
+ if (idx === -1) {
286
+ throw new SessionLeaseError(`session-lease: no lease to ${verb} for ${sessionId}`);
287
+ }
288
+ const lease = all[idx];
289
+ if (lease.token !== token) {
290
+ throw new SessionLeaseError(`session-lease: ${verb} token mismatch for ${sessionId} — only the holder may ${verb}`);
291
+ }
292
+ const next = apply(lease);
293
+ all[idx] = next;
294
+ writeLeaseFile(this.leasesPath, all);
295
+ return next;
296
+ });
297
+ }
298
+ }
299
+ /** Shape guard — a torn or hand-edited row must not poison the whole store. */
300
+ function isSessionLease(value) {
301
+ if (typeof value !== "object" || value === null)
302
+ return false;
303
+ const lease = value;
304
+ return (typeof lease.leaseId === "string" &&
305
+ typeof lease.sessionId === "string" &&
306
+ typeof lease.holder === "string" &&
307
+ typeof lease.acquiredAt === "string" &&
308
+ typeof lease.heartbeatAt === "string" &&
309
+ typeof lease.token === "string" &&
310
+ typeof lease.ttlMs === "number");
311
+ }
312
+ /**
313
+ * Read + parse the store. A missing, empty or garbled file degrades to `[]` —
314
+ * the side-store is advisory, and a read must never throw into a launch path.
315
+ * The consequence is stated plainly: a corrupt store forgets every lease, which
316
+ * makes slots look FREE (admits work), never held.
317
+ */
318
+ function readLeaseFile(leasesPath) {
319
+ if (!existsSync(leasesPath))
320
+ return [];
321
+ try {
322
+ const raw = readFileSync(leasesPath, "utf8").trim();
323
+ if (raw === "")
324
+ return [];
325
+ const parsed = JSON.parse(raw);
326
+ const leases = parsed?.leases;
327
+ if (!Array.isArray(leases))
328
+ return [];
329
+ return leases.filter(isSessionLease);
330
+ }
331
+ catch {
332
+ return [];
333
+ }
334
+ }
335
+ /** Atomic write: tmp file in the same dir, then rename (as the registry does). */
336
+ function writeLeaseFile(leasesPath, leases) {
337
+ mkdirSync(dirname(leasesPath), { recursive: true });
338
+ const tmp = `${leasesPath}.tmp.${process.pid}`;
339
+ writeFileSync(tmp, `${JSON.stringify({ version: 1, leases }, null, 2)}\n`, "utf8");
340
+ renameSync(tmp, leasesPath);
341
+ }
342
+ //# sourceMappingURL=session-lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-lease.js","sourceRoot":"","sources":["../src/session-lease.ts"],"names":[],"mappings":"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"}