@fml-inc/fml 0.7.0 → 0.8.0-rc.2

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 (45) hide show
  1. package/.claude-plugin/plugin.json +2 -4
  2. package/README.md +323 -79
  3. package/agent-assets/fml/SKILL.md +180 -0
  4. package/bin/fml +4 -18
  5. package/bin/hook-handler +7 -0
  6. package/bin/mcp-server +4 -18
  7. package/bin/observer +6 -0
  8. package/dist/chunk-3KISRT57.js +140 -0
  9. package/dist/chunk-56TPYMNK.js +4657 -0
  10. package/dist/chunk-7V7G7NVF.js +142 -0
  11. package/dist/chunk-A6HAEVJX.js +1606 -0
  12. package/dist/chunk-DZKYW327.js +1037 -0
  13. package/dist/chunk-ENW2GJNO.js +109 -0
  14. package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
  15. package/dist/{chunk-PTUZMKSN.js → chunk-R45CWWZO.js} +34 -23
  16. package/dist/chunk-WZLLO563.js +953 -0
  17. package/dist/chunk-YSU5CS2V.js +3331 -0
  18. package/dist/cli.js +7742 -1072
  19. package/dist/{esm-2EUDFZWP.js → esm-4YKLKFRX.js} +546 -741
  20. package/dist/hook-handler.js +440 -0
  21. package/dist/mcp/server.js +112 -98
  22. package/dist/observer-server.js +1566 -0
  23. package/dist/sync-cycle.js +31 -0
  24. package/dist/{token-store-3IQSLOTQ.js → token-store-BT5THIAJ.js} +10 -3
  25. package/hooks/hooks.json +290 -0
  26. package/package.json +24 -27
  27. package/dist/chunk-PTUZMKSN.js.map +0 -1
  28. package/dist/chunk-PZ5AY32C.js.map +0 -1
  29. package/dist/chunk-S4TJNLEN.js +0 -61
  30. package/dist/chunk-S4TJNLEN.js.map +0 -1
  31. package/dist/chunk-VV4JHH4H.js +0 -437
  32. package/dist/chunk-VV4JHH4H.js.map +0 -1
  33. package/dist/chunk-YH5ROR7G.js +0 -69
  34. package/dist/chunk-YH5ROR7G.js.map +0 -1
  35. package/dist/cli.js.map +0 -1
  36. package/dist/daemon-MSLDHLMH.js +0 -21
  37. package/dist/daemon-MSLDHLMH.js.map +0 -1
  38. package/dist/daemon-utils-OODBIYB7.js +0 -10
  39. package/dist/daemon-utils-OODBIYB7.js.map +0 -1
  40. package/dist/esm-2EUDFZWP.js.map +0 -1
  41. package/dist/mcp/server.js.map +0 -1
  42. package/dist/postinstall.js +0 -16
  43. package/dist/postinstall.js.map +0 -1
  44. package/dist/token-store-3IQSLOTQ.js.map +0 -1
  45. package/scripts/postinstall.cjs +0 -25
