thinkpool-pair 0.7.364 → 0.7.366

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 (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
@@ -1,364 +1 @@
1
- // Host-wide scheduled-run admission for account-served room bridge children.
2
- //
3
- // Every room bridge is a separate process, so a process-local count/window cannot
4
- // enforce the laptop safety boundary. This ledger serializes admissions under
5
- // TP_PAIR_ROOT. It stores only hashes and millisecond timestamps: never room codes,
6
- // prompts, provider/model names, credentials, endpoints, or host paths.
7
-
8
- import fs from 'node:fs'
9
- import os from 'node:os'
10
- import path from 'node:path'
11
- import { createHash, randomUUID } from 'node:crypto'
12
- import { SPAWN } from './cross-terminal.mjs'
13
-
14
- const VERSION = 1
15
- const LEDGER_NAME = 'scheduled-run-admission.json'
16
- const LOCK_NAME = '.scheduled-run-admission.lock'
17
- const MAX_LEDGER_BYTES = 128 * 1024
18
- const DEFAULT_LOCK_WAIT_MS = 1_000
19
- const DEFAULT_STALE_LOCK_MS = 30_000
20
- const DEFAULT_RETRY_MS = 12
21
- const DEFAULT_MAX_LEASE_MS = 31 * 60 * 1_000
22
- const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,255}$/
23
-
24
- const rootPath = (root) => path.resolve(root || process.env.TP_PAIR_ROOT || path.join(os.homedir(), '.thinkpool-pair'))
25
- const pathsFor = (root) => {
26
- const dir = rootPath(root)
27
- return {
28
- dir,
29
- ledger: path.join(dir, LEDGER_NAME),
30
- lock: path.join(dir, LOCK_NAME),
31
- }
32
- }
33
- const digest = (value) => createHash('sha256').update(String(value)).digest('hex')
34
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
35
- const finiteInt = (value) => Number.isSafeInteger(value) && value >= 0
36
- const exactKeys = (value, expected) =>
37
- value && typeof value === 'object' &&
38
- Object.keys(value).sort().join('\0') === [...expected].sort().join('\0')
39
-
40
- function normalizeLimits (limits = {}) {
41
- const machineMax = Number.isSafeInteger(limits.machineMax) && limits.machineMax > 0 ? limits.machineMax : SPAWN.machineMax
42
- const perWindowCap = Number.isSafeInteger(limits.perWindowCap) && limits.perWindowCap > 0 ? limits.perWindowCap : SPAWN.perWindowCap
43
- const windowMs = Number.isSafeInteger(limits.windowMs) && limits.windowMs > 0 ? limits.windowMs : SPAWN.windowMs
44
- const maxLeaseMs = Number.isSafeInteger(limits.maxLeaseMs) && limits.maxLeaseMs > 0 ? limits.maxLeaseMs : DEFAULT_MAX_LEASE_MS
45
- return Object.freeze({ machineMax, perWindowCap, windowMs, maxLeaseMs })
46
- }
47
-
48
- function emptyLedger () {
49
- return { version: VERSION, leases: [], spawnTimes: [] }
50
- }
51
-
52
- function validLease (lease) {
53
- return Boolean(exactKeys(lease, ['runKey', 'terminalKey', 'tokenHash', 'acquiredAt', 'expiresAt']) &&
54
- /^[a-f0-9]{64}$/.test(lease.runKey) &&
55
- /^[a-f0-9]{64}$/.test(lease.terminalKey) &&
56
- /^[a-f0-9]{64}$/.test(lease.tokenHash) &&
57
- finiteInt(lease.acquiredAt) &&
58
- finiteInt(lease.expiresAt) &&
59
- lease.expiresAt > lease.acquiredAt)
60
- }
61
-
62
- function readLedger (file, limits) {
63
- let fd = null
64
- let raw
65
- try {
66
- fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0))
67
- const stat = fs.fstatSync(fd)
68
- if (!stat.isFile() || stat.size > MAX_LEDGER_BYTES) throw new Error('invalid_admission_ledger')
69
- raw = fs.readFileSync(fd, 'utf8')
70
- } catch (error) {
71
- if (error?.code === 'ENOENT') return emptyLedger()
72
- throw error
73
- } finally {
74
- if (fd != null) fs.closeSync(fd)
75
- }
76
- const parsed = JSON.parse(raw)
77
- if (!exactKeys(parsed, ['version', 'leases', 'spawnTimes']) || parsed.version !== VERSION || !Array.isArray(parsed.leases) || !Array.isArray(parsed.spawnTimes)) throw new Error('invalid_admission_ledger')
78
- // Generous parse bounds let a future lower configured cap prune an older
79
- // snapshot, while still refusing attacker-sized or corrupted structures.
80
- if (parsed.leases.length > Math.max(64, limits.machineMax * 4) || parsed.spawnTimes.length > Math.max(128, limits.perWindowCap * 8)) throw new Error('unbounded_admission_ledger')
81
- if (!parsed.leases.every(validLease) || !parsed.spawnTimes.every(finiteInt)) throw new Error('invalid_admission_ledger')
82
- if (new Set(parsed.leases.map((lease) => lease.runKey)).size !== parsed.leases.length ||
83
- new Set(parsed.leases.map((lease) => lease.terminalKey)).size !== parsed.leases.length) throw new Error('duplicate_admission_ledger')
84
- return parsed
85
- }
86
-
87
- function pruneLedger (ledger, now, limits) {
88
- // Never truncate still-active leases merely because a later process starts
89
- // with a lower cap. Every active lease continues to consume capacity until
90
- // exact release or expiry.
91
- const leases = ledger.leases.filter((lease) => lease.expiresAt > now)
92
- const spawnTimes = ledger.spawnTimes
93
- .filter((at) => at > now - limits.windowMs && at <= now)
94
- .sort((a, b) => a - b)
95
- return {
96
- ledger: { version: VERSION, leases, spawnTimes },
97
- changed: leases.length !== ledger.leases.length || spawnTimes.length !== ledger.spawnTimes.length,
98
- }
99
- }
100
-
101
- let tempSerial = 0
102
- function atomicWriteLedger (file, ledger) {
103
- const serialized = `${JSON.stringify(ledger)}\n`
104
- if (Buffer.byteLength(serialized) > MAX_LEDGER_BYTES) throw new Error('unbounded_admission_ledger')
105
- const tmp = path.join(path.dirname(file), `.${path.basename(file)}.tmp.${process.pid}.${Date.now()}.${tempSerial++}`)
106
- let fd = null
107
- try {
108
- fd = fs.openSync(tmp, 'wx', 0o600)
109
- const bytes = Buffer.from(serialized)
110
- for (let offset = 0; offset < bytes.length;) {
111
- const written = fs.writeSync(fd, bytes, offset, bytes.length - offset)
112
- if (written <= 0) throw new Error('short_admission_write')
113
- offset += written
114
- }
115
- fs.fsyncSync(fd)
116
- fs.closeSync(fd)
117
- fd = null
118
- fs.renameSync(tmp, file)
119
- fs.chmodSync(file, 0o600)
120
- // Persist the directory entry where the host filesystem supports it.
121
- let dirFd = null
122
- try {
123
- dirFd = fs.openSync(path.dirname(file), 'r')
124
- fs.fsyncSync(dirFd)
125
- } catch {
126
- // Windows and some network filesystems reject directory fsync. The
127
- // replacement file itself was still fsynced before the atomic rename.
128
- } finally {
129
- if (dirFd != null) fs.closeSync(dirFd)
130
- }
131
- } catch (error) {
132
- try { if (fd != null) fs.closeSync(fd) } catch { /* best effort */ }
133
- try { fs.rmSync(tmp, { force: true }) } catch { /* best effort */ }
134
- throw error
135
- }
136
- }
137
-
138
- function ensureRoot (dir) {
139
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
140
- const stat = fs.lstatSync(dir)
141
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('invalid_pair_root')
142
- }
143
-
144
- function readLockOwner (lock) {
145
- const file = path.join(lock, 'owner')
146
- const stat = fs.lstatSync(file)
147
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return null
148
- const owner = JSON.parse(fs.readFileSync(file, 'utf8'))
149
- if (!exactKeys(owner, ['pid', 'token', 'createdAt']) ||
150
- !Number.isSafeInteger(owner.pid) || owner.pid < 1 ||
151
- typeof owner.token !== 'string' || !/^[a-f0-9-]{36}$/.test(owner.token) ||
152
- !finiteInt(owner.createdAt)) return null
153
- return owner
154
- }
155
-
156
- function ownerProcessState (pid) {
157
- try {
158
- process.kill(pid, 0)
159
- return 'live'
160
- } catch (error) {
161
- if (error?.code === 'ESRCH') return 'dead'
162
- // EPERM and every unknown platform error fail closed.
163
- return 'unknown'
164
- }
165
- }
166
-
167
- function recoverStaleLock (lock, staleLockMs, now) {
168
- const stat = fs.lstatSync(lock)
169
- if (!stat.isDirectory() || stat.isSymbolicLink() || now - stat.mtimeMs <= staleLockMs) return false
170
- let owner = null
171
- try { owner = readLockOwner(lock) } catch { return false }
172
- if (!owner || ownerProcessState(owner.pid) !== 'dead') return false
173
- // Rename is the recovery claim. Only the process that atomically moves this
174
- // exact stale directory may remove it; contenders see ENOENT and retry. A
175
- // live, permission-unknown, or malformed owner is never stolen.
176
- const stale = `${lock}.stale.${process.pid}.${randomUUID()}`
177
- fs.renameSync(lock, stale)
178
- fs.rmSync(stale, { recursive: true, force: true })
179
- return true
180
- }
181
-
182
- async function acquireLock ({ dir, lock }, { lockWaitMs = DEFAULT_LOCK_WAIT_MS, staleLockMs = DEFAULT_STALE_LOCK_MS, retryMs = DEFAULT_RETRY_MS } = {}) {
183
- ensureRoot(dir)
184
- const startedAt = Date.now()
185
- while (Date.now() - startedAt <= lockWaitMs) {
186
- const owner = Object.freeze({ pid: process.pid, token: randomUUID(), createdAt: Date.now() })
187
- try {
188
- fs.mkdirSync(lock, { mode: 0o700 })
189
- fs.writeFileSync(path.join(lock, 'owner'), JSON.stringify(owner), { flag: 'wx', mode: 0o600 })
190
- return owner
191
- } catch (error) {
192
- if (error?.code !== 'EEXIST') {
193
- // If mkdir succeeded but writing the ownership marker failed, remove
194
- // only the markerless lock we just created.
195
- try {
196
- if (fs.existsSync(lock) && !fs.existsSync(path.join(lock, 'owner'))) fs.rmdirSync(lock)
197
- } catch { /* a contender/stale recovery may own the path now */ }
198
- throw error
199
- }
200
- try {
201
- if (recoverStaleLock(lock, staleLockMs, Date.now())) continue
202
- } catch (recoveryError) {
203
- if (!['ENOENT', 'EEXIST'].includes(recoveryError?.code)) throw recoveryError
204
- }
205
- await sleep(Math.max(1, retryMs))
206
- }
207
- }
208
- throw new Error('admission_lock_timeout')
209
- }
210
-
211
- async function withLedgerLock (options, operation) {
212
- const paths = pathsFor(options?.root)
213
- let lockOwner = null
214
- try {
215
- lockOwner = await acquireLock(paths, options)
216
- return await operation(paths)
217
- } finally {
218
- if (lockOwner) {
219
- try {
220
- // A paused process may resume after its stale lock was recovered. Never
221
- // let that old holder remove the new holder's lock.
222
- const ownerFile = path.join(paths.lock, 'owner')
223
- const currentOwner = readLockOwner(paths.lock)
224
- if (currentOwner?.pid === lockOwner.pid && currentOwner?.token === lockOwner.token) {
225
- fs.rmSync(ownerFile, { force: true })
226
- fs.rmdirSync(paths.lock)
227
- }
228
- } catch { /* stale recovery/host cleanup owns it */ }
229
- }
230
- }
231
- }
232
-
233
- function invalidIdentity (runId, terminalId) {
234
- return typeof runId !== 'string' || !SAFE_ID.test(runId) || typeof terminalId !== 'string' || !SAFE_ID.test(terminalId)
235
- }
236
-
237
- // Acquire before the database claim and local spawn. `runId` may be the
238
- // deterministic occurrence key (schedule-id + next-run-at) because the database
239
- // UUID does not exist until claim_due_code_schedule succeeds.
240
- export async function acquireScheduledRunAdmission ({
241
- runId,
242
- terminalId,
243
- deadlineAt,
244
- now = Date.now(),
245
- root,
246
- limits,
247
- lockWaitMs,
248
- staleLockMs,
249
- retryMs,
250
- } = {}) {
251
- const policy = normalizeLimits(limits)
252
- if (invalidIdentity(runId, terminalId) || !finiteInt(now) || !finiteInt(deadlineAt) || deadlineAt <= now || deadlineAt - now > policy.maxLeaseMs) {
253
- return Object.freeze({ ok: false, code: 'invalid_admission_request' })
254
- }
255
- const token = randomUUID()
256
- const runKey = digest(runId)
257
- const terminalKey = digest(terminalId)
258
- try {
259
- return await withLedgerLock({ root, lockWaitMs, staleLockMs, retryMs }, ({ ledger: file }) => {
260
- const loaded = readLedger(file, policy)
261
- const { ledger, changed } = pruneLedger(loaded, now, policy)
262
- const sameRun = ledger.leases.find((lease) => lease.runKey === runKey)
263
- const sameTerminal = ledger.leases.find((lease) => lease.terminalKey === terminalKey)
264
- if (sameRun || sameTerminal) {
265
- if (changed) atomicWriteLedger(file, ledger)
266
- return Object.freeze({
267
- ok: false,
268
- code: sameRun && sameTerminal && sameRun === sameTerminal
269
- ? 'duplicate_lease'
270
- : sameRun ? 'duplicate_run' : 'duplicate_terminal',
271
- })
272
- }
273
- if (ledger.leases.length >= policy.machineMax) {
274
- if (changed) atomicWriteLedger(file, ledger)
275
- return Object.freeze({ ok: false, code: 'machine_cap' })
276
- }
277
- if (ledger.spawnTimes.length >= policy.perWindowCap) {
278
- if (changed) atomicWriteLedger(file, ledger)
279
- return Object.freeze({ ok: false, code: 'burst_cap', retryAt: ledger.spawnTimes[0] + policy.windowMs })
280
- }
281
- ledger.leases.push({ runKey, terminalKey, tokenHash: digest(token), acquiredAt: now, expiresAt: deadlineAt })
282
- ledger.spawnTimes.push(now)
283
- atomicWriteLedger(file, ledger)
284
- return Object.freeze({
285
- ok: true,
286
- code: 'admitted',
287
- lease: Object.freeze({ runId, terminalId, token, expiresAt: deadlineAt }),
288
- })
289
- })
290
- } catch {
291
- return Object.freeze({ ok: false, code: 'admission_unavailable' })
292
- }
293
- }
294
-
295
- // Exact-pair + one-time token release makes outcome/close cleanup idempotent
296
- // without allowing one room child to free another child's active lease.
297
- export async function releaseScheduledRunAdmission ({
298
- runId,
299
- terminalId,
300
- token,
301
- now = Date.now(),
302
- root,
303
- limits,
304
- lockWaitMs,
305
- staleLockMs,
306
- retryMs,
307
- } = {}) {
308
- const policy = normalizeLimits(limits)
309
- if (invalidIdentity(runId, terminalId) || typeof token !== 'string' || token.length < 16 || token.length > 128 || !finiteInt(now)) {
310
- return Object.freeze({ ok: false, code: 'invalid_release_request' })
311
- }
312
- const runKey = digest(runId)
313
- const terminalKey = digest(terminalId)
314
- const tokenHash = digest(token)
315
- try {
316
- return await withLedgerLock({ root, lockWaitMs, staleLockMs, retryMs }, ({ ledger: file }) => {
317
- const loaded = readLedger(file, policy)
318
- const { ledger, changed } = pruneLedger(loaded, now, policy)
319
- const index = ledger.leases.findIndex((lease) => lease.runKey === runKey && lease.terminalKey === terminalKey)
320
- if (index < 0) {
321
- if (ledger.leases.some((lease) => lease.runKey === runKey || lease.terminalKey === terminalKey)) {
322
- return Object.freeze({ ok: false, code: 'lease_mismatch' })
323
- }
324
- if (changed) atomicWriteLedger(file, ledger)
325
- return Object.freeze({ ok: true, code: 'already_released' })
326
- }
327
- if (ledger.leases[index].tokenHash !== tokenHash) return Object.freeze({ ok: false, code: 'lease_mismatch' })
328
- ledger.leases.splice(index, 1)
329
- atomicWriteLedger(file, ledger)
330
- return Object.freeze({ ok: true, code: 'released' })
331
- })
332
- } catch {
333
- return Object.freeze({ ok: false, code: 'admission_unavailable' })
334
- }
335
- }
336
-
337
- // Secret-free diagnostics/tests: counts and expiry only.
338
- export async function inspectScheduledRunAdmissions ({
339
- now = Date.now(),
340
- root,
341
- limits,
342
- lockWaitMs,
343
- staleLockMs,
344
- retryMs,
345
- } = {}) {
346
- const policy = normalizeLimits(limits)
347
- if (!finiteInt(now)) return Object.freeze({ ok: false, code: 'invalid_inspection_request' })
348
- try {
349
- return await withLedgerLock({ root, lockWaitMs, staleLockMs, retryMs }, ({ ledger: file }) => {
350
- const loaded = readLedger(file, policy)
351
- const { ledger, changed } = pruneLedger(loaded, now, policy)
352
- if (changed) atomicWriteLedger(file, ledger)
353
- return Object.freeze({
354
- ok: true,
355
- code: 'inspected',
356
- active: ledger.leases.length,
357
- burst: ledger.spawnTimes.length,
358
- nextExpiryAt: ledger.leases.length ? Math.min(...ledger.leases.map((lease) => lease.expiresAt)) : null,
359
- })
360
- })
361
- } catch {
362
- return Object.freeze({ ok: false, code: 'admission_unavailable' })
363
- }
364
- }
1
+ import e from"node:fs";import n from"node:os";import t from"node:path";import{createHash as r,randomUUID as s}from"node:crypto";import{SPAWN as i}from"./cross-terminal.mjs";const o=131072,a=/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,255}$/,c=e=>r("sha256").update(String(e)).digest("hex"),l=e=>new Promise(n=>setTimeout(n,e)),d=e=>Number.isSafeInteger(e)&&e>=0,u=(e,n)=>e&&"object"==typeof e&&Object.keys(e).sort().join("\0")===[...n].sort().join("\0");function m(e={}){const n=Number.isSafeInteger(e.machineMax)&&e.machineMax>0?e.machineMax:i.machineMax,t=Number.isSafeInteger(e.perWindowCap)&&e.perWindowCap>0?e.perWindowCap:i.perWindowCap,r=Number.isSafeInteger(e.windowMs)&&e.windowMs>0?e.windowMs:i.windowMs,s=Number.isSafeInteger(e.maxLeaseMs)&&e.maxLeaseMs>0?e.maxLeaseMs:186e4;return Object.freeze({machineMax:n,perWindowCap:t,windowMs:r,maxLeaseMs:s})}function f(e){return Boolean(u(e,["runKey","terminalKey","tokenHash","acquiredAt","expiresAt"])&&/^[a-f0-9]{64}$/.test(e.runKey)&&/^[a-f0-9]{64}$/.test(e.terminalKey)&&/^[a-f0-9]{64}$/.test(e.tokenHash)&&d(e.acquiredAt)&&d(e.expiresAt)&&e.expiresAt>e.acquiredAt)}function y(n,t){let r,s=null;try{s=e.openSync(n,e.constants.O_RDONLY|(e.constants.O_NOFOLLOW||0));const t=e.fstatSync(s);if(!t.isFile()||t.size>o)throw new Error("invalid_admission_ledger");r=e.readFileSync(s,"utf8")}catch(e){if("ENOENT"===e?.code)return{version:1,leases:[],spawnTimes:[]};throw e}finally{null!=s&&e.closeSync(s)}const i=JSON.parse(r);if(!u(i,["version","leases","spawnTimes"])||1!==i.version||!Array.isArray(i.leases)||!Array.isArray(i.spawnTimes))throw new Error("invalid_admission_ledger");if(i.leases.length>Math.max(64,4*t.machineMax)||i.spawnTimes.length>Math.max(128,8*t.perWindowCap))throw new Error("unbounded_admission_ledger");if(!i.leases.every(f)||!i.spawnTimes.every(d))throw new Error("invalid_admission_ledger");if(new Set(i.leases.map(e=>e.runKey)).size!==i.leases.length||new Set(i.leases.map(e=>e.terminalKey)).size!==i.leases.length)throw new Error("duplicate_admission_ledger");return i}function p(e,n,t){const r=e.leases.filter(e=>e.expiresAt>n),s=e.spawnTimes.filter(e=>e>n-t.windowMs&&e<=n).sort((e,n)=>e-n);return{ledger:{version:1,leases:r,spawnTimes:s},changed:r.length!==e.leases.length||s.length!==e.spawnTimes.length}}let w=0;function h(n,r){const s=`${JSON.stringify(r)}\n`;if(Buffer.byteLength(s)>o)throw new Error("unbounded_admission_ledger");const i=t.join(t.dirname(n),`.${t.basename(n)}.tmp.${process.pid}.${Date.now()}.${w++}`);let a=null;try{a=e.openSync(i,"wx",384);const r=Buffer.from(s);for(let n=0;n<r.length;){const t=e.writeSync(a,r,n,r.length-n);if(t<=0)throw new Error("short_admission_write");n+=t}e.fsyncSync(a),e.closeSync(a),a=null,e.renameSync(i,n),e.chmodSync(n,384);let o=null;try{o=e.openSync(t.dirname(n),"r"),e.fsyncSync(o)}catch{}finally{null!=o&&e.closeSync(o)}}catch(n){try{null!=a&&e.closeSync(a)}catch{}try{e.rmSync(i,{force:!0})}catch{}throw n}}function k(n){const r=t.join(n,"owner"),s=e.lstatSync(r);if(!s.isFile()||s.isSymbolicLink()||s.size>1024)return null;const i=JSON.parse(e.readFileSync(r,"utf8"));return u(i,["pid","token","createdAt"])&&Number.isSafeInteger(i.pid)&&!(i.pid<1)&&"string"==typeof i.token&&/^[a-f0-9-]{36}$/.test(i.token)&&d(i.createdAt)?i:null}function g(n,t,r){const i=e.lstatSync(n);if(!i.isDirectory()||i.isSymbolicLink()||r-i.mtimeMs<=t)return!1;let o=null;try{o=k(n)}catch{return!1}if(!o||"dead"!==function(e){try{return process.kill(e,0),"live"}catch(e){return"ESRCH"===e?.code?"dead":"unknown"}}(o.pid))return!1;const a=`${n}.stale.${process.pid}.${s()}`;return e.renameSync(n,a),e.rmSync(a,{recursive:!0,force:!0}),!0}async function S(r,i){const o=(e=>{const r=(e=>t.resolve(e||process.env.TP_PAIR_ROOT||t.join(n.homedir(),".thinkpool-pair")))(e);return{dir:r,ledger:t.join(r,"scheduled-run-admission.json"),lock:t.join(r,".scheduled-run-admission.lock")}})(r?.root);let a=null;try{return a=await async function({dir:n,lock:r},{lockWaitMs:i=1e3,staleLockMs:o=3e4,retryMs:a=12}={}){!function(n){e.mkdirSync(n,{recursive:!0,mode:448});const t=e.lstatSync(n);if(!t.isDirectory()||t.isSymbolicLink())throw new Error("invalid_pair_root")}(n);const c=Date.now();for(;Date.now()-c<=i;){const n=Object.freeze({pid:process.pid,token:s(),createdAt:Date.now()});try{return e.mkdirSync(r,{mode:448}),e.writeFileSync(t.join(r,"owner"),JSON.stringify(n),{flag:"wx",mode:384}),n}catch(n){if("EEXIST"!==n?.code){try{e.existsSync(r)&&!e.existsSync(t.join(r,"owner"))&&e.rmdirSync(r)}catch{}throw n}try{if(g(r,o,Date.now()))continue}catch(e){if(!["ENOENT","EEXIST"].includes(e?.code))throw e}await l(Math.max(1,a))}}throw new Error("admission_lock_timeout")}(o,r),await i(o)}finally{if(a)try{const n=t.join(o.lock,"owner"),r=k(o.lock);r?.pid===a.pid&&r?.token===a.token&&(e.rmSync(n,{force:!0}),e.rmdirSync(o.lock))}catch{}}}function M(e,n){return"string"!=typeof e||!a.test(e)||"string"!=typeof n||!a.test(n)}export async function acquireScheduledRunAdmission({runId:e,terminalId:n,deadlineAt:t,now:r=Date.now(),root:i,limits:o,lockWaitMs:a,staleLockMs:l,retryMs:u}={}){const f=m(o);if(M(e,n)||!d(r)||!d(t)||t<=r||t-r>f.maxLeaseMs)return Object.freeze({ok:!1,code:"invalid_admission_request"});const w=s(),k=c(e),g=c(n);try{return await S({root:i,lockWaitMs:a,staleLockMs:l,retryMs:u},({ledger:s})=>{const i=y(s,f),{ledger:o,changed:a}=p(i,r,f),l=o.leases.find(e=>e.runKey===k),d=o.leases.find(e=>e.terminalKey===g);return l||d?(a&&h(s,o),Object.freeze({ok:!1,code:l&&d&&l===d?"duplicate_lease":l?"duplicate_run":"duplicate_terminal"})):o.leases.length>=f.machineMax?(a&&h(s,o),Object.freeze({ok:!1,code:"machine_cap"})):o.spawnTimes.length>=f.perWindowCap?(a&&h(s,o),Object.freeze({ok:!1,code:"burst_cap",retryAt:o.spawnTimes[0]+f.windowMs})):(o.leases.push({runKey:k,terminalKey:g,tokenHash:c(w),acquiredAt:r,expiresAt:t}),o.spawnTimes.push(r),h(s,o),Object.freeze({ok:!0,code:"admitted",lease:Object.freeze({runId:e,terminalId:n,token:w,expiresAt:t})}))})}catch{return Object.freeze({ok:!1,code:"admission_unavailable"})}}export async function releaseScheduledRunAdmission({runId:e,terminalId:n,token:t,now:r=Date.now(),root:s,limits:i,lockWaitMs:o,staleLockMs:a,retryMs:l}={}){const u=m(i);if(M(e,n)||"string"!=typeof t||t.length<16||t.length>128||!d(r))return Object.freeze({ok:!1,code:"invalid_release_request"});const f=c(e),w=c(n),k=c(t);try{return await S({root:s,lockWaitMs:o,staleLockMs:a,retryMs:l},({ledger:e})=>{const n=y(e,u),{ledger:t,changed:s}=p(n,r,u),i=t.leases.findIndex(e=>e.runKey===f&&e.terminalKey===w);return i<0?t.leases.some(e=>e.runKey===f||e.terminalKey===w)?Object.freeze({ok:!1,code:"lease_mismatch"}):(s&&h(e,t),Object.freeze({ok:!0,code:"already_released"})):t.leases[i].tokenHash!==k?Object.freeze({ok:!1,code:"lease_mismatch"}):(t.leases.splice(i,1),h(e,t),Object.freeze({ok:!0,code:"released"}))})}catch{return Object.freeze({ok:!1,code:"admission_unavailable"})}}export async function inspectScheduledRunAdmissions({now:e=Date.now(),root:n,limits:t,lockWaitMs:r,staleLockMs:s,retryMs:i}={}){const o=m(t);if(!d(e))return Object.freeze({ok:!1,code:"invalid_inspection_request"});try{return await S({root:n,lockWaitMs:r,staleLockMs:s,retryMs:i},({ledger:n})=>{const t=y(n,o),{ledger:r,changed:s}=p(t,e,o);return s&&h(n,r),Object.freeze({ok:!0,code:"inspected",active:r.leases.length,burst:r.spawnTimes.length,nextExpiryAt:r.leases.length?Math.min(...r.leases.map(e=>e.expiresAt)):null})})}catch{return Object.freeze({ok:!1,code:"admission_unavailable"})}}