@arcanemachine/inter-agent-opencode 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.
package/dist/state.js ADDED
@@ -0,0 +1,1306 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { StateError } from "./errors.js";
5
+ export const LEASE_VERSION = 1;
6
+ export const LEASE_REFRESH_INTERVAL_MS = 10_000;
7
+ export const LEASE_EXPIRY_ALLOWANCE_MS = 30_000;
8
+ export const PREFERENCES_VERSION = 1;
9
+ export const OPENCODE_STATE_DIRNAME = "opencode";
10
+ const CONNECTION_FILENAME = "connection.json";
11
+ const PREFERENCES_FILENAME = "preferences.json";
12
+ const LEASE_LOCK_DIRNAME = ".connection.lock";
13
+ const LEASE_COORDINATOR_DIRNAME = ".connection.lock.coordinator";
14
+ const LEASE_COORDINATOR_RECOVERY_DIRNAME = ".connection.lock.coordinator.recovery";
15
+ const LEASE_COORDINATOR_RECLAIM_DIRNAME = ".reclaim";
16
+ const LEASE_COORDINATOR_RECLAIM_QUARANTINE_PREFIX = ".reclaim.quarantine.";
17
+ const LEASE_COORDINATOR_QUARANTINE_PREFIX = ".connection.lock.coordinator.quarantine.";
18
+ const LEASE_LOCK_QUARANTINE_PREFIX = ".connection.lock.quarantine.";
19
+ const LEASE_LOCK_MARKER = "owner";
20
+ const LEASE_LOCK_TIMEOUT_MS = 10_000;
21
+ const LEASE_LOCK_STALE_MS = 60_000;
22
+ const LEASE_COORDINATOR_STALE_MS = 60_000;
23
+ const LEASE_RECOVERY_ABANDONED_MS = 1_000;
24
+ const SCOPE_HASH_PATTERN = /^[a-f0-9]{64}$/;
25
+ export function isRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+ export function hashScope(value) {
29
+ return createHash("sha256").update(value, "utf8").digest("hex");
30
+ }
31
+ export function workspaceKey(canonicalWorkspacePath) {
32
+ return hashScope(canonicalWorkspacePath);
33
+ }
34
+ export function sessionKey(openCodeSessionID) {
35
+ return hashScope(openCodeSessionID);
36
+ }
37
+ export function canonicalWorkspacePath(workspacePath) {
38
+ try {
39
+ return realpathSync(workspacePath);
40
+ }
41
+ catch {
42
+ return resolve(workspacePath);
43
+ }
44
+ }
45
+ export function generateOwnerToken() {
46
+ return randomBytes(32).toString("base64url");
47
+ }
48
+ export function assertScopeHashes(workspaceHash, sessionHash) {
49
+ if (!SCOPE_HASH_PATTERN.test(workspaceHash))
50
+ throw new StateError("invalid workspace scope hash");
51
+ if (!SCOPE_HASH_PATTERN.test(sessionHash))
52
+ throw new StateError("invalid session scope hash");
53
+ }
54
+ function resolveReal(path) {
55
+ try {
56
+ return realpathSync(path);
57
+ }
58
+ catch {
59
+ return resolve(path);
60
+ }
61
+ }
62
+ function validateDirectoryPath(path) {
63
+ const absolute = resolve(path);
64
+ let current = dirname(absolute);
65
+ const components = [];
66
+ let child = absolute;
67
+ while (child !== current) {
68
+ components.unshift(basename(child));
69
+ child = current;
70
+ current = dirname(current);
71
+ }
72
+ components.unshift(child);
73
+ let candidate = components[0] ?? absolute;
74
+ for (const component of components.slice(1)) {
75
+ candidate = join(candidate, component);
76
+ try {
77
+ const stat = lstatSync(candidate);
78
+ if (stat.isSymbolicLink() || !stat.isDirectory())
79
+ throw new StateError("session state path is not contained within the data directory");
80
+ }
81
+ catch (error) {
82
+ if (error instanceof StateError)
83
+ throw error;
84
+ if (error.code === "ENOENT")
85
+ break;
86
+ throw new StateError("unable to inspect inter-agent state path");
87
+ }
88
+ }
89
+ }
90
+ function ensureDirectoryTree(path) {
91
+ const absolute = resolve(path);
92
+ let current = dirname(absolute);
93
+ const components = [];
94
+ let child = absolute;
95
+ while (child !== current) {
96
+ components.unshift(basename(child));
97
+ child = current;
98
+ current = dirname(current);
99
+ }
100
+ components.unshift(child);
101
+ let candidate = components[0] ?? absolute;
102
+ for (const component of components.slice(1)) {
103
+ candidate = join(candidate, component);
104
+ let created = false;
105
+ try {
106
+ mkdirSync(candidate, { mode: 0o700 });
107
+ created = true;
108
+ }
109
+ catch (error) {
110
+ const code = error.code;
111
+ if (code !== "EEXIST")
112
+ throw new StateError("unable to create session state directory");
113
+ }
114
+ try {
115
+ const stat = lstatSync(candidate);
116
+ if (stat.isSymbolicLink() || !stat.isDirectory())
117
+ throw new StateError("session state path is not contained within the data directory");
118
+ if (created)
119
+ chmodSync(candidate, 0o700);
120
+ }
121
+ catch (error) {
122
+ if (error instanceof StateError)
123
+ throw error;
124
+ throw new StateError("unable to create session state directory");
125
+ }
126
+ }
127
+ }
128
+ function stateDir(dataDir, workspaceHash, sessionHash) {
129
+ return join(dataDir, OPENCODE_STATE_DIRNAME, "workspaces", workspaceHash, "sessions", sessionHash);
130
+ }
131
+ export function sessionDir(dataDir, workspaceHash, sessionHash) {
132
+ assertScopeHashes(workspaceHash, sessionHash);
133
+ validateDirectoryPath(dataDir);
134
+ const realData = resolveReal(dataDir);
135
+ const dir = stateDir(realData, workspaceHash, sessionHash);
136
+ validateDirectoryPath(dir);
137
+ return dir;
138
+ }
139
+ export function ensureSessionDir(dataDir, workspaceHash, sessionHash) {
140
+ assertScopeHashes(workspaceHash, sessionHash);
141
+ validateDirectoryPath(dataDir);
142
+ try {
143
+ ensureDirectoryTree(dataDir);
144
+ chmodSync(resolve(dataDir), 0o700);
145
+ }
146
+ catch (error) {
147
+ if (error instanceof StateError)
148
+ throw error;
149
+ throw new StateError("unable to prepare inter-agent state directory");
150
+ }
151
+ const realData = resolveReal(dataDir);
152
+ const expected = stateDir(realData, workspaceHash, sessionHash);
153
+ validateDirectoryPath(expected);
154
+ try {
155
+ ensureDirectoryTree(expected);
156
+ chmodSync(expected, 0o700);
157
+ }
158
+ catch (error) {
159
+ if (error instanceof StateError)
160
+ throw error;
161
+ throw new StateError("unable to create session state directory");
162
+ }
163
+ return expected;
164
+ }
165
+ export function ensurePrivateDir(dir) {
166
+ try {
167
+ const existing = lstatSync(dir);
168
+ if (existing.isSymbolicLink() || !existing.isDirectory())
169
+ throw new StateError("state directory is not a private directory");
170
+ }
171
+ catch (error) {
172
+ if (error instanceof StateError)
173
+ throw error;
174
+ try {
175
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
176
+ }
177
+ catch {
178
+ throw new StateError("unable to create private state directory");
179
+ }
180
+ }
181
+ try {
182
+ chmodSync(dir, 0o700);
183
+ }
184
+ catch {
185
+ // Windows may not support POSIX mode bits.
186
+ }
187
+ }
188
+ export function readJsonFile(path) {
189
+ const stat = lstatSync(path);
190
+ if (stat.isSymbolicLink() || !stat.isFile())
191
+ throw new StateError("state file is not a regular file");
192
+ let parsed;
193
+ try {
194
+ parsed = JSON.parse(readFileSync(path, "utf8"));
195
+ }
196
+ catch {
197
+ throw new StateError("state file is not valid JSON");
198
+ }
199
+ if (!isRecord(parsed))
200
+ throw new StateError("state file must contain a JSON object");
201
+ return parsed;
202
+ }
203
+ export function stageJsonWrite(path, value) {
204
+ const temp = join(dirname(path), `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
205
+ const fd = openSync(temp, "wx", 0o600);
206
+ try {
207
+ writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, {
208
+ encoding: "utf8",
209
+ });
210
+ }
211
+ catch {
212
+ try {
213
+ closeSync(fd);
214
+ }
215
+ catch {
216
+ // Best effort close of the failed write.
217
+ }
218
+ try {
219
+ unlinkSync(temp);
220
+ }
221
+ catch {
222
+ // Best effort cleanup of this process's private temporary file.
223
+ }
224
+ throw new StateError("unable to write state file");
225
+ }
226
+ try {
227
+ closeSync(fd);
228
+ }
229
+ catch {
230
+ // Best effort close; the file content is already durable.
231
+ }
232
+ try {
233
+ chmodSync(temp, 0o600);
234
+ }
235
+ catch {
236
+ // Windows may not support POSIX mode bits.
237
+ }
238
+ return temp;
239
+ }
240
+ export function commitJsonWrite(tempPath, path) {
241
+ try {
242
+ renameSync(tempPath, path);
243
+ }
244
+ catch {
245
+ try {
246
+ unlinkSync(tempPath);
247
+ }
248
+ catch {
249
+ // Best effort cleanup of this process's private temporary file.
250
+ }
251
+ throw new StateError("unable to replace state file");
252
+ }
253
+ try {
254
+ chmodSync(path, 0o600);
255
+ }
256
+ catch {
257
+ // Windows may not support POSIX mode bits.
258
+ }
259
+ }
260
+ export function writeJsonAtomic(path, value) {
261
+ ensurePrivateDir(dirname(path));
262
+ const temp = stageJsonWrite(path, value);
263
+ commitJsonWrite(temp, path);
264
+ }
265
+ function sleepForLeaseLock() {
266
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
267
+ }
268
+ function isTransientLockRace(error) {
269
+ const code = error.code;
270
+ if (code === "ENOENT" ||
271
+ code === "ENOTDIR" ||
272
+ code === "ENOTEMPTY" ||
273
+ code === "EEXIST" ||
274
+ code === "EBUSY")
275
+ return true;
276
+ if (error instanceof StateError)
277
+ return (error.message === "connection lease is busy; retry later" ||
278
+ error.message === "connection lease recovery owner changed" ||
279
+ /^unable to (?:acquire|claim(?: stale)?|reclaim|recover|remove|release|inspect) connection lease (?:lock|recovery|coordinator)(?: owner)?(?:$|:)/.test(error.message));
280
+ return false;
281
+ }
282
+ function removeLockDirectory(path) {
283
+ try {
284
+ const stat = lstatSync(path);
285
+ if (stat.isSymbolicLink() || !stat.isDirectory())
286
+ throw new StateError("connection lease lock is not a private directory");
287
+ try {
288
+ unlinkSync(join(path, LEASE_LOCK_MARKER));
289
+ }
290
+ catch (error) {
291
+ if (error.code !== "ENOENT") {
292
+ if (isTransientLockRace(error))
293
+ throw new StateError("connection lease is busy; retry later");
294
+ throw new StateError("unable to recover connection lease lock");
295
+ }
296
+ }
297
+ try {
298
+ rmdirSync(path);
299
+ }
300
+ catch (error) {
301
+ if (isTransientLockRace(error))
302
+ throw new StateError("connection lease is busy; retry later");
303
+ throw new StateError("unable to recover connection lease lock");
304
+ }
305
+ }
306
+ catch (error) {
307
+ if (error instanceof StateError)
308
+ throw error;
309
+ if (error.code === "ENOENT")
310
+ return;
311
+ if (isTransientLockRace(error))
312
+ throw new StateError("connection lease is busy; retry later");
313
+ throw new StateError("unable to recover connection lease lock");
314
+ }
315
+ }
316
+ function readLockMarker(path) {
317
+ try {
318
+ return readFileSync(join(path, LEASE_LOCK_MARKER), "utf8");
319
+ }
320
+ catch (error) {
321
+ if (isTransientLockRace(error))
322
+ return undefined;
323
+ throw new StateError("unable to inspect connection lease lock");
324
+ }
325
+ }
326
+ function restoreLeaseLockGeneration(quarantine, lockPath) {
327
+ try {
328
+ renameSync(quarantine, lockPath);
329
+ }
330
+ catch (error) {
331
+ const code = error.code;
332
+ if (code === "EEXIST" || code === "ENOENT")
333
+ return;
334
+ throw new StateError("unable to preserve connection lease lock");
335
+ }
336
+ }
337
+ function removeOwnedLeaseLock(dir, lockPath, token) {
338
+ const coordinatorRelease = acquireCoordinator(dir);
339
+ let failure;
340
+ try {
341
+ const marker = readLockMarker(lockPath);
342
+ if (marker === undefined) {
343
+ if (!existsSync(lockPath))
344
+ return;
345
+ throw new StateError("connection lease lock owner changed");
346
+ }
347
+ if (marker !== token)
348
+ throw new StateError("connection lease lock owner changed");
349
+ const quarantine = join(dir, `${LEASE_LOCK_QUARANTINE_PREFIX}${generateOwnerToken()}`);
350
+ let renamed = false;
351
+ try {
352
+ renameSync(lockPath, quarantine);
353
+ renamed = true;
354
+ }
355
+ catch (error) {
356
+ if (error.code !== "ENOENT")
357
+ throw new StateError("unable to release connection lease lock");
358
+ }
359
+ if (renamed) {
360
+ const quarantinedMarker = readLockMarker(quarantine);
361
+ if (quarantinedMarker !== token) {
362
+ restoreLeaseLockGeneration(quarantine, lockPath);
363
+ throw new StateError("connection lease lock owner changed");
364
+ }
365
+ removeLockDirectory(quarantine);
366
+ }
367
+ }
368
+ catch (error) {
369
+ failure = error;
370
+ }
371
+ try {
372
+ coordinatorRelease();
373
+ }
374
+ catch (error) {
375
+ if (failure === undefined)
376
+ failure = error;
377
+ }
378
+ if (failure !== undefined)
379
+ throw failure;
380
+ }
381
+ function removeRecoveryDirectory(path, reclaimToken) {
382
+ let entries;
383
+ try {
384
+ entries = readdirSync(path);
385
+ }
386
+ catch (error) {
387
+ if (isTransientLockRace(error))
388
+ return;
389
+ throw error;
390
+ }
391
+ for (const entry of entries) {
392
+ if (!entry.startsWith(LEASE_COORDINATOR_RECLAIM_QUARANTINE_PREFIX))
393
+ continue;
394
+ const quarantine = join(path, entry);
395
+ let stat;
396
+ try {
397
+ stat = lstatSync(quarantine);
398
+ }
399
+ catch (error) {
400
+ if (isTransientLockRace(error))
401
+ continue;
402
+ throw error;
403
+ }
404
+ if (Date.now() - stat.mtimeMs <= LEASE_COORDINATOR_STALE_MS)
405
+ throw new StateError("connection lease is busy; retry later");
406
+ removeLockDirectory(quarantine);
407
+ }
408
+ const reclaimPath = join(path, LEASE_COORDINATOR_RECLAIM_DIRNAME);
409
+ try {
410
+ const reclaimMarker = readLockMarker(reclaimPath);
411
+ if (reclaimToken !== undefined && reclaimMarker !== reclaimToken)
412
+ throw new StateError("connection lease recovery owner changed");
413
+ if (reclaimMarker !== undefined)
414
+ removeLockDirectory(reclaimPath);
415
+ }
416
+ catch (error) {
417
+ if (error instanceof StateError)
418
+ throw error;
419
+ if (error.code !== "ENOENT")
420
+ throw new StateError("unable to remove connection lease recovery owner");
421
+ }
422
+ try {
423
+ removeLockDirectory(path);
424
+ }
425
+ catch (error) {
426
+ if (error instanceof StateError) {
427
+ if (error.message.includes("unable to recover connection lease lock"))
428
+ throw new StateError("connection lease is busy; retry later");
429
+ throw error;
430
+ }
431
+ throw error;
432
+ }
433
+ }
434
+ function removeOwnedRecoveryClaim(path, token) {
435
+ const reclaimPath = join(path, LEASE_COORDINATOR_RECLAIM_DIRNAME);
436
+ try {
437
+ if (readLockMarker(reclaimPath) === token)
438
+ removeLockDirectory(reclaimPath);
439
+ }
440
+ catch (error) {
441
+ if (!isTransientLockRace(error))
442
+ throw error;
443
+ }
444
+ }
445
+ function hasFreshRecoveryClaimQuarantine(recoveryPath, staleThreshold = LEASE_COORDINATOR_STALE_MS) {
446
+ let names;
447
+ try {
448
+ names = readdirSync(recoveryPath);
449
+ }
450
+ catch (error) {
451
+ if (isTransientLockRace(error))
452
+ return false;
453
+ throw new StateError("unable to inspect connection lease recovery claims");
454
+ }
455
+ let fresh = false;
456
+ for (const name of names) {
457
+ if (!name.startsWith(LEASE_COORDINATOR_RECLAIM_QUARANTINE_PREFIX))
458
+ continue;
459
+ const quarantine = join(recoveryPath, name);
460
+ let stat;
461
+ try {
462
+ stat = lstatSync(quarantine);
463
+ }
464
+ catch (error) {
465
+ if (isTransientLockRace(error))
466
+ continue;
467
+ throw new StateError("unable to inspect connection lease recovery claims");
468
+ }
469
+ if (stat.isSymbolicLink() || !stat.isDirectory())
470
+ throw new StateError("connection lease recovery claim is not private");
471
+ if (Date.now() - stat.mtimeMs > staleThreshold) {
472
+ try {
473
+ removeLockDirectory(quarantine);
474
+ }
475
+ catch (error) {
476
+ if (!isTransientLockRace(error))
477
+ throw error;
478
+ }
479
+ }
480
+ else
481
+ fresh = true;
482
+ }
483
+ return fresh;
484
+ }
485
+ function reclaimStaleRecoveryClaim(recoveryPath, observedMarker, observedMtime, staleThreshold = LEASE_COORDINATOR_STALE_MS) {
486
+ if (hasFreshRecoveryClaimQuarantine(recoveryPath, staleThreshold))
487
+ return undefined;
488
+ const reclaimPath = join(recoveryPath, LEASE_COORDINATOR_RECLAIM_DIRNAME);
489
+ const token = generateOwnerToken();
490
+ const quarantine = join(recoveryPath, `${LEASE_COORDINATOR_RECLAIM_QUARANTINE_PREFIX}${token}`);
491
+ let moved = false;
492
+ try {
493
+ const current = lstatSync(reclaimPath);
494
+ if (current.isSymbolicLink() || !current.isDirectory())
495
+ throw new StateError("connection lease recovery claim is not private");
496
+ const currentMarker = readLockMarker(reclaimPath);
497
+ if (observedMarker === "" && currentMarker === undefined) {
498
+ rmdirSync(reclaimPath);
499
+ mkdirSync(reclaimPath, { mode: 0o700 });
500
+ writeFileSync(join(reclaimPath, LEASE_LOCK_MARKER), token, {
501
+ encoding: "utf8",
502
+ flag: "wx",
503
+ mode: 0o600,
504
+ });
505
+ return token;
506
+ }
507
+ if (currentMarker !== observedMarker ||
508
+ Date.now() - observedMtime <= staleThreshold)
509
+ return undefined;
510
+ try {
511
+ renameSync(reclaimPath, quarantine);
512
+ moved = true;
513
+ }
514
+ catch (error) {
515
+ if (error.code === "ENOENT")
516
+ return undefined;
517
+ throw new StateError("unable to reclaim connection lease recovery claim");
518
+ }
519
+ if (readLockMarker(quarantine) !== observedMarker) {
520
+ renameSync(quarantine, reclaimPath);
521
+ moved = false;
522
+ return undefined;
523
+ }
524
+ removeLockDirectory(quarantine);
525
+ mkdirSync(reclaimPath, { mode: 0o700 });
526
+ writeFileSync(join(reclaimPath, LEASE_LOCK_MARKER), token, {
527
+ encoding: "utf8",
528
+ flag: "wx",
529
+ mode: 0o600,
530
+ });
531
+ return token;
532
+ }
533
+ finally {
534
+ if (moved) {
535
+ try {
536
+ removeLockDirectory(quarantine);
537
+ }
538
+ catch (error) {
539
+ if (error.code !== "ENOENT")
540
+ throw error;
541
+ }
542
+ }
543
+ }
544
+ }
545
+ function createRecoveryGuard(dir) {
546
+ const recoveryPath = join(dir, LEASE_COORDINATOR_RECOVERY_DIRNAME);
547
+ const markerPath = join(recoveryPath, LEASE_LOCK_MARKER);
548
+ const token = generateOwnerToken();
549
+ let created = false;
550
+ try {
551
+ mkdirSync(recoveryPath, { mode: 0o700 });
552
+ created = true;
553
+ writeFileSync(markerPath, token, {
554
+ encoding: "utf8",
555
+ flag: "wx",
556
+ mode: 0o600,
557
+ });
558
+ }
559
+ catch (error) {
560
+ if (created && readLockMarker(recoveryPath) === token)
561
+ removeRecoveryDirectory(recoveryPath, token);
562
+ if (error.code === "EEXIST")
563
+ return undefined;
564
+ throw new StateError("unable to acquire connection lease recovery guard");
565
+ }
566
+ let released = false;
567
+ return () => {
568
+ if (released)
569
+ return;
570
+ released = true;
571
+ const reclaimPath = join(recoveryPath, LEASE_COORDINATOR_RECLAIM_DIRNAME);
572
+ let reclaimToken = generateOwnerToken();
573
+ let claimed = false;
574
+ const claimStartedAt = Date.now();
575
+ while (!claimed) {
576
+ let claimCreated = false;
577
+ try {
578
+ mkdirSync(reclaimPath, { mode: 0o700 });
579
+ claimCreated = true;
580
+ writeFileSync(join(reclaimPath, LEASE_LOCK_MARKER), reclaimToken, {
581
+ encoding: "utf8",
582
+ flag: "wx",
583
+ mode: 0o600,
584
+ });
585
+ claimed = true;
586
+ }
587
+ catch (error) {
588
+ if (claimCreated) {
589
+ try {
590
+ rmdirSync(reclaimPath);
591
+ }
592
+ catch {
593
+ // Best effort cleanup of this process's empty claim.
594
+ }
595
+ }
596
+ const code = error.code;
597
+ if (code !== "EEXIST") {
598
+ if (isTransientLockRace(error))
599
+ return;
600
+ throw new StateError("unable to release connection lease recovery guard");
601
+ }
602
+ if (readLockMarker(recoveryPath) !== token)
603
+ return;
604
+ try {
605
+ const claimMarker = readLockMarker(reclaimPath);
606
+ if (claimMarker === undefined) {
607
+ rmdirSync(reclaimPath);
608
+ continue;
609
+ }
610
+ const claimStat = lstatSync(reclaimPath);
611
+ if (Date.now() - Number(claimStat.mtimeMs) >
612
+ LEASE_COORDINATOR_STALE_MS) {
613
+ const replacement = reclaimStaleRecoveryClaim(recoveryPath, claimMarker, Number(claimStat.mtimeMs));
614
+ if (replacement) {
615
+ reclaimToken = replacement;
616
+ claimed = true;
617
+ break;
618
+ }
619
+ }
620
+ }
621
+ catch (claimError) {
622
+ if (!isTransientLockRace(claimError))
623
+ throw claimError;
624
+ }
625
+ if (Date.now() - claimStartedAt >= LEASE_LOCK_TIMEOUT_MS)
626
+ return;
627
+ sleepForLeaseLock();
628
+ }
629
+ }
630
+ const quarantine = join(dir, `${LEASE_COORDINATOR_QUARANTINE_PREFIX}${reclaimToken}`);
631
+ try {
632
+ if (readLockMarker(recoveryPath) !== token)
633
+ return;
634
+ try {
635
+ renameSync(recoveryPath, quarantine);
636
+ }
637
+ catch (error) {
638
+ if (error.code === "ENOENT")
639
+ return;
640
+ throw new StateError("unable to release connection lease recovery guard");
641
+ }
642
+ if (readLockMarker(quarantine) === token &&
643
+ readLockMarker(join(quarantine, LEASE_COORDINATOR_RECLAIM_DIRNAME)) ===
644
+ reclaimToken) {
645
+ try {
646
+ removeRecoveryDirectory(quarantine, reclaimToken);
647
+ }
648
+ catch (error) {
649
+ if (!isTransientLockRace(error))
650
+ throw error;
651
+ }
652
+ }
653
+ else
654
+ renameSync(quarantine, recoveryPath);
655
+ }
656
+ finally {
657
+ if (claimed)
658
+ removeOwnedRecoveryClaim(recoveryPath, reclaimToken);
659
+ }
660
+ };
661
+ }
662
+ function reclaimStaleRecoveryGuard(dir, observedMarker, observedMtime, staleThreshold = LEASE_COORDINATOR_STALE_MS) {
663
+ const recoveryPath = join(dir, LEASE_COORDINATOR_RECOVERY_DIRNAME);
664
+ const reclaimPath = join(recoveryPath, LEASE_COORDINATOR_RECLAIM_DIRNAME);
665
+ let reclaimToken = generateOwnerToken();
666
+ let claimed = false;
667
+ try {
668
+ let existing;
669
+ try {
670
+ existing = lstatSync(reclaimPath);
671
+ }
672
+ catch (error) {
673
+ if (error.code !== "ENOENT")
674
+ throw error;
675
+ }
676
+ if (existing) {
677
+ if (existing.isSymbolicLink() || !existing.isDirectory())
678
+ throw new StateError("connection lease recovery claim is not private");
679
+ const existingMarker = readLockMarker(reclaimPath);
680
+ if (existingMarker !== undefined &&
681
+ Date.now() - Number(existing.mtimeMs) <= staleThreshold)
682
+ return undefined;
683
+ const replacement = reclaimStaleRecoveryClaim(recoveryPath, existingMarker ?? "", Number(existing.mtimeMs), staleThreshold);
684
+ if (!replacement)
685
+ return undefined;
686
+ reclaimToken = replacement;
687
+ claimed = true;
688
+ }
689
+ else {
690
+ mkdirSync(reclaimPath, { mode: 0o700 });
691
+ claimed = true;
692
+ writeFileSync(join(reclaimPath, LEASE_LOCK_MARKER), reclaimToken, {
693
+ encoding: "utf8",
694
+ flag: "wx",
695
+ mode: 0o600,
696
+ });
697
+ }
698
+ }
699
+ catch (error) {
700
+ if (isTransientLockRace(error))
701
+ return undefined;
702
+ throw new StateError("unable to claim stale connection lease recovery");
703
+ }
704
+ const quarantine = join(dir, `${LEASE_COORDINATOR_QUARANTINE_PREFIX}${reclaimToken}`);
705
+ try {
706
+ let current;
707
+ try {
708
+ current = lstatSync(recoveryPath);
709
+ }
710
+ catch (error) {
711
+ if (error.code === "ENOENT")
712
+ return undefined;
713
+ throw error;
714
+ }
715
+ if (current.isSymbolicLink() || !current.isDirectory())
716
+ throw new StateError("connection lease recovery guard is not private");
717
+ if (readLockMarker(recoveryPath) !== observedMarker ||
718
+ Date.now() - observedMtime <= staleThreshold)
719
+ return undefined;
720
+ try {
721
+ renameSync(recoveryPath, quarantine);
722
+ }
723
+ catch (error) {
724
+ if (error.code === "ENOENT")
725
+ return undefined;
726
+ throw new StateError("unable to reclaim connection lease recovery guard");
727
+ }
728
+ if (readLockMarker(quarantine) !== observedMarker ||
729
+ readLockMarker(join(quarantine, LEASE_COORDINATOR_RECLAIM_DIRNAME)) !==
730
+ reclaimToken) {
731
+ renameSync(quarantine, recoveryPath);
732
+ return undefined;
733
+ }
734
+ try {
735
+ removeRecoveryDirectory(quarantine, reclaimToken);
736
+ }
737
+ catch (error) {
738
+ if (!isTransientLockRace(error))
739
+ throw error;
740
+ return undefined;
741
+ }
742
+ return createRecoveryGuard(dir);
743
+ }
744
+ finally {
745
+ if (claimed)
746
+ removeOwnedRecoveryClaim(recoveryPath, reclaimToken);
747
+ }
748
+ }
749
+ function acquireCoordinatorRecovery(dir) {
750
+ const recoveryPath = join(dir, LEASE_COORDINATOR_RECOVERY_DIRNAME);
751
+ const startedAt = Date.now();
752
+ while (true) {
753
+ const created = createRecoveryGuard(dir);
754
+ if (created)
755
+ return created;
756
+ try {
757
+ const stat = lstatSync(recoveryPath);
758
+ if (stat.isSymbolicLink() || !stat.isDirectory())
759
+ throw new StateError("connection lease recovery guard is not private");
760
+ let coordinatorStale = false;
761
+ try {
762
+ const coordinator = lstatSync(join(dir, LEASE_COORDINATOR_DIRNAME));
763
+ coordinatorStale =
764
+ Date.now() - Number(coordinator.mtimeMs) > LEASE_COORDINATOR_STALE_MS;
765
+ }
766
+ catch (error) {
767
+ if (error.code !== "ENOENT")
768
+ throw error;
769
+ coordinatorStale = true;
770
+ }
771
+ if (Date.now() - stat.mtimeMs > LEASE_COORDINATOR_STALE_MS ||
772
+ (coordinatorStale &&
773
+ Date.now() - stat.mtimeMs > LEASE_RECOVERY_ABANDONED_MS)) {
774
+ const replacement = reclaimStaleRecoveryGuard(dir, readLockMarker(recoveryPath), stat.mtimeMs, coordinatorStale &&
775
+ Date.now() - Number(stat.mtimeMs) > LEASE_RECOVERY_ABANDONED_MS
776
+ ? LEASE_RECOVERY_ABANDONED_MS
777
+ : LEASE_COORDINATOR_STALE_MS);
778
+ if (replacement)
779
+ return replacement;
780
+ }
781
+ }
782
+ catch (error) {
783
+ if (error instanceof StateError) {
784
+ if (!isTransientLockRace(error))
785
+ throw error;
786
+ }
787
+ else if (!isTransientLockRace(error)) {
788
+ throw new StateError("unable to inspect connection lease recovery guard");
789
+ }
790
+ }
791
+ if (Date.now() - startedAt >= LEASE_LOCK_TIMEOUT_MS)
792
+ throw new StateError("connection lease is busy; retry later");
793
+ sleepForLeaseLock();
794
+ }
795
+ }
796
+ function createCoordinator(dir, allowRecoveryGuard) {
797
+ const coordinatorPath = join(dir, LEASE_COORDINATOR_DIRNAME);
798
+ const markerPath = join(coordinatorPath, LEASE_LOCK_MARKER);
799
+ if (!allowRecoveryGuard) {
800
+ try {
801
+ const recovery = lstatSync(join(dir, LEASE_COORDINATOR_RECOVERY_DIRNAME));
802
+ if (recovery.isSymbolicLink() || !recovery.isDirectory())
803
+ throw new StateError("connection lease recovery guard is not private");
804
+ return undefined;
805
+ }
806
+ catch (error) {
807
+ if (error instanceof StateError)
808
+ throw error;
809
+ const code = error.code;
810
+ if (code !== "ENOENT" && code !== "ENOTDIR")
811
+ throw new StateError("unable to inspect connection lease recovery guard");
812
+ }
813
+ }
814
+ const token = generateOwnerToken();
815
+ let created = false;
816
+ try {
817
+ mkdirSync(coordinatorPath, { mode: 0o700 });
818
+ created = true;
819
+ writeFileSync(markerPath, token, {
820
+ encoding: "utf8",
821
+ flag: "wx",
822
+ mode: 0o600,
823
+ });
824
+ }
825
+ catch (error) {
826
+ if (created) {
827
+ try {
828
+ unlinkSync(markerPath);
829
+ }
830
+ catch {
831
+ // Best effort cleanup of this process's private marker.
832
+ }
833
+ try {
834
+ rmdirSync(coordinatorPath);
835
+ }
836
+ catch {
837
+ // Best effort cleanup of this process's private coordinator.
838
+ }
839
+ throw new StateError("unable to acquire connection lease coordinator");
840
+ }
841
+ if (error.code === "EEXIST")
842
+ return undefined;
843
+ throw new StateError("unable to acquire connection lease coordinator");
844
+ }
845
+ let released = false;
846
+ return () => {
847
+ if (released)
848
+ return;
849
+ released = true;
850
+ const recoveryRelease = acquireCoordinatorRecovery(dir);
851
+ try {
852
+ const marker = readLockMarker(coordinatorPath);
853
+ if (marker !== token)
854
+ return;
855
+ removeLockDirectory(coordinatorPath);
856
+ }
857
+ finally {
858
+ recoveryRelease();
859
+ }
860
+ };
861
+ }
862
+ function acquireCoordinator(dir) {
863
+ const coordinatorPath = join(dir, LEASE_COORDINATOR_DIRNAME);
864
+ const startedAt = Date.now();
865
+ while (true) {
866
+ const created = createCoordinator(dir, false);
867
+ if (created)
868
+ return created;
869
+ try {
870
+ const stat = lstatSync(coordinatorPath);
871
+ if (stat.isSymbolicLink() || !stat.isDirectory())
872
+ throw new StateError("connection lease coordinator is not private");
873
+ const observedMarker = readLockMarker(coordinatorPath);
874
+ if (Date.now() - stat.mtimeMs > LEASE_COORDINATOR_STALE_MS) {
875
+ const recoveryRelease = acquireCoordinatorRecovery(dir);
876
+ try {
877
+ let currentStat;
878
+ try {
879
+ currentStat = lstatSync(coordinatorPath);
880
+ }
881
+ catch (error) {
882
+ if (error.code === "ENOENT")
883
+ continue;
884
+ throw error;
885
+ }
886
+ if (currentStat.isSymbolicLink() || !currentStat.isDirectory())
887
+ throw new StateError("connection lease coordinator is not private");
888
+ const currentMarker = readLockMarker(coordinatorPath);
889
+ if (currentMarker !== observedMarker ||
890
+ Date.now() - currentStat.mtimeMs <= LEASE_COORDINATOR_STALE_MS)
891
+ continue;
892
+ removeLockDirectory(coordinatorPath);
893
+ const replacement = createCoordinator(dir, true);
894
+ if (replacement)
895
+ return replacement;
896
+ }
897
+ finally {
898
+ recoveryRelease();
899
+ }
900
+ continue;
901
+ }
902
+ }
903
+ catch (error) {
904
+ if (error instanceof StateError) {
905
+ if (!isTransientLockRace(error))
906
+ throw error;
907
+ }
908
+ else if (error.code === "ENOENT") {
909
+ try {
910
+ const recoveryPath = join(dir, LEASE_COORDINATOR_RECOVERY_DIRNAME);
911
+ const recoveryStat = lstatSync(recoveryPath);
912
+ const recoveryAge = Date.now() - recoveryStat.mtimeMs;
913
+ if (recoveryAge > LEASE_COORDINATOR_STALE_MS) {
914
+ const recovery = acquireCoordinatorRecovery(dir);
915
+ recovery();
916
+ }
917
+ }
918
+ catch (recoveryError) {
919
+ if (!isTransientLockRace(recoveryError))
920
+ throw recoveryError;
921
+ }
922
+ }
923
+ else if (!isTransientLockRace(error)) {
924
+ throw new StateError("unable to inspect connection lease coordinator");
925
+ }
926
+ }
927
+ if (Date.now() - startedAt >= LEASE_LOCK_TIMEOUT_MS)
928
+ throw new StateError("connection lease is busy; retry later");
929
+ sleepForLeaseLock();
930
+ }
931
+ }
932
+ function acquireLeaseLock(dir) {
933
+ const lockPath = join(dir, LEASE_LOCK_DIRNAME);
934
+ const markerPath = join(lockPath, LEASE_LOCK_MARKER);
935
+ const startedAt = Date.now();
936
+ while (true) {
937
+ const coordinatorRelease = acquireCoordinator(dir);
938
+ let coordinatorReleased = false;
939
+ try {
940
+ let created = false;
941
+ try {
942
+ mkdirSync(lockPath, { mode: 0o700 });
943
+ created = true;
944
+ const token = generateOwnerToken();
945
+ writeFileSync(markerPath, token, {
946
+ encoding: "utf8",
947
+ flag: "wx",
948
+ mode: 0o600,
949
+ });
950
+ coordinatorRelease();
951
+ coordinatorReleased = true;
952
+ let released = false;
953
+ return () => {
954
+ if (released)
955
+ return;
956
+ released = true;
957
+ removeOwnedLeaseLock(dir, lockPath, token);
958
+ };
959
+ }
960
+ catch (error) {
961
+ if (created) {
962
+ try {
963
+ unlinkSync(markerPath);
964
+ }
965
+ catch {
966
+ // Best effort cleanup of this process's private marker.
967
+ }
968
+ try {
969
+ rmdirSync(lockPath);
970
+ }
971
+ catch {
972
+ // Best effort cleanup of this process's private lock.
973
+ }
974
+ if (isTransientLockRace(error))
975
+ throw new StateError("connection lease is busy; retry later");
976
+ throw new StateError("unable to acquire connection lease lock");
977
+ }
978
+ const code = error.code;
979
+ if (code !== "EEXIST")
980
+ throw new StateError("unable to acquire connection lease lock");
981
+ try {
982
+ const stat = lstatSync(lockPath);
983
+ if (stat.isSymbolicLink() || !stat.isDirectory())
984
+ throw new StateError("connection lease lock is not a private directory");
985
+ if (Date.now() - stat.mtimeMs > LEASE_LOCK_STALE_MS) {
986
+ removeLockDirectory(lockPath);
987
+ continue;
988
+ }
989
+ }
990
+ catch (statError) {
991
+ if (statError instanceof StateError)
992
+ throw statError;
993
+ if (statError.code === "ENOENT")
994
+ continue;
995
+ throw new StateError("unable to inspect connection lease lock");
996
+ }
997
+ }
998
+ }
999
+ finally {
1000
+ if (!coordinatorReleased)
1001
+ coordinatorRelease();
1002
+ }
1003
+ if (Date.now() - startedAt >= LEASE_LOCK_TIMEOUT_MS)
1004
+ throw new StateError("connection lease is busy; retry later");
1005
+ sleepForLeaseLock();
1006
+ }
1007
+ }
1008
+ function isoTime(now) {
1009
+ return new Date(now).toISOString();
1010
+ }
1011
+ export function checkLease(lease, expected, now = Date.now()) {
1012
+ if (lease === undefined)
1013
+ return "missing";
1014
+ if (!isRecord(lease))
1015
+ return "malformed";
1016
+ if (lease.version !== LEASE_VERSION)
1017
+ return "unsupportedVersion";
1018
+ if (typeof lease.workspaceHash !== "string" ||
1019
+ typeof lease.sessionHash !== "string")
1020
+ return "malformed";
1021
+ if (lease.workspaceHash !== expected.workspaceHash)
1022
+ return "workspaceMismatch";
1023
+ if (lease.sessionHash !== expected.sessionHash)
1024
+ return "sessionMismatch";
1025
+ if (typeof lease.workspacePath !== "string" ||
1026
+ typeof lease.openCodeSessionID !== "string" ||
1027
+ typeof lease.ownerToken !== "string" ||
1028
+ lease.ownerToken.length === 0 ||
1029
+ typeof lease.name !== "string" ||
1030
+ (lease.label !== null && typeof lease.label !== "string") ||
1031
+ typeof lease.host !== "string" ||
1032
+ typeof lease.port !== "number" ||
1033
+ !Number.isSafeInteger(lease.port) ||
1034
+ typeof lease.tls !== "boolean" ||
1035
+ typeof lease.connectedAt !== "string" ||
1036
+ typeof lease.heartbeatAt !== "string" ||
1037
+ typeof lease.expiresAt !== "string")
1038
+ return "malformed";
1039
+ const canonicalLeaseWorkspace = canonicalWorkspacePath(lease.workspacePath);
1040
+ if (canonicalLeaseWorkspace !== lease.workspacePath ||
1041
+ workspaceKey(canonicalLeaseWorkspace) !== lease.workspaceHash)
1042
+ return "workspaceMismatch";
1043
+ if (sessionKey(lease.openCodeSessionID) !== lease.sessionHash)
1044
+ return "sessionMismatch";
1045
+ if (expected.workspacePath !== undefined) {
1046
+ const canonicalExpectedWorkspace = canonicalWorkspacePath(expected.workspacePath);
1047
+ if (lease.workspacePath !== canonicalExpectedWorkspace)
1048
+ return "workspaceMismatch";
1049
+ }
1050
+ if (expected.openCodeSessionID !== undefined &&
1051
+ lease.openCodeSessionID !== expected.openCodeSessionID)
1052
+ return "sessionMismatch";
1053
+ const expiresAt = Date.parse(lease.expiresAt);
1054
+ const connectedAt = Date.parse(lease.connectedAt);
1055
+ const heartbeatAt = Date.parse(lease.heartbeatAt);
1056
+ if (Number.isNaN(expiresAt) ||
1057
+ Number.isNaN(connectedAt) ||
1058
+ Number.isNaN(heartbeatAt))
1059
+ return "malformed";
1060
+ if (expiresAt <= now)
1061
+ return "expired";
1062
+ return "fresh";
1063
+ }
1064
+ function buildLease(input, ownerToken, now) {
1065
+ return {
1066
+ version: LEASE_VERSION,
1067
+ workspacePath: input.workspacePath,
1068
+ workspaceHash: input.workspaceHash,
1069
+ openCodeSessionID: input.openCodeSessionID,
1070
+ sessionHash: input.sessionHash,
1071
+ ownerToken,
1072
+ name: input.name,
1073
+ label: input.label ?? null,
1074
+ host: input.host,
1075
+ port: input.port,
1076
+ tls: input.tls,
1077
+ connectedAt: isoTime(now),
1078
+ heartbeatAt: isoTime(now),
1079
+ expiresAt: isoTime(now + LEASE_EXPIRY_ALLOWANCE_MS),
1080
+ };
1081
+ }
1082
+ function readLeaseRecord(dataDir, workspaceHash, sessionHash) {
1083
+ const dir = sessionDir(dataDir, workspaceHash, sessionHash);
1084
+ const path = join(dir, CONNECTION_FILENAME);
1085
+ if (!existsSync(path))
1086
+ return { present: false };
1087
+ return { present: true, record: readJsonFile(path) };
1088
+ }
1089
+ export function readLeaseFile(dataDir, workspaceHash, sessionHash) {
1090
+ const file = readLeaseRecord(dataDir, workspaceHash, sessionHash);
1091
+ if (file.present) {
1092
+ const check = checkLease(file.record, { workspaceHash, sessionHash });
1093
+ if (check === "workspaceMismatch" || check === "sessionMismatch")
1094
+ throw new StateError("connection record does not match its scope");
1095
+ }
1096
+ return file;
1097
+ }
1098
+ function finishLeaseOperation(releaseLock, operationFailed) {
1099
+ try {
1100
+ releaseLock();
1101
+ }
1102
+ catch (error) {
1103
+ if (operationFailed || isTransientLockRace(error))
1104
+ return;
1105
+ throw error;
1106
+ }
1107
+ }
1108
+ function normalizeClaimInput(input) {
1109
+ const workspacePath = canonicalWorkspacePath(input.workspacePath);
1110
+ const workspaceHash = workspaceKey(workspacePath);
1111
+ const sessionHash = sessionKey(input.openCodeSessionID);
1112
+ if (input.workspaceHash !== workspaceHash)
1113
+ throw new StateError("workspace scope hash does not match its path");
1114
+ if (input.sessionHash !== sessionHash)
1115
+ throw new StateError("session scope hash does not match its ID");
1116
+ return { ...input, workspacePath, workspaceHash, sessionHash };
1117
+ }
1118
+ export function claimLease(dataDir, input, now = Date.now()) {
1119
+ const normalized = normalizeClaimInput(input);
1120
+ const dir = ensureSessionDir(dataDir, normalized.workspaceHash, normalized.sessionHash);
1121
+ const releaseLock = acquireLeaseLock(dir);
1122
+ let operationFailed = false;
1123
+ try {
1124
+ const { present, record } = readLeaseRecord(dataDir, normalized.workspaceHash, normalized.sessionHash);
1125
+ if (present) {
1126
+ const check = checkLease(record, {
1127
+ workspaceHash: normalized.workspaceHash,
1128
+ sessionHash: normalized.sessionHash,
1129
+ workspacePath: normalized.workspacePath,
1130
+ openCodeSessionID: normalized.openCodeSessionID,
1131
+ }, now);
1132
+ if (check === "fresh")
1133
+ throw new StateError("connection lease is held by another process or controller");
1134
+ if (check === "malformed" ||
1135
+ check === "unsupportedVersion" ||
1136
+ check === "workspaceMismatch" ||
1137
+ check === "sessionMismatch")
1138
+ throw new StateError("existing connection record is malformed or mismatched; remove it before reconnecting");
1139
+ }
1140
+ const lease = buildLease(normalized, generateOwnerToken(), now);
1141
+ writeJsonAtomic(join(dir, CONNECTION_FILENAME), lease);
1142
+ return lease;
1143
+ }
1144
+ catch (error) {
1145
+ operationFailed = true;
1146
+ throw error;
1147
+ }
1148
+ finally {
1149
+ finishLeaseOperation(releaseLock, operationFailed);
1150
+ }
1151
+ }
1152
+ export function refreshLease(dataDir, workspaceHash, sessionHash, ownerToken, now = Date.now()) {
1153
+ const dir = ensureSessionDir(dataDir, workspaceHash, sessionHash);
1154
+ const releaseLock = acquireLeaseLock(dir);
1155
+ let operationFailed = false;
1156
+ try {
1157
+ const { present, record } = readLeaseRecord(dataDir, workspaceHash, sessionHash);
1158
+ if (!present)
1159
+ throw new StateError("connection lease is missing; reconnect first");
1160
+ const check = checkLease(record, { workspaceHash, sessionHash }, now);
1161
+ if (check === "malformed" || check === "unsupportedVersion")
1162
+ throw new StateError("connection record is malformed");
1163
+ if (check === "workspaceMismatch" || check === "sessionMismatch")
1164
+ throw new StateError("connection record does not match this session scope");
1165
+ const existing = record;
1166
+ if (existing.ownerToken !== ownerToken)
1167
+ throw new StateError("connection lease is owned by another process");
1168
+ const updated = {
1169
+ ...existing,
1170
+ heartbeatAt: isoTime(now),
1171
+ expiresAt: isoTime(now + LEASE_EXPIRY_ALLOWANCE_MS),
1172
+ };
1173
+ writeJsonAtomic(join(dir, CONNECTION_FILENAME), updated);
1174
+ return updated;
1175
+ }
1176
+ catch (error) {
1177
+ operationFailed = true;
1178
+ throw error;
1179
+ }
1180
+ finally {
1181
+ finishLeaseOperation(releaseLock, operationFailed);
1182
+ }
1183
+ }
1184
+ export function releaseLease(dataDir, workspaceHash, sessionHash, ownerToken, now = Date.now()) {
1185
+ const dir = sessionDir(dataDir, workspaceHash, sessionHash);
1186
+ const { present } = readLeaseRecord(dataDir, workspaceHash, sessionHash);
1187
+ if (!present)
1188
+ return;
1189
+ const releaseLock = acquireLeaseLock(dir);
1190
+ let operationFailed = false;
1191
+ try {
1192
+ const { present: stillPresent, record } = readLeaseRecord(dataDir, workspaceHash, sessionHash);
1193
+ if (!stillPresent)
1194
+ return;
1195
+ const check = checkLease(record, { workspaceHash, sessionHash }, now);
1196
+ if (check === "malformed" ||
1197
+ check === "unsupportedVersion" ||
1198
+ check === "workspaceMismatch" ||
1199
+ check === "sessionMismatch")
1200
+ throw new StateError("connection record is malformed or mismatched");
1201
+ const existing = record;
1202
+ if (existing.ownerToken !== ownerToken)
1203
+ throw new StateError("connection lease is owned by another process");
1204
+ try {
1205
+ unlinkSync(join(dir, CONNECTION_FILENAME));
1206
+ }
1207
+ catch (error) {
1208
+ if (error.code !== "ENOENT")
1209
+ throw new StateError("unable to remove connection lease");
1210
+ }
1211
+ }
1212
+ catch (error) {
1213
+ operationFailed = true;
1214
+ throw error;
1215
+ }
1216
+ finally {
1217
+ finishLeaseOperation(releaseLock, operationFailed);
1218
+ }
1219
+ }
1220
+ export function resolveLease(dataDir, input, now = Date.now()) {
1221
+ const workspacePath = canonicalWorkspacePath(input.workspacePath);
1222
+ const workspaceHash = workspaceKey(workspacePath);
1223
+ const sessionHash = sessionKey(input.openCodeSessionID);
1224
+ const { present, record } = readLeaseRecord(dataDir, workspaceHash, sessionHash);
1225
+ if (!present)
1226
+ return {
1227
+ workspacePath,
1228
+ workspaceHash,
1229
+ sessionHash,
1230
+ present: false,
1231
+ check: "missing",
1232
+ };
1233
+ const check = checkLease(record, {
1234
+ workspaceHash,
1235
+ sessionHash,
1236
+ workspacePath,
1237
+ openCodeSessionID: input.openCodeSessionID,
1238
+ }, now);
1239
+ return {
1240
+ workspacePath,
1241
+ workspaceHash,
1242
+ sessionHash,
1243
+ present: true,
1244
+ ...(check !== "workspaceMismatch" &&
1245
+ check !== "sessionMismatch" &&
1246
+ check !== "malformed" &&
1247
+ check !== "unsupportedVersion"
1248
+ ? { lease: record }
1249
+ : {}),
1250
+ check,
1251
+ };
1252
+ }
1253
+ function isPreferences(value) {
1254
+ if (!isRecord(value))
1255
+ return false;
1256
+ if (value.version !== PREFERENCES_VERSION)
1257
+ return false;
1258
+ if (typeof value.workspacePath !== "string")
1259
+ return false;
1260
+ if (typeof value.workspaceHash !== "string")
1261
+ return false;
1262
+ if (typeof value.openCodeSessionID !== "string")
1263
+ return false;
1264
+ if (typeof value.sessionHash !== "string")
1265
+ return false;
1266
+ if (value.name !== null && typeof value.name !== "string")
1267
+ return false;
1268
+ if (value.label !== null && typeof value.label !== "string")
1269
+ return false;
1270
+ if (typeof value.autoConnect !== "boolean")
1271
+ return false;
1272
+ return true;
1273
+ }
1274
+ function validatePreferencesScope(preferences, expected, identity) {
1275
+ if (preferences.workspaceHash !== expected.workspaceHash)
1276
+ throw new StateError("session preferences do not match the workspace scope");
1277
+ if (preferences.sessionHash !== expected.sessionHash)
1278
+ throw new StateError("session preferences do not match the session scope");
1279
+ const canonicalPath = canonicalWorkspacePath(preferences.workspacePath);
1280
+ if (canonicalPath !== preferences.workspacePath ||
1281
+ workspaceKey(canonicalPath) !== preferences.workspaceHash)
1282
+ throw new StateError("session preferences do not match their workspace path");
1283
+ if (sessionKey(preferences.openCodeSessionID) !== preferences.sessionHash)
1284
+ throw new StateError("session preferences do not match their session ID");
1285
+ if (identity &&
1286
+ (preferences.workspacePath !==
1287
+ canonicalWorkspacePath(identity.workspacePath) ||
1288
+ preferences.openCodeSessionID !== identity.openCodeSessionID))
1289
+ throw new StateError("session preferences do not match this session identity");
1290
+ }
1291
+ export function readPreferences(dataDir, workspaceHash, sessionHash, identity) {
1292
+ const dir = sessionDir(dataDir, workspaceHash, sessionHash);
1293
+ const path = join(dir, PREFERENCES_FILENAME);
1294
+ if (!existsSync(path))
1295
+ return undefined;
1296
+ const record = readJsonFile(path);
1297
+ if (!isPreferences(record))
1298
+ throw new StateError("session preferences are malformed");
1299
+ validatePreferencesScope(record, { workspaceHash, sessionHash }, identity);
1300
+ return record;
1301
+ }
1302
+ export function writePreferences(dataDir, workspaceHash, sessionHash, preferences) {
1303
+ validatePreferencesScope(preferences, { workspaceHash, sessionHash });
1304
+ writeJsonAtomic(join(ensureSessionDir(dataDir, workspaceHash, sessionHash), PREFERENCES_FILENAME), preferences);
1305
+ }
1306
+ //# sourceMappingURL=state.js.map