@@ -0,0 +1,953 @@
1
+ import {
2
+ OBSERVER_SERVICE_NAME
3
+ } from "./chunk-56TPYMNK.js";
4
+ import {
5
+ FML_OBSERVER_LOCK_FILE,
6
+ FML_OBSERVER_PORT,
7
+ FML_OBSERVER_STATE_FILE
8
+ } from "./chunk-A6HAEVJX.js";
9
+
10
+ // src/observer/state.ts
11
+ import { randomUUID } from "crypto";
12
+ import { execFileSync } from "child_process";
13
+ import fs from "fs";
14
+ import path from "path";
15
+ var OBSERVER_STATE_SCHEMA_VERSION = 1;
16
+ var OBSERVER_HEARTBEAT_STALE_MS = 2 * 60 * 1e3;
17
+ var OBSERVER_RECLAIM_GUARD_STALE_MS = 3e4;
18
+ var OBSERVER_RECLAIM_GUARD_UNVERIFIED_TTL_MS = 5 * 6e4;
19
+ var MAX_RECLAIM_GUARD_RECOVERIES = 2;
20
+ var MAX_RECLAIM_GUARD_RACE_RETRIES = 16;
21
+ var MAX_INITIAL_STATE_PUBLICATION_RETRIES = 2;
22
+ var EXCLUSIVE_LINK_UNSUPPORTED_ERROR_CODES = /* @__PURE__ */ new Set([
23
+ "ENOSYS",
24
+ "ENOTSUP",
25
+ "EOPNOTSUPP",
26
+ "EPERM",
27
+ "EXDEV"
28
+ ]);
29
+ var TRANSIENT_OBSERVER_FILE_ERROR_CODES = /* @__PURE__ */ new Set([
30
+ "EACCES",
31
+ "EAGAIN",
32
+ "EBUSY",
33
+ "EPERM"
34
+ ]);
35
+ var reclaimProcessNonce = randomUUID();
36
+ var activeReclaimGuardIds = /* @__PURE__ */ new Set();
37
+ function isRecord(value) {
38
+ return !!value && typeof value === "object" && !Array.isArray(value);
39
+ }
40
+ function isObserverPhase(value) {
41
+ return value === "starting" || value === "syncing" || value === "idle" || value === "waiting_for_auth" || value === "backing_off" || value === "stopping";
42
+ }
43
+ function optionalNumber(record, key) {
44
+ const value = record[key];
45
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
46
+ }
47
+ function optionalString(record, key) {
48
+ const value = record[key];
49
+ return typeof value === "string" ? value : void 0;
50
+ }
51
+ function parseObserverState(value) {
52
+ if (!isRecord(value)) return null;
53
+ if (value.schemaVersion !== OBSERVER_STATE_SCHEMA_VERSION || value.service !== OBSERVER_SERVICE_NAME || typeof value.pid !== "number" || !Number.isInteger(value.pid) || value.pid <= 0 || typeof value.port !== "number" || !Number.isInteger(value.port) || value.port <= 0 || value.port > 65535 || typeof value.instanceId !== "string" || value.instanceId.length === 0 || typeof value.startedAtMs !== "number" || typeof value.heartbeatAtMs !== "number" || !isObserverPhase(value.phase)) {
54
+ return null;
55
+ }
56
+ return {
57
+ schemaVersion: OBSERVER_STATE_SCHEMA_VERSION,
58
+ service: OBSERVER_SERVICE_NAME,
59
+ pid: value.pid,
60
+ port: value.port,
61
+ instanceId: value.instanceId,
62
+ startedAtMs: value.startedAtMs,
63
+ heartbeatAtMs: value.heartbeatAtMs,
64
+ phase: value.phase,
65
+ processIdentity: optionalString(value, "processIdentity"),
66
+ environment: optionalString(value, "environment"),
67
+ lastAttemptAtMs: optionalNumber(value, "lastAttemptAtMs"),
68
+ lastSuccessAtMs: optionalNumber(value, "lastSuccessAtMs"),
69
+ nextAttemptAtMs: optionalNumber(value, "nextAttemptAtMs"),
70
+ lastOutcome: optionalString(value, "lastOutcome"),
71
+ lastError: optionalString(value, "lastError")
72
+ };
73
+ }
74
+ function isPidAlive(pid) {
75
+ try {
76
+ process.kill(pid, 0);
77
+ return true;
78
+ } catch (error) {
79
+ return error.code === "EPERM";
80
+ }
81
+ }
82
+ function readObserverState(stateFile = FML_OBSERVER_STATE_FILE) {
83
+ try {
84
+ return parseObserverState(
85
+ JSON.parse(fs.readFileSync(stateFile, "utf8"))
86
+ );
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ function readObserverStateStatus(opts = {}) {
92
+ const stateFile = opts.stateFile ?? FML_OBSERVER_STATE_FILE;
93
+ const exists = fs.existsSync(stateFile);
94
+ const state = readObserverState(stateFile);
95
+ if (!state) {
96
+ return { exists, valid: false, running: false, responsive: false, state };
97
+ }
98
+ const running = (opts.pidAlive ?? isPidAlive)(state.pid);
99
+ return {
100
+ exists,
101
+ valid: true,
102
+ running,
103
+ responsive: running && (opts.nowMs ?? Date.now()) - state.heartbeatAtMs <= OBSERVER_HEARTBEAT_STALE_MS,
104
+ state
105
+ };
106
+ }
107
+ function createInitialObserverState(opts = {}) {
108
+ const nowMs = opts.nowMs ?? Date.now();
109
+ return {
110
+ schemaVersion: OBSERVER_STATE_SCHEMA_VERSION,
111
+ service: OBSERVER_SERVICE_NAME,
112
+ pid: process.pid,
113
+ port: opts.port ?? FML_OBSERVER_PORT,
114
+ instanceId: randomUUID(),
115
+ startedAtMs: nowMs,
116
+ heartbeatAtMs: nowMs,
117
+ phase: "starting",
118
+ processIdentity: getCurrentProcessIdentity() ?? void 0
119
+ };
120
+ }
121
+ function serializeState(state) {
122
+ return `${JSON.stringify(state, null, 2)}
123
+ `;
124
+ }
125
+ var ObserverStateMutationBusyError = class extends Error {
126
+ constructor() {
127
+ super("observer lease mutation is already in progress");
128
+ this.name = "ObserverStateMutationBusyError";
129
+ }
130
+ };
131
+ function parseObserverLeaseOwner(value) {
132
+ try {
133
+ const parsed = JSON.parse(value);
134
+ return typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 && typeof parsed.instanceId === "string" && parsed.instanceId.length > 0 ? { pid: parsed.pid, instanceId: parsed.instanceId } : null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ function readObserverLeaseOwner(lockFile = FML_OBSERVER_LOCK_FILE) {
140
+ try {
141
+ return parseObserverLeaseOwner(fs.readFileSync(lockFile, "utf8"));
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+ function inspectObserverFile(file) {
147
+ try {
148
+ return {
149
+ raw: fs.readFileSync(file, "utf8"),
150
+ exists: true,
151
+ busy: false
152
+ };
153
+ } catch (error) {
154
+ if (error.code === "ENOENT") {
155
+ return { raw: null, exists: false, busy: false };
156
+ }
157
+ if (isTransientObserverFileError(error)) {
158
+ return { raw: null, exists: true, busy: true };
159
+ }
160
+ throw error;
161
+ }
162
+ }
163
+ function inspectObserverLease(opts = {}) {
164
+ const stateResult = inspectObserverFile(
165
+ opts.stateFile ?? FML_OBSERVER_STATE_FILE
166
+ );
167
+ const lockResult = inspectObserverFile(
168
+ opts.lockFile ?? FML_OBSERVER_LOCK_FILE
169
+ );
170
+ let state = null;
171
+ if (stateResult.raw !== null) {
172
+ try {
173
+ state = parseObserverState(JSON.parse(stateResult.raw));
174
+ } catch {
175
+ state = null;
176
+ }
177
+ }
178
+ return {
179
+ state,
180
+ owner: lockResult.raw === null ? null : parseObserverLeaseOwner(lockResult.raw),
181
+ stateExists: stateResult.exists,
182
+ lockExists: lockResult.exists,
183
+ stateBusy: stateResult.busy,
184
+ lockBusy: lockResult.busy,
185
+ busy: stateResult.busy || lockResult.busy
186
+ };
187
+ }
188
+ function observerLeaseIsOwned(instanceId, lockFile = FML_OBSERVER_LOCK_FILE) {
189
+ return readObserverLeaseOwner(lockFile)?.instanceId === instanceId;
190
+ }
191
+ function getProcessIdentity(pid) {
192
+ try {
193
+ if (process.platform === "linux") {
194
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
195
+ const fieldsAfterCommand = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
196
+ const startTimeTicks = fieldsAfterCommand[19];
197
+ return startTimeTicks ? `linux:${startTimeTicks}` : null;
198
+ }
199
+ if (process.platform === "darwin") {
200
+ const startedAt = execFileSync(
201
+ "ps",
202
+ ["-p", String(pid), "-o", "lstart="],
203
+ {
204
+ encoding: "utf8",
205
+ stdio: ["ignore", "pipe", "ignore"],
206
+ timeout: 1e3
207
+ }
208
+ ).trim();
209
+ return startedAt ? `darwin:${startedAt}` : null;
210
+ }
211
+ if (process.platform === "win32") {
212
+ const startedAt = execFileSync(
213
+ "powershell.exe",
214
+ [
215
+ "-NoProfile",
216
+ "-NonInteractive",
217
+ "-Command",
218
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().Ticks`
219
+ ],
220
+ {
221
+ encoding: "utf8",
222
+ stdio: ["ignore", "pipe", "ignore"],
223
+ timeout: 1e3
224
+ }
225
+ ).trim();
226
+ return startedAt ? `win32:${startedAt}` : null;
227
+ }
228
+ } catch {
229
+ }
230
+ return null;
231
+ }
232
+ var currentProcessIdentityCache;
233
+ function getCurrentProcessIdentity() {
234
+ if (currentProcessIdentityCache === void 0) {
235
+ currentProcessIdentityCache = getProcessIdentity(process.pid);
236
+ }
237
+ return currentProcessIdentityCache;
238
+ }
239
+ function parseObserverReclaimGuardOwner(value) {
240
+ try {
241
+ const parsed = JSON.parse(value);
242
+ if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.instanceId !== "string" || parsed.instanceId.length === 0 || typeof parsed.guardId !== "string" || parsed.guardId.length === 0 || typeof parsed.createdAtMs !== "number" || parsed.processIdentity !== void 0 && typeof parsed.processIdentity !== "string" || parsed.processNonce !== void 0 && typeof parsed.processNonce !== "string") {
243
+ return null;
244
+ }
245
+ return parsed;
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ function isTransientObserverFileError(error) {
251
+ const code = error?.code;
252
+ return code !== void 0 && TRANSIENT_OBSERVER_FILE_ERROR_CODES.has(code);
253
+ }
254
+ function readObserverMutationFile(file) {
255
+ try {
256
+ return fs.readFileSync(file, "utf8");
257
+ } catch (error) {
258
+ if (error.code === "ENOENT") return null;
259
+ if (isTransientObserverFileError(error)) {
260
+ throw new ObserverStateMutationBusyError();
261
+ }
262
+ throw error;
263
+ }
264
+ }
265
+ function readObserverStateForMutation(stateFile) {
266
+ const raw = readObserverMutationFile(stateFile);
267
+ if (raw === null) return null;
268
+ try {
269
+ return parseObserverState(JSON.parse(raw));
270
+ } catch {
271
+ return null;
272
+ }
273
+ }
274
+ function assertObserverStateMutationOwnership(instanceId, stateFile, lockFile, errorMessage) {
275
+ const leaseOwner = readObserverMutationFile(lockFile);
276
+ if (leaseOwner === null || parseObserverLeaseOwner(leaseOwner)?.instanceId !== instanceId) {
277
+ throw new Error(errorMessage);
278
+ }
279
+ if (readObserverStateForMutation(stateFile)?.instanceId !== instanceId) {
280
+ throw new Error(errorMessage);
281
+ }
282
+ }
283
+ function isRetryableReclaimGuardRace(error) {
284
+ return error.code === "ENOENT" || isTransientObserverFileError(error);
285
+ }
286
+ function removeReclaimTombstone(tombstone) {
287
+ try {
288
+ fs.rmSync(tombstone, { force: true });
289
+ } catch (error) {
290
+ if (!isRetryableReclaimGuardRace(error)) throw error;
291
+ }
292
+ }
293
+ function restoreDisplacedReclaimGuard(tombstone, reclaimFile, fallbackRaw) {
294
+ let canonicalReserved = false;
295
+ let tombstoneRestored = false;
296
+ let fallbackPublished = false;
297
+ try {
298
+ fs.linkSync(tombstone, reclaimFile);
299
+ canonicalReserved = true;
300
+ tombstoneRestored = true;
301
+ } catch (error) {
302
+ if (error.code === "EEXIST") {
303
+ canonicalReserved = true;
304
+ } else if (!isRetryableReclaimGuardRace(error)) {
305
+ throw error;
306
+ }
307
+ }
308
+ if (!canonicalReserved) {
309
+ try {
310
+ fs.copyFileSync(tombstone, reclaimFile, fs.constants.COPYFILE_EXCL);
311
+ canonicalReserved = true;
312
+ tombstoneRestored = true;
313
+ } catch (error) {
314
+ if (error.code === "EEXIST") {
315
+ canonicalReserved = true;
316
+ } else if (!isRetryableReclaimGuardRace(error)) {
317
+ throw error;
318
+ }
319
+ }
320
+ }
321
+ if (!canonicalReserved) {
322
+ let fd;
323
+ try {
324
+ fd = fs.openSync(reclaimFile, "wx", 384);
325
+ canonicalReserved = true;
326
+ } catch (error) {
327
+ if (error.code === "EEXIST") {
328
+ canonicalReserved = true;
329
+ } else if (isTransientObserverFileError(error)) {
330
+ return false;
331
+ } else {
332
+ throw error;
333
+ }
334
+ fd = -1;
335
+ }
336
+ if (fd >= 0) {
337
+ let publicationError;
338
+ try {
339
+ fs.writeFileSync(fd, fallbackRaw ?? "");
340
+ } catch (error) {
341
+ publicationError = error;
342
+ }
343
+ try {
344
+ fs.closeSync(fd);
345
+ } catch (error) {
346
+ if (publicationError === void 0 || !isTransientObserverFileError(error)) {
347
+ publicationError = error;
348
+ }
349
+ }
350
+ if (publicationError !== void 0 && !isTransientObserverFileError(publicationError)) {
351
+ throw publicationError;
352
+ }
353
+ fallbackPublished = publicationError === void 0 && fallbackRaw !== void 0;
354
+ }
355
+ }
356
+ if (canonicalReserved && (fallbackPublished || tombstoneRestored)) {
357
+ removeReclaimTombstone(tombstone);
358
+ }
359
+ return canonicalReserved;
360
+ }
361
+ function sameFileIdentity(left, right) {
362
+ return left.dev === right.dev && left.ino === right.ino;
363
+ }
364
+ function removeCreatedFileByIdentity(file, created) {
365
+ let observed;
366
+ try {
367
+ observed = fs.statSync(file);
368
+ } catch (error) {
369
+ if (!isRetryableReclaimGuardRace(error)) throw error;
370
+ return error.code === "ENOENT";
371
+ }
372
+ if (!sameFileIdentity(observed, created)) return true;
373
+ const tombstone = `${file}.failed-${process.pid}-${randomUUID()}`;
374
+ try {
375
+ fs.renameSync(file, tombstone);
376
+ } catch (error) {
377
+ if (!isRetryableReclaimGuardRace(error)) throw error;
378
+ return error.code === "ENOENT";
379
+ }
380
+ let displaced;
381
+ try {
382
+ displaced = fs.statSync(tombstone);
383
+ } catch (error) {
384
+ if (!isRetryableReclaimGuardRace(error)) throw error;
385
+ return restoreDisplacedReclaimGuard(tombstone, file);
386
+ }
387
+ let displacedRaw;
388
+ try {
389
+ displacedRaw = fs.readFileSync(tombstone, "utf8");
390
+ } catch (error) {
391
+ if (!isRetryableReclaimGuardRace(error)) throw error;
392
+ }
393
+ if (sameFileIdentity(displaced, created)) {
394
+ removeReclaimTombstone(tombstone);
395
+ return true;
396
+ }
397
+ return restoreDisplacedReclaimGuard(tombstone, file, displacedRaw);
398
+ }
399
+ function rewriteExclusiveFileBody(fd, contents) {
400
+ const body = Buffer.from(contents, "utf8");
401
+ fs.ftruncateSync(fd, 0);
402
+ let offset = 0;
403
+ while (offset < body.length) {
404
+ const written = fs.writeSync(
405
+ fd,
406
+ body,
407
+ offset,
408
+ body.length - offset,
409
+ offset
410
+ );
411
+ if (written === 0) {
412
+ throw Object.assign(
413
+ new Error("observer guard rewrite made no progress"),
414
+ {
415
+ code: "EBUSY"
416
+ }
417
+ );
418
+ }
419
+ offset += written;
420
+ }
421
+ }
422
+ function publishExclusiveLease(lockFile, serializedOwner) {
423
+ const stagingFile = `${lockFile}.${process.pid}.${randomUUID()}.tmp`;
424
+ let fd = null;
425
+ let staged = null;
426
+ let publicationError;
427
+ try {
428
+ fd = fs.openSync(stagingFile, "wx", 384);
429
+ fs.writeFileSync(fd, serializedOwner);
430
+ fs.fsyncSync(fd);
431
+ staged = fs.fstatSync(fd);
432
+ const stagedFd = fd;
433
+ fd = null;
434
+ fs.closeSync(stagedFd);
435
+ try {
436
+ fs.linkSync(stagingFile, lockFile);
437
+ } catch (error) {
438
+ if (error.code === "EEXIST") return null;
439
+ let linkedDespiteError = false;
440
+ try {
441
+ const published2 = fs.statSync(lockFile);
442
+ linkedDespiteError = sameFileIdentity(published2, staged) && fs.readFileSync(lockFile, "utf8") === serializedOwner;
443
+ } catch {
444
+ }
445
+ if (!linkedDespiteError) {
446
+ const code = error.code;
447
+ if (code !== void 0 && EXCLUSIVE_LINK_UNSUPPORTED_ERROR_CODES.has(code)) {
448
+ throw new Error(
449
+ `The filesystem containing ${lockFile} does not support atomic FML observer leases; choose an FML state or observer directory on a filesystem with hard-link support`,
450
+ { cause: error }
451
+ );
452
+ }
453
+ throw error;
454
+ }
455
+ }
456
+ const published = fs.statSync(lockFile);
457
+ if (fs.readFileSync(lockFile, "utf8") !== serializedOwner) {
458
+ try {
459
+ removeCreatedFileByIdentity(lockFile, published);
460
+ } catch {
461
+ }
462
+ throw new Error(`FML observer lease publication was incomplete`);
463
+ }
464
+ return published;
465
+ } catch (error) {
466
+ publicationError = error;
467
+ throw error;
468
+ } finally {
469
+ if (fd !== null) {
470
+ try {
471
+ fs.closeSync(fd);
472
+ } catch (closeError) {
473
+ if (publicationError === void 0) throw closeError;
474
+ }
475
+ }
476
+ try {
477
+ fs.rmSync(stagingFile, { force: true });
478
+ } catch {
479
+ }
480
+ }
481
+ }
482
+ function ownsReclaimGuard(reclaimFile, guardId) {
483
+ try {
484
+ return parseObserverReclaimGuardOwner(fs.readFileSync(reclaimFile, "utf8"))?.guardId === guardId;
485
+ } catch {
486
+ return false;
487
+ }
488
+ }
489
+ function ownsReclaimGuardForMutation(reclaimFile, guardId) {
490
+ const raw = readObserverMutationFile(reclaimFile);
491
+ return raw !== null && parseObserverReclaimGuardOwner(raw)?.guardId === guardId;
492
+ }
493
+ function writeLeaseAndState(state, stateFile, lockFile, reclaimFile, reclaimGuardId) {
494
+ const serializedOwner = `${JSON.stringify({
495
+ pid: state.pid,
496
+ instanceId: state.instanceId
497
+ })}
498
+ `;
499
+ const created = publishExclusiveLease(lockFile, serializedOwner);
500
+ if (!created) return false;
501
+ if (!ownsReclaimGuard(reclaimFile, reclaimGuardId) || !observerLeaseIsOwned(state.instanceId, lockFile)) {
502
+ removeCreatedFileByIdentity(lockFile, created);
503
+ return false;
504
+ }
505
+ try {
506
+ writeObserverState(state, stateFile);
507
+ } catch (error) {
508
+ try {
509
+ removeCreatedFileByIdentity(lockFile, created);
510
+ } catch {
511
+ }
512
+ throw error;
513
+ }
514
+ const coherent = ownsReclaimGuard(reclaimFile, reclaimGuardId) && observerLeaseIsOwned(state.instanceId, lockFile) && readObserverState(stateFile)?.instanceId === state.instanceId;
515
+ if (!coherent) {
516
+ try {
517
+ removeCreatedFileByIdentity(lockFile, created);
518
+ } catch {
519
+ }
520
+ }
521
+ return coherent;
522
+ }
523
+ function leaseCanBeReclaimed(opts) {
524
+ const owner = parseObserverLeaseOwner(opts.rawOwner);
525
+ if (!owner) {
526
+ if (opts.liveOwnerPolicy === "process_identity") {
527
+ return false;
528
+ }
529
+ return opts.nowMs - opts.lockMtimeMs >= OBSERVER_RECLAIM_GUARD_STALE_MS;
530
+ }
531
+ if (!opts.pidAlive(owner.pid)) return true;
532
+ const ownerState = readObserverState(opts.stateFile);
533
+ if (opts.liveOwnerPolicy === "process_identity") {
534
+ if (ownerState?.pid !== owner.pid || ownerState.instanceId !== owner.instanceId) {
535
+ return false;
536
+ }
537
+ if (ownerState.processIdentity === void 0) {
538
+ return false;
539
+ }
540
+ const currentIdentity = opts.processIdentity(owner.pid);
541
+ return currentIdentity !== null && currentIdentity !== ownerState.processIdentity;
542
+ }
543
+ if (ownerState?.pid === owner.pid && ownerState.instanceId === owner.instanceId) {
544
+ return opts.nowMs - ownerState.heartbeatAtMs > OBSERVER_HEARTBEAT_STALE_MS;
545
+ }
546
+ return opts.nowMs - opts.lockMtimeMs > OBSERVER_HEARTBEAT_STALE_MS;
547
+ }
548
+ function acquireReclaimGuard(owner, reclaimFile, nowMs, opts = {}) {
549
+ const guardId = randomUUID();
550
+ const processIdentity = opts.processIdentity ?? ((pid) => pid === process.pid ? getCurrentProcessIdentity() : getProcessIdentity(pid));
551
+ let recoveryAttempts = 0;
552
+ let raceRetries = 0;
553
+ while (recoveryAttempts < MAX_RECLAIM_GUARD_RECOVERIES && raceRetries <= MAX_RECLAIM_GUARD_RACE_RETRIES) {
554
+ try {
555
+ const fd = fs.openSync(reclaimFile, "wx", 384);
556
+ let created = null;
557
+ const serializedGuard = `${JSON.stringify({
558
+ pid: process.pid,
559
+ instanceId: owner.instanceId,
560
+ guardId,
561
+ createdAtMs: nowMs,
562
+ processIdentity: processIdentity(process.pid) ?? void 0,
563
+ processNonce: reclaimProcessNonce
564
+ })}
565
+ `;
566
+ let publicationError;
567
+ let publicationFailed = false;
568
+ let cleanupBlocked = false;
569
+ try {
570
+ created = fs.fstatSync(fd);
571
+ fs.writeFileSync(fd, serializedGuard);
572
+ } catch (error) {
573
+ if (isTransientObserverFileError(error)) {
574
+ try {
575
+ rewriteExclusiveFileBody(fd, serializedGuard);
576
+ } catch (retryError) {
577
+ publicationFailed = true;
578
+ publicationError = retryError;
579
+ }
580
+ } else {
581
+ publicationFailed = true;
582
+ publicationError = error;
583
+ }
584
+ if (publicationFailed && created) {
585
+ try {
586
+ cleanupBlocked = !removeCreatedFileByIdentity(reclaimFile, created);
587
+ } catch (cleanupError) {
588
+ publicationError = cleanupError;
589
+ }
590
+ }
591
+ }
592
+ let displacedAfterPublication = false;
593
+ if (!publicationFailed) {
594
+ try {
595
+ displacedAfterPublication = parseObserverReclaimGuardOwner(fs.readFileSync(reclaimFile, "utf8"))?.guardId !== guardId;
596
+ } catch (error) {
597
+ if (error.code === "ENOENT") {
598
+ displacedAfterPublication = true;
599
+ } else {
600
+ publicationFailed = true;
601
+ publicationError = error;
602
+ if (isTransientObserverFileError(error) && created) {
603
+ try {
604
+ cleanupBlocked = !removeCreatedFileByIdentity(
605
+ reclaimFile,
606
+ created
607
+ );
608
+ } catch (cleanupError) {
609
+ publicationError = cleanupError;
610
+ }
611
+ }
612
+ }
613
+ }
614
+ }
615
+ try {
616
+ fs.closeSync(fd);
617
+ } catch (error) {
618
+ if (!publicationFailed) publicationError = error;
619
+ publicationFailed = true;
620
+ }
621
+ if (publicationFailed) {
622
+ if (cleanupBlocked) return null;
623
+ if (isTransientObserverFileError(publicationError)) {
624
+ if (ownsReclaimGuard(reclaimFile, guardId)) {
625
+ activeReclaimGuardIds.add(guardId);
626
+ return guardId;
627
+ }
628
+ raceRetries += 1;
629
+ continue;
630
+ }
631
+ throw publicationError;
632
+ }
633
+ if (displacedAfterPublication) return null;
634
+ activeReclaimGuardIds.add(guardId);
635
+ return guardId;
636
+ } catch (error) {
637
+ if (error.code !== "EEXIST") {
638
+ if (isTransientObserverFileError(error)) return null;
639
+ throw error;
640
+ }
641
+ let observedRaw;
642
+ let observed;
643
+ try {
644
+ observedRaw = fs.readFileSync(reclaimFile, "utf8");
645
+ observed = fs.statSync(reclaimFile);
646
+ } catch (readError) {
647
+ if (readError.code === "ENOENT") {
648
+ raceRetries += 1;
649
+ continue;
650
+ }
651
+ if (isTransientObserverFileError(readError)) return null;
652
+ throw readError;
653
+ }
654
+ const observedOwner = parseObserverReclaimGuardOwner(observedRaw);
655
+ if (observedOwner) {
656
+ const alive = (opts.pidAlive ?? isPidAlive)(observedOwner.pid);
657
+ const abandonedByThisProcess = observedOwner.processNonce === reclaimProcessNonce && !activeReclaimGuardIds.has(observedOwner.guardId);
658
+ if (alive && !abandonedByThisProcess) {
659
+ const currentIdentity = observedOwner.processIdentity === void 0 ? null : processIdentity(observedOwner.pid);
660
+ if (currentIdentity !== null && currentIdentity === observedOwner.processIdentity) {
661
+ return null;
662
+ }
663
+ if (currentIdentity === null && nowMs - observedOwner.createdAtMs < OBSERVER_RECLAIM_GUARD_UNVERIFIED_TTL_MS) {
664
+ return null;
665
+ }
666
+ }
667
+ } else if (nowMs - observed.mtimeMs < OBSERVER_RECLAIM_GUARD_STALE_MS) {
668
+ return null;
669
+ }
670
+ const tombstone = `${reclaimFile}.stale-${process.pid}-${randomUUID()}`;
671
+ try {
672
+ fs.renameSync(reclaimFile, tombstone);
673
+ } catch (renameError) {
674
+ if (renameError.code === "ENOENT") {
675
+ raceRetries += 1;
676
+ continue;
677
+ }
678
+ if (isTransientObserverFileError(renameError)) return null;
679
+ throw renameError;
680
+ }
681
+ recoveryAttempts += 1;
682
+ let displacedRaw;
683
+ try {
684
+ displacedRaw = fs.readFileSync(tombstone, "utf8");
685
+ } catch (error2) {
686
+ if (!isRetryableReclaimGuardRace(error2)) throw error2;
687
+ restoreDisplacedReclaimGuard(tombstone, reclaimFile, observedRaw);
688
+ return null;
689
+ }
690
+ let displaced;
691
+ try {
692
+ displaced = fs.statSync(tombstone);
693
+ } catch (error2) {
694
+ if (!isRetryableReclaimGuardRace(error2)) throw error2;
695
+ restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
696
+ return null;
697
+ }
698
+ if (displacedRaw !== observedRaw || displaced.dev !== observed.dev || displaced.ino !== observed.ino || displaced.mtimeMs !== observed.mtimeMs || !observedOwner && nowMs - displaced.mtimeMs < OBSERVER_RECLAIM_GUARD_STALE_MS) {
699
+ restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
700
+ return null;
701
+ }
702
+ removeReclaimTombstone(tombstone);
703
+ }
704
+ }
705
+ return null;
706
+ }
707
+ function releaseReclaimGuard(reclaimFile, guardId) {
708
+ try {
709
+ let observedRaw;
710
+ let observed;
711
+ try {
712
+ observedRaw = fs.readFileSync(reclaimFile, "utf8");
713
+ observed = fs.statSync(reclaimFile);
714
+ } catch (error) {
715
+ if (error.code === "ENOENT" || isTransientObserverFileError(error)) {
716
+ return;
717
+ }
718
+ throw error;
719
+ }
720
+ if (parseObserverReclaimGuardOwner(observedRaw)?.guardId !== guardId) {
721
+ return;
722
+ }
723
+ const tombstone = `${reclaimFile}.release-${process.pid}-${randomUUID()}`;
724
+ fs.renameSync(reclaimFile, tombstone);
725
+ let displacedRaw;
726
+ try {
727
+ displacedRaw = fs.readFileSync(tombstone, "utf8");
728
+ } catch (error) {
729
+ if (!isRetryableReclaimGuardRace(error)) throw error;
730
+ restoreDisplacedReclaimGuard(tombstone, reclaimFile, observedRaw);
731
+ return;
732
+ }
733
+ let displaced;
734
+ try {
735
+ displaced = fs.statSync(tombstone);
736
+ } catch (error) {
737
+ if (!isRetryableReclaimGuardRace(error)) throw error;
738
+ restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
739
+ return;
740
+ }
741
+ if (displacedRaw !== observedRaw || displaced.dev !== observed.dev || displaced.ino !== observed.ino || displaced.mtimeMs !== observed.mtimeMs) {
742
+ restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
743
+ return;
744
+ }
745
+ removeReclaimTombstone(tombstone);
746
+ } catch (error) {
747
+ if (error.code !== "ENOENT" && !isTransientObserverFileError(error)) {
748
+ throw error;
749
+ }
750
+ } finally {
751
+ activeReclaimGuardIds.delete(guardId);
752
+ }
753
+ }
754
+ function claimObserverState(state, opts = {}) {
755
+ const stateFile = opts.stateFile ?? FML_OBSERVER_STATE_FILE;
756
+ const lockFile = opts.lockFile ?? FML_OBSERVER_LOCK_FILE;
757
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true, mode: 448 });
758
+ const nowMs = opts.nowMs ?? Date.now();
759
+ const reclaimFile = `${lockFile}.reclaim`;
760
+ let initialPublicationRetries = 0;
761
+ while (!fs.existsSync(lockFile)) {
762
+ const initialGuardId = acquireReclaimGuard(state, reclaimFile, nowMs, {
763
+ pidAlive: opts.pidAlive,
764
+ processIdentity: opts.processIdentity
765
+ });
766
+ if (!initialGuardId) return false;
767
+ let retryPublication = false;
768
+ try {
769
+ try {
770
+ if (writeLeaseAndState(
771
+ state,
772
+ stateFile,
773
+ lockFile,
774
+ reclaimFile,
775
+ initialGuardId
776
+ )) {
777
+ return true;
778
+ }
779
+ } catch (error) {
780
+ if (!isTransientObserverFileError(error) || initialPublicationRetries >= MAX_INITIAL_STATE_PUBLICATION_RETRIES) {
781
+ throw error;
782
+ }
783
+ initialPublicationRetries += 1;
784
+ retryPublication = true;
785
+ }
786
+ } finally {
787
+ releaseReclaimGuard(reclaimFile, initialGuardId);
788
+ }
789
+ if (!retryPublication) break;
790
+ }
791
+ let observedRaw;
792
+ let observedStat;
793
+ try {
794
+ observedRaw = fs.readFileSync(lockFile, "utf8");
795
+ observedStat = fs.statSync(lockFile);
796
+ } catch (error) {
797
+ if (error.code === "ENOENT") return false;
798
+ throw error;
799
+ }
800
+ const reclaimable = leaseCanBeReclaimed({
801
+ rawOwner: observedRaw,
802
+ lockMtimeMs: observedStat.mtimeMs,
803
+ stateFile,
804
+ nowMs,
805
+ pidAlive: opts.pidAlive ?? isPidAlive,
806
+ processIdentity: opts.processIdentity ?? getProcessIdentity,
807
+ liveOwnerPolicy: opts.liveOwnerPolicy ?? "heartbeat"
808
+ });
809
+ if (!reclaimable) return false;
810
+ const reclaimGuardId = acquireReclaimGuard(state, reclaimFile, nowMs, {
811
+ pidAlive: opts.pidAlive,
812
+ processIdentity: opts.processIdentity
813
+ });
814
+ if (!reclaimGuardId) return false;
815
+ try {
816
+ const currentRaw = fs.readFileSync(lockFile, "utf8");
817
+ const currentStat = fs.statSync(lockFile);
818
+ if (!ownsReclaimGuard(reclaimFile, reclaimGuardId) || currentRaw !== observedRaw || currentStat.dev !== observedStat.dev || currentStat.ino !== observedStat.ino || currentStat.mtimeMs !== observedStat.mtimeMs || !leaseCanBeReclaimed({
819
+ rawOwner: currentRaw,
820
+ lockMtimeMs: currentStat.mtimeMs,
821
+ stateFile,
822
+ nowMs,
823
+ pidAlive: opts.pidAlive ?? isPidAlive,
824
+ processIdentity: opts.processIdentity ?? getProcessIdentity,
825
+ liveOwnerPolicy: opts.liveOwnerPolicy ?? "heartbeat"
826
+ })) {
827
+ return false;
828
+ }
829
+ fs.unlinkSync(lockFile);
830
+ return writeLeaseAndState(
831
+ state,
832
+ stateFile,
833
+ lockFile,
834
+ reclaimFile,
835
+ reclaimGuardId
836
+ );
837
+ } catch (error) {
838
+ if (error.code === "ENOENT") return false;
839
+ throw error;
840
+ } finally {
841
+ releaseReclaimGuard(reclaimFile, reclaimGuardId);
842
+ }
843
+ }
844
+ function writeObserverState(state, stateFile = FML_OBSERVER_STATE_FILE) {
845
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true, mode: 448 });
846
+ const tempFile = `${stateFile}.${process.pid}.${randomUUID()}.tmp`;
847
+ try {
848
+ fs.writeFileSync(tempFile, serializeState(state), { mode: 384 });
849
+ fs.renameSync(tempFile, stateFile);
850
+ } catch (error) {
851
+ try {
852
+ fs.rmSync(tempFile, { force: true });
853
+ } catch {
854
+ }
855
+ throw error;
856
+ }
857
+ try {
858
+ fs.chmodSync(stateFile, 384);
859
+ } catch {
860
+ }
861
+ }
862
+ function updateObserverState(state, update, stateFile = FML_OBSERVER_STATE_FILE, lockFile = FML_OBSERVER_LOCK_FILE) {
863
+ const reclaimFile = `${lockFile}.reclaim`;
864
+ const reclaimGuardId = acquireReclaimGuard(state, reclaimFile, Date.now());
865
+ if (!reclaimGuardId) {
866
+ assertObserverStateMutationOwnership(
867
+ state.instanceId,
868
+ stateFile,
869
+ lockFile,
870
+ "observer lease ownership was lost"
871
+ );
872
+ throw new ObserverStateMutationBusyError();
873
+ }
874
+ try {
875
+ assertObserverStateMutationOwnership(
876
+ state.instanceId,
877
+ stateFile,
878
+ lockFile,
879
+ "observer lease ownership was lost"
880
+ );
881
+ if (!ownsReclaimGuardForMutation(reclaimFile, reclaimGuardId)) {
882
+ throw new ObserverStateMutationBusyError();
883
+ }
884
+ const next = { ...state, ...update };
885
+ try {
886
+ writeObserverState(next, stateFile);
887
+ } catch (error) {
888
+ if (isTransientObserverFileError(error)) {
889
+ assertObserverStateMutationOwnership(
890
+ state.instanceId,
891
+ stateFile,
892
+ lockFile,
893
+ "observer lease ownership was lost during state update"
894
+ );
895
+ if (!ownsReclaimGuardForMutation(reclaimFile, reclaimGuardId)) {
896
+ throw new ObserverStateMutationBusyError();
897
+ }
898
+ throw new ObserverStateMutationBusyError();
899
+ }
900
+ throw error;
901
+ }
902
+ assertObserverStateMutationOwnership(
903
+ state.instanceId,
904
+ stateFile,
905
+ lockFile,
906
+ "observer lease ownership was lost during state update"
907
+ );
908
+ if (!ownsReclaimGuardForMutation(reclaimFile, reclaimGuardId)) {
909
+ throw new ObserverStateMutationBusyError();
910
+ }
911
+ return next;
912
+ } finally {
913
+ releaseReclaimGuard(reclaimFile, reclaimGuardId);
914
+ }
915
+ }
916
+ function removeObserverStateIfOwned(instanceId, opts = {}) {
917
+ const stateFile = opts.stateFile ?? FML_OBSERVER_STATE_FILE;
918
+ const lockFile = opts.lockFile ?? FML_OBSERVER_LOCK_FILE;
919
+ const reclaimFile = `${lockFile}.reclaim`;
920
+ const reclaimGuardId = acquireReclaimGuard(
921
+ { pid: process.pid, instanceId },
922
+ reclaimFile,
923
+ Date.now()
924
+ );
925
+ if (!reclaimGuardId) {
926
+ return;
927
+ }
928
+ try {
929
+ if (!ownsReclaimGuard(reclaimFile, reclaimGuardId) || !observerLeaseIsOwned(instanceId, lockFile) || readObserverState(stateFile)?.instanceId !== instanceId) {
930
+ return;
931
+ }
932
+ fs.rmSync(stateFile, { force: true });
933
+ if (ownsReclaimGuard(reclaimFile, reclaimGuardId) && observerLeaseIsOwned(instanceId, lockFile)) {
934
+ fs.rmSync(lockFile, { force: true });
935
+ }
936
+ } finally {
937
+ releaseReclaimGuard(reclaimFile, reclaimGuardId);
938
+ }
939
+ }
940
+
941
+ export {
942
+ OBSERVER_HEARTBEAT_STALE_MS,
943
+ isPidAlive,
944
+ readObserverStateStatus,
945
+ createInitialObserverState,
946
+ ObserverStateMutationBusyError,
947
+ inspectObserverLease,
948
+ observerLeaseIsOwned,
949
+ getProcessIdentity,
950
+ claimObserverState,
951
+ updateObserverState,
952
+ removeObserverStateIfOwned
953
+ };