@fml-inc/fml 0.8.6 → 0.8.7
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/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -5
- package/dist/{chunk-5TOOBX7I.js → chunk-4YLSKFDU.js} +1140 -111
- package/dist/{chunk-V2XC7JTB.js → chunk-5JJQWV4G.js} +381 -95
- package/dist/{chunk-YFHZJPTC.js → chunk-6BTC3S7C.js} +4 -2
- package/dist/chunk-F57IFY3K.js +21 -0
- package/dist/{chunk-2DQQXBWM.js → chunk-LPSZCTTJ.js} +37 -1
- package/dist/chunk-XFALHTDL.js +277 -0
- package/dist/cli.js +168 -126
- package/dist/hook-handler.js +214 -22
- package/dist/observer-server.js +108 -22
- package/dist/sync-cycle.js +3 -2
- package/package.json +1 -1
- package/dist/chunk-E2MATWOU.js +0 -1225
package/dist/hook-handler.js
CHANGED
|
@@ -1,38 +1,171 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
hookCaptureIsDisabled
|
|
4
|
-
|
|
3
|
+
hookCaptureIsDisabled,
|
|
4
|
+
openObserverLog
|
|
5
|
+
} from "./chunk-LPSZCTTJ.js";
|
|
5
6
|
import {
|
|
6
7
|
getActiveHookDestinationBinding
|
|
7
8
|
} from "./chunk-PJWMSHHZ.js";
|
|
8
9
|
import {
|
|
10
|
+
HookInboxCapacityError,
|
|
11
|
+
OBSERVER_HEARTBEAT_STALE_MS,
|
|
12
|
+
OBSERVER_IDENTITY_HEADER,
|
|
13
|
+
OBSERVER_SERVICE_NAME,
|
|
9
14
|
SHELL_TOOL_NAMES,
|
|
10
15
|
createHookInboxEntry,
|
|
11
16
|
hookInboxDeliveryPayload,
|
|
12
17
|
normalizeObserverHookCapture,
|
|
13
18
|
persistHookInboxEntry,
|
|
14
|
-
readObserverAuthToken
|
|
15
|
-
|
|
19
|
+
readObserverAuthToken,
|
|
20
|
+
readObserverState,
|
|
21
|
+
recordHookInboxLoss
|
|
22
|
+
} from "./chunk-4YLSKFDU.js";
|
|
16
23
|
import {
|
|
17
24
|
resolveRepositoryRootFromCwd,
|
|
18
25
|
resolveRepositorySlugFromCwd
|
|
19
26
|
} from "./chunk-7V7G7NVF.js";
|
|
27
|
+
import {
|
|
28
|
+
resolvePackageRoot
|
|
29
|
+
} from "./chunk-F57IFY3K.js";
|
|
20
30
|
import {
|
|
21
31
|
FML_HOOK_CONTEXT_STATE_DIR,
|
|
22
32
|
FML_HOOK_INBOX_DIR,
|
|
23
33
|
FML_OBSERVER_AUTH_TOKEN_FILE,
|
|
34
|
+
FML_OBSERVER_DIR,
|
|
24
35
|
FML_OBSERVER_ENDPOINT_FILE,
|
|
36
|
+
FML_OBSERVER_STATE_FILE,
|
|
25
37
|
getValidToken
|
|
26
38
|
} from "./chunk-B3H2YWPJ.js";
|
|
27
39
|
import "./chunk-MLKGABMK.js";
|
|
28
40
|
|
|
29
41
|
// src/hooks/handler.ts
|
|
30
|
-
import
|
|
42
|
+
import fs3 from "fs";
|
|
31
43
|
|
|
32
|
-
// src/hooks/
|
|
33
|
-
import {
|
|
44
|
+
// src/hooks/observer-revival.ts
|
|
45
|
+
import { spawn } from "child_process";
|
|
34
46
|
import fs from "fs";
|
|
35
47
|
import path from "path";
|
|
48
|
+
var OBSERVER_REVIVAL_MIN_INTERVAL_MS = 10 * 6e4;
|
|
49
|
+
var FML_OBSERVER_REVIVAL_STAMP_DIR = path.join(
|
|
50
|
+
FML_OBSERVER_DIR,
|
|
51
|
+
"hook-revival"
|
|
52
|
+
);
|
|
53
|
+
var STAMP_PREFIX = "window-";
|
|
54
|
+
var RETAINED_STAMPS = 3;
|
|
55
|
+
var CLAIM_LOCK_NAME = "claim.lock";
|
|
56
|
+
var OBSERVER_REVIVAL_CLAIM_LOCK_STALE_MS = 3e4;
|
|
57
|
+
function launchDetachedObserver() {
|
|
58
|
+
const observerScript = path.join(resolvePackageRoot(), "bin", "observer");
|
|
59
|
+
const logFd = openObserverLog();
|
|
60
|
+
try {
|
|
61
|
+
fs.writeSync(
|
|
62
|
+
logFd,
|
|
63
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} hook handler relaunching the FML observer (recorded endpoint unreachable); PATH=${process.env.PATH ?? ""}
|
|
64
|
+
`
|
|
65
|
+
);
|
|
66
|
+
const child = spawn(process.execPath, [observerScript], {
|
|
67
|
+
detached: true,
|
|
68
|
+
env: process.env,
|
|
69
|
+
shell: false,
|
|
70
|
+
stdio: ["ignore", logFd, logFd],
|
|
71
|
+
windowsHide: true
|
|
72
|
+
});
|
|
73
|
+
child.once("error", () => {
|
|
74
|
+
});
|
|
75
|
+
child.unref();
|
|
76
|
+
} finally {
|
|
77
|
+
fs.closeSync(logFd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function pruneOldStamps(stampDir, currentWindow) {
|
|
81
|
+
try {
|
|
82
|
+
for (const name of fs.readdirSync(stampDir)) {
|
|
83
|
+
if (!name.startsWith(STAMP_PREFIX)) continue;
|
|
84
|
+
const window = Number(name.slice(STAMP_PREFIX.length));
|
|
85
|
+
if (Number.isFinite(window) && window < currentWindow - RETAINED_STAMPS) {
|
|
86
|
+
fs.rmSync(path.join(stampDir, name), { force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function previousWindowClaimedWithin(stampDir, window, now, minIntervalMs) {
|
|
93
|
+
const previous = path.join(stampDir, `${STAMP_PREFIX}${window - 1}`);
|
|
94
|
+
let contents;
|
|
95
|
+
try {
|
|
96
|
+
contents = fs.readFileSync(previous, "utf8");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return error.code !== "ENOENT";
|
|
99
|
+
}
|
|
100
|
+
const match = /^(\d+)(?:\s|$)/.exec(contents);
|
|
101
|
+
if (!match) return true;
|
|
102
|
+
return now - Number(match[1]) < minIntervalMs;
|
|
103
|
+
}
|
|
104
|
+
function acquireClaimLock(lockDir) {
|
|
105
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(lockDir, { mode: 448 });
|
|
108
|
+
return true;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== "EEXIST") return false;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const age = Date.now() - fs.statSync(lockDir).mtimeMs;
|
|
114
|
+
if (age < OBSERVER_REVIVAL_CLAIM_LOCK_STALE_MS) return false;
|
|
115
|
+
fs.rmdirSync(lockDir);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function claimWindow(stampDir, window, now, minIntervalMs) {
|
|
123
|
+
if (previousWindowClaimedWithin(stampDir, window, now, minIntervalMs)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const stamp = path.join(stampDir, `${STAMP_PREFIX}${window}`);
|
|
127
|
+
let fd;
|
|
128
|
+
try {
|
|
129
|
+
fd = fs.openSync(stamp, "wx", 384);
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
fs.writeSync(fd, `${now} pid=${process.pid}
|
|
135
|
+
`);
|
|
136
|
+
} finally {
|
|
137
|
+
fs.closeSync(fd);
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
function requestObserverRevival(options = {}) {
|
|
142
|
+
const stampDir = options.stampDir ?? FML_OBSERVER_REVIVAL_STAMP_DIR;
|
|
143
|
+
const minIntervalMs = options.minIntervalMs ?? OBSERVER_REVIVAL_MIN_INTERVAL_MS;
|
|
144
|
+
const now = (options.nowMs ?? Date.now)();
|
|
145
|
+
const window = Math.floor(now / minIntervalMs);
|
|
146
|
+
try {
|
|
147
|
+
fs.mkdirSync(stampDir, { recursive: true, mode: 448 });
|
|
148
|
+
const lockDir = path.join(stampDir, CLAIM_LOCK_NAME);
|
|
149
|
+
if (!acquireClaimLock(lockDir)) return false;
|
|
150
|
+
let claimed;
|
|
151
|
+
try {
|
|
152
|
+
claimed = claimWindow(stampDir, window, now, minIntervalMs);
|
|
153
|
+
} finally {
|
|
154
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
if (!claimed) return false;
|
|
157
|
+
pruneOldStamps(stampDir, window);
|
|
158
|
+
(options.launch ?? launchDetachedObserver)();
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/hooks/context.ts
|
|
166
|
+
import { createHash } from "crypto";
|
|
167
|
+
import fs2 from "fs";
|
|
168
|
+
import path2 from "path";
|
|
36
169
|
var HOOK_CONTEXT_ENDPOINT = "/v1/panopticon/hook-context";
|
|
37
170
|
var HOOK_CONTEXT_TIMEOUT_MS = 2e3;
|
|
38
171
|
var MAX_ADDITIONAL_CONTEXT_CHARS = 3700;
|
|
@@ -73,11 +206,11 @@ function hookContextInjectionId(target2, syncId) {
|
|
|
73
206
|
}
|
|
74
207
|
function markerPath(stateDir, kind, value) {
|
|
75
208
|
const digest = createHash("sha256").update(value).digest("hex");
|
|
76
|
-
return
|
|
209
|
+
return path2.join(stateDir, kind, digest);
|
|
77
210
|
}
|
|
78
211
|
function ensureMarkerDirectory(filePath) {
|
|
79
212
|
try {
|
|
80
|
-
|
|
213
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true, mode: 448 });
|
|
81
214
|
return true;
|
|
82
215
|
} catch {
|
|
83
216
|
return false;
|
|
@@ -87,12 +220,12 @@ function isSubsequentUserPrompt(sessionId, eventSyncId, stateDir = FML_HOOK_CONT
|
|
|
87
220
|
const filePath = markerPath(stateDir, "prompt", sessionId);
|
|
88
221
|
if (!ensureMarkerDirectory(filePath)) return false;
|
|
89
222
|
try {
|
|
90
|
-
|
|
223
|
+
fs2.writeFileSync(filePath, eventSyncId, { flag: "wx", mode: 384 });
|
|
91
224
|
return false;
|
|
92
225
|
} catch (error) {
|
|
93
226
|
if (error.code !== "EEXIST") return false;
|
|
94
227
|
try {
|
|
95
|
-
return
|
|
228
|
+
return fs2.readFileSync(filePath, "utf8") !== eventSyncId;
|
|
96
229
|
} catch {
|
|
97
230
|
return false;
|
|
98
231
|
}
|
|
@@ -100,7 +233,7 @@ function isSubsequentUserPrompt(sessionId, eventSyncId, stateDir = FML_HOOK_CONT
|
|
|
100
233
|
}
|
|
101
234
|
function pathContextWasEmitted(sessionId, filePath, stateDir) {
|
|
102
235
|
try {
|
|
103
|
-
return
|
|
236
|
+
return fs2.existsSync(
|
|
104
237
|
markerPath(stateDir, "path", `${sessionId}:${filePath}`)
|
|
105
238
|
);
|
|
106
239
|
} catch {
|
|
@@ -111,7 +244,7 @@ function claimPathContext(sessionId, filePath, stateDir) {
|
|
|
111
244
|
const claim = markerPath(stateDir, "path", `${sessionId}:${filePath}`);
|
|
112
245
|
if (!ensureMarkerDirectory(claim)) return false;
|
|
113
246
|
try {
|
|
114
|
-
|
|
247
|
+
fs2.writeFileSync(claim, "", { flag: "wx", mode: 384 });
|
|
115
248
|
return true;
|
|
116
249
|
} catch {
|
|
117
250
|
return false;
|
|
@@ -120,18 +253,18 @@ function claimPathContext(sessionId, filePath, stateDir) {
|
|
|
120
253
|
function pruneOldMarkers(directory, nowMs) {
|
|
121
254
|
if ((nowMs + process.pid) % 256 !== 0) return;
|
|
122
255
|
for (const kind of ["prompt", "path"]) {
|
|
123
|
-
const markerDirectory =
|
|
256
|
+
const markerDirectory = path2.join(directory, kind);
|
|
124
257
|
let names;
|
|
125
258
|
try {
|
|
126
|
-
names =
|
|
259
|
+
names = fs2.readdirSync(markerDirectory).slice(0, 1e3);
|
|
127
260
|
} catch {
|
|
128
261
|
continue;
|
|
129
262
|
}
|
|
130
263
|
for (const name of names) {
|
|
131
|
-
const filePath =
|
|
264
|
+
const filePath = path2.join(markerDirectory, name);
|
|
132
265
|
try {
|
|
133
|
-
if (nowMs -
|
|
134
|
-
|
|
266
|
+
if (nowMs - fs2.statSync(filePath).mtimeMs > STATE_MAX_AGE_MS) {
|
|
267
|
+
fs2.rmSync(filePath, { force: true });
|
|
135
268
|
}
|
|
136
269
|
} catch {
|
|
137
270
|
}
|
|
@@ -142,7 +275,7 @@ function canonicalHookContextFilePath(filePath, cwd, repositoryRoot) {
|
|
|
142
275
|
const windows = [filePath, cwd, repositoryRoot].some(
|
|
143
276
|
(value) => value !== void 0 && value !== null && /^[A-Za-z]:[\\/]/.test(value)
|
|
144
277
|
);
|
|
145
|
-
const pathApi = windows ?
|
|
278
|
+
const pathApi = windows ? path2.win32 : path2.posix;
|
|
146
279
|
const normalized = pathApi.normalize(filePath);
|
|
147
280
|
const resolved = pathApi.isAbsolute(normalized) ? normalized : cwd && pathApi.isAbsolute(cwd) ? pathApi.resolve(cwd, normalized) : normalized;
|
|
148
281
|
if (repositoryRoot && pathApi.isAbsolute(resolved)) {
|
|
@@ -557,7 +690,7 @@ function parseHookTarget(value) {
|
|
|
557
690
|
}
|
|
558
691
|
function observerEndpoint(endpointFile) {
|
|
559
692
|
try {
|
|
560
|
-
const parsed = JSON.parse(
|
|
693
|
+
const parsed = JSON.parse(fs3.readFileSync(endpointFile, "utf8"));
|
|
561
694
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
562
695
|
return null;
|
|
563
696
|
}
|
|
@@ -659,6 +792,7 @@ async function processHookHandler(target2, opts, includeContext) {
|
|
|
659
792
|
const inboxDir = opts.inboxDir ?? FML_HOOK_INBOX_DIR;
|
|
660
793
|
let inboxEntry;
|
|
661
794
|
let persisted = false;
|
|
795
|
+
let refusedBytes;
|
|
662
796
|
let destination = null;
|
|
663
797
|
if (event) {
|
|
664
798
|
try {
|
|
@@ -673,7 +807,10 @@ async function processHookHandler(target2, opts, includeContext) {
|
|
|
673
807
|
try {
|
|
674
808
|
persistHookInboxEntry(inboxEntry, inboxDir, opts.inboxLimits);
|
|
675
809
|
persisted = true;
|
|
676
|
-
} catch {
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (error instanceof HookInboxCapacityError) {
|
|
812
|
+
refusedBytes = Buffer.byteLength(JSON.stringify(inboxEntry), "utf8") + 1;
|
|
813
|
+
}
|
|
677
814
|
}
|
|
678
815
|
}
|
|
679
816
|
const contextPromise = includeContext && event && destination ? opts.requestContext ? opts.requestContext(target2, event, destination) : requestServerHookContext(target2, event, destination, {
|
|
@@ -687,6 +824,13 @@ async function processHookHandler(target2, opts, includeContext) {
|
|
|
687
824
|
const authToken = readObserverAuthToken(
|
|
688
825
|
opts.authTokenFile ?? FML_OBSERVER_AUTH_TOKEN_FILE
|
|
689
826
|
);
|
|
827
|
+
const reviveObserver = () => {
|
|
828
|
+
try {
|
|
829
|
+
(opts.reviveObserver ?? requestObserverRevival)();
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
let deliveryUnconfirmed = false;
|
|
690
834
|
const deliveryPromise = (async () => {
|
|
691
835
|
if (!endpoint || !authToken) return persisted;
|
|
692
836
|
const host = endpoint.host === "::1" ? "[::1]" : endpoint.host;
|
|
@@ -709,8 +853,21 @@ async function processHookHandler(target2, opts, includeContext) {
|
|
|
709
853
|
}
|
|
710
854
|
);
|
|
711
855
|
await response.arrayBuffer();
|
|
856
|
+
if (response.headers.get(OBSERVER_IDENTITY_HEADER) !== OBSERVER_SERVICE_NAME) {
|
|
857
|
+
reviveObserver();
|
|
858
|
+
return persisted;
|
|
859
|
+
}
|
|
712
860
|
return response.ok || persisted;
|
|
713
|
-
} catch {
|
|
861
|
+
} catch (error) {
|
|
862
|
+
if (isTimeoutOrAbort(error)) {
|
|
863
|
+
deliveryUnconfirmed = true;
|
|
864
|
+
if (observerHeartbeatIsStale(opts.stateFile ?? FML_OBSERVER_STATE_FILE)) {
|
|
865
|
+
reviveObserver();
|
|
866
|
+
}
|
|
867
|
+
return persisted;
|
|
868
|
+
}
|
|
869
|
+
if (!requestNeverReachedObserver(error)) deliveryUnconfirmed = true;
|
|
870
|
+
reviveObserver();
|
|
714
871
|
return persisted;
|
|
715
872
|
}
|
|
716
873
|
})();
|
|
@@ -718,8 +875,43 @@ async function processHookHandler(target2, opts, includeContext) {
|
|
|
718
875
|
deliveryPromise,
|
|
719
876
|
contextPromise.catch(() => ({}))
|
|
720
877
|
]);
|
|
878
|
+
if (refusedBytes !== void 0 && !captured && !deliveryUnconfirmed) {
|
|
879
|
+
try {
|
|
880
|
+
recordHookInboxLoss(inboxDir, refusedBytes);
|
|
881
|
+
} catch {
|
|
882
|
+
}
|
|
883
|
+
}
|
|
721
884
|
return { captured, output: output2 };
|
|
722
885
|
}
|
|
886
|
+
function isTimeoutOrAbort(error) {
|
|
887
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
888
|
+
}
|
|
889
|
+
var NEVER_CONNECTED_CODES = /* @__PURE__ */ new Set([
|
|
890
|
+
"ECONNREFUSED",
|
|
891
|
+
"EHOSTUNREACH",
|
|
892
|
+
"ENETUNREACH",
|
|
893
|
+
"ENOTFOUND",
|
|
894
|
+
"EADDRNOTAVAIL"
|
|
895
|
+
]);
|
|
896
|
+
function requestNeverReachedObserver(error) {
|
|
897
|
+
const seen = /* @__PURE__ */ new Set();
|
|
898
|
+
let current = error;
|
|
899
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
900
|
+
seen.add(current);
|
|
901
|
+
const code = current.code;
|
|
902
|
+
if (code !== void 0 && NEVER_CONNECTED_CODES.has(code)) return true;
|
|
903
|
+
for (const known of NEVER_CONNECTED_CODES) {
|
|
904
|
+
if (current.message.includes(known)) return true;
|
|
905
|
+
}
|
|
906
|
+
current = current.cause;
|
|
907
|
+
}
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
function observerHeartbeatIsStale(stateFile) {
|
|
911
|
+
const state = readObserverState(stateFile);
|
|
912
|
+
if (!state) return true;
|
|
913
|
+
return Date.now() - state.heartbeatAtMs > OBSERVER_HEARTBEAT_STALE_MS;
|
|
914
|
+
}
|
|
723
915
|
async function runHookHandlerForOutput(target2, opts = {}) {
|
|
724
916
|
return (await processHookHandler(target2, opts, true)).output;
|
|
725
917
|
}
|
package/dist/observer-server.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
ObserverStateMutationBusyError,
|
|
4
3
|
checkForUpdate,
|
|
5
|
-
claimObserverState,
|
|
6
|
-
createInitialObserverState,
|
|
7
4
|
isUpdatableVersion,
|
|
8
5
|
isWinShim,
|
|
9
|
-
observerLeaseIsOwned,
|
|
10
6
|
quoteWinArg,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
updateObserverState
|
|
14
|
-
} from "./chunk-E2MATWOU.js";
|
|
7
|
+
resolveBin
|
|
8
|
+
} from "./chunk-XFALHTDL.js";
|
|
15
9
|
import {
|
|
16
10
|
createHookSyncLoop,
|
|
17
11
|
getActiveHookDestinationBinding
|
|
@@ -20,25 +14,33 @@ import {
|
|
|
20
14
|
OBSERVER_PROTOCOL_VERSION,
|
|
21
15
|
OBSERVER_SERVICE_NAME,
|
|
22
16
|
ObserverHookSpoolEntryTooLargeError,
|
|
17
|
+
ObserverStateMutationBusyError,
|
|
23
18
|
acknowledgeHookInboxEntry,
|
|
19
|
+
claimObserverState,
|
|
20
|
+
createInitialObserverState,
|
|
24
21
|
createObserverHttpServer,
|
|
25
22
|
drainHookInbox,
|
|
26
23
|
getOrCreateObserverAuthToken,
|
|
27
24
|
normalizeObserverHookCapture,
|
|
25
|
+
observerLeaseIsOwned,
|
|
28
26
|
openObserverDatabase,
|
|
29
27
|
parseHookCaptureRequest,
|
|
30
28
|
parseHookInboxMetadata,
|
|
31
29
|
readHookInboxStatusAsync,
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
removeObserverStateIfOwned,
|
|
31
|
+
retireHookInboxEntry,
|
|
32
|
+
updateObserverState
|
|
33
|
+
} from "./chunk-4YLSKFDU.js";
|
|
34
34
|
import "./chunk-7V7G7NVF.js";
|
|
35
35
|
import {
|
|
36
36
|
ObserverSyncCycleAbortedError,
|
|
37
37
|
runObserverSyncCycleSubprocess
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-6BTC3S7C.js";
|
|
39
39
|
import {
|
|
40
|
-
createCliRawSync
|
|
41
|
-
|
|
40
|
+
createCliRawSync,
|
|
41
|
+
invalidateCliRawSyncDiscoveryCache
|
|
42
|
+
} from "./chunk-5JJQWV4G.js";
|
|
43
|
+
import "./chunk-F57IFY3K.js";
|
|
42
44
|
import {
|
|
43
45
|
FML_OBSERVER_AUTH_TOKEN_FILE,
|
|
44
46
|
FML_OBSERVER_ENDPOINT_FILE,
|
|
@@ -1046,7 +1048,7 @@ function parseOtlpSpoolMaxBytes(configured) {
|
|
|
1046
1048
|
}
|
|
1047
1049
|
|
|
1048
1050
|
// src/observer/sync-loop.ts
|
|
1049
|
-
var OBSERVER_IDLE_INTERVAL_MS =
|
|
1051
|
+
var OBSERVER_IDLE_INTERVAL_MS = 5 * 6e4;
|
|
1050
1052
|
var OBSERVER_BUSY_INTERVAL_MS = 2e3;
|
|
1051
1053
|
var OBSERVER_AUTH_INTERVAL_MS = 6e4;
|
|
1052
1054
|
var OBSERVER_MAX_TIMER_DELAY_MS = 2147483647;
|
|
@@ -1084,8 +1086,14 @@ function createObserverSyncLoop(opts) {
|
|
|
1084
1086
|
const runOnce = opts.runOnce ?? runObserverSyncCycleSubprocess;
|
|
1085
1087
|
let stopping = false;
|
|
1086
1088
|
let wakePending = false;
|
|
1089
|
+
let nudgePending = false;
|
|
1087
1090
|
let wakeWaiter = null;
|
|
1088
1091
|
let activeAbort = null;
|
|
1092
|
+
const nudge = () => {
|
|
1093
|
+
nudgePending = true;
|
|
1094
|
+
wakePending = true;
|
|
1095
|
+
wakeWaiter?.();
|
|
1096
|
+
};
|
|
1089
1097
|
const wake = () => {
|
|
1090
1098
|
wakePending = true;
|
|
1091
1099
|
activeAbort?.abort();
|
|
@@ -1121,6 +1129,10 @@ function createObserverSyncLoop(opts) {
|
|
|
1121
1129
|
};
|
|
1122
1130
|
const done = (async () => {
|
|
1123
1131
|
while (!stopping) {
|
|
1132
|
+
if (nudgePending) {
|
|
1133
|
+
nudgePending = false;
|
|
1134
|
+
opts.beforeNudgedCycle?.();
|
|
1135
|
+
}
|
|
1124
1136
|
const attemptedAtMs = nowMs();
|
|
1125
1137
|
opts.onUpdate({
|
|
1126
1138
|
phase: "syncing",
|
|
@@ -1189,6 +1201,7 @@ function createObserverSyncLoop(opts) {
|
|
|
1189
1201
|
})();
|
|
1190
1202
|
return {
|
|
1191
1203
|
done,
|
|
1204
|
+
nudge,
|
|
1192
1205
|
wake,
|
|
1193
1206
|
stop: () => {
|
|
1194
1207
|
if (stopping) return;
|
|
@@ -1199,6 +1212,47 @@ function createObserverSyncLoop(opts) {
|
|
|
1199
1212
|
};
|
|
1200
1213
|
}
|
|
1201
1214
|
|
|
1215
|
+
// src/observer/session-end-sync-wake.ts
|
|
1216
|
+
var SESSION_END_SYNC_WAKE_INTERVAL_MS = 6e4;
|
|
1217
|
+
function createSessionEndSyncWakeScheduler(options) {
|
|
1218
|
+
const intervalMs = Math.max(
|
|
1219
|
+
1,
|
|
1220
|
+
options.intervalMs ?? SESSION_END_SYNC_WAKE_INTERVAL_MS
|
|
1221
|
+
);
|
|
1222
|
+
const nowMs = options.nowMs ?? Date.now;
|
|
1223
|
+
let lastWakeAtMs = null;
|
|
1224
|
+
let trailingTimer = null;
|
|
1225
|
+
let stopped = false;
|
|
1226
|
+
const wake = () => {
|
|
1227
|
+
lastWakeAtMs = nowMs();
|
|
1228
|
+
options.onWake();
|
|
1229
|
+
};
|
|
1230
|
+
return {
|
|
1231
|
+
request: () => {
|
|
1232
|
+
if (stopped) return;
|
|
1233
|
+
const now = nowMs();
|
|
1234
|
+
const elapsedMs = lastWakeAtMs === null ? intervalMs : now - lastWakeAtMs;
|
|
1235
|
+
if (elapsedMs >= intervalMs || elapsedMs < 0) {
|
|
1236
|
+
if (trailingTimer) clearTimeout(trailingTimer);
|
|
1237
|
+
trailingTimer = null;
|
|
1238
|
+
wake();
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (trailingTimer) return;
|
|
1242
|
+
trailingTimer = setTimeout(() => {
|
|
1243
|
+
trailingTimer = null;
|
|
1244
|
+
if (!stopped) wake();
|
|
1245
|
+
}, intervalMs - elapsedMs);
|
|
1246
|
+
trailingTimer.unref();
|
|
1247
|
+
},
|
|
1248
|
+
stop: () => {
|
|
1249
|
+
stopped = true;
|
|
1250
|
+
if (trailingTimer) clearTimeout(trailingTimer);
|
|
1251
|
+
trailingTimer = null;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1202
1256
|
// src/observer/auto-update.ts
|
|
1203
1257
|
var AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1204
1258
|
var AUTO_UPDATE_JITTER_MS = 10 * 60 * 1e3;
|
|
@@ -1285,12 +1339,15 @@ function createAutoUpdater(dependencies) {
|
|
|
1285
1339
|
}
|
|
1286
1340
|
|
|
1287
1341
|
// src/observer-server.ts
|
|
1288
|
-
var HEARTBEAT_INTERVAL_MS =
|
|
1342
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1289
1343
|
var STATE_UPDATE_RETRY_MS = 50;
|
|
1290
1344
|
var STARTUP_STATE_UPDATE_TIMEOUT_MS = 5e3;
|
|
1291
1345
|
var PORT_FALLBACK_COUNT = 10;
|
|
1292
1346
|
var HOOK_REJECTION_WARNING_INTERVAL_MS = 6e4;
|
|
1293
|
-
var HOOK_INBOX_DRAIN_INTERVAL_MS =
|
|
1347
|
+
var HOOK_INBOX_DRAIN_INTERVAL_MS = 15e3;
|
|
1348
|
+
var HOOK_INBOX_DRAIN_BUDGET_MS = 5e3;
|
|
1349
|
+
var HOOK_INBOX_DRAIN_ENTRIES = 100;
|
|
1350
|
+
var HOOK_INBOX_DRAIN_BULK_ENTRIES = 1e3;
|
|
1294
1351
|
var HOOK_QUARANTINE_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1295
1352
|
var OBSERVER_DIRECTORY_LEASE = {
|
|
1296
1353
|
stateFile: FML_OBSERVER_STATE_FILE,
|
|
@@ -1436,6 +1493,15 @@ var hookSyncLoop = null;
|
|
|
1436
1493
|
var autoUpdater = null;
|
|
1437
1494
|
var otlpSyncLoop = null;
|
|
1438
1495
|
var pendingStateUpdate = null;
|
|
1496
|
+
var sessionEndSyncWakeScheduler = createSessionEndSyncWakeScheduler({
|
|
1497
|
+
onWake: () => {
|
|
1498
|
+
if (syncLoop) {
|
|
1499
|
+
syncLoop.nudge();
|
|
1500
|
+
} else {
|
|
1501
|
+
invalidateCliRawSyncDiscoveryCache();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1439
1505
|
var authToken;
|
|
1440
1506
|
var database;
|
|
1441
1507
|
try {
|
|
@@ -1480,7 +1546,7 @@ var hookRejectionStatus = () => ({
|
|
|
1480
1546
|
...lastRejectedReason !== void 0 ? { lastRejectedReason } : {},
|
|
1481
1547
|
...lastRejectedTarget !== void 0 ? { lastRejectedTarget } : {}
|
|
1482
1548
|
});
|
|
1483
|
-
var packageVersion = true ? "0.8.
|
|
1549
|
+
var packageVersion = true ? "0.8.7+af45d2af" : "dev";
|
|
1484
1550
|
var health = () => ({
|
|
1485
1551
|
status: "ok",
|
|
1486
1552
|
service: OBSERVER_SERVICE_NAME,
|
|
@@ -1546,6 +1612,7 @@ var shutdown = async () => {
|
|
|
1546
1612
|
if (hookInboxDrainTimer) clearInterval(hookInboxDrainTimer);
|
|
1547
1613
|
if (hookQuarantinePruneTimer) clearInterval(hookQuarantinePruneTimer);
|
|
1548
1614
|
if (stateUpdateRetryTimer) clearTimeout(stateUpdateRetryTimer);
|
|
1615
|
+
sessionEndSyncWakeScheduler.stop();
|
|
1549
1616
|
autoUpdater?.stop();
|
|
1550
1617
|
if (bothObserverLeasesAreOwned(state.instanceId)) {
|
|
1551
1618
|
try {
|
|
@@ -1674,6 +1741,9 @@ var captureHookRequest = (request, metadata) => {
|
|
|
1674
1741
|
if (captured && event.eventType === "SessionStart") {
|
|
1675
1742
|
autoUpdater?.onSessionStart();
|
|
1676
1743
|
}
|
|
1744
|
+
if (captured && (event.eventType === "Stop" || event.eventType === "SessionEnd")) {
|
|
1745
|
+
sessionEndSyncWakeScheduler.request();
|
|
1746
|
+
}
|
|
1677
1747
|
hookSyncLoop?.wake();
|
|
1678
1748
|
return "committed";
|
|
1679
1749
|
};
|
|
@@ -1681,13 +1751,26 @@ var captureHookInboxEntry = (entry) => captureHookRequest(entry.request, entry);
|
|
|
1681
1751
|
var drainPendingHookInbox = () => {
|
|
1682
1752
|
if (hookInboxDrainPromise) return hookInboxDrainPromise;
|
|
1683
1753
|
const drainPromise = (async () => {
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1754
|
+
const startedAtMs = Date.now();
|
|
1755
|
+
let sawUnsupported = false;
|
|
1756
|
+
let batch = HOOK_INBOX_DRAIN_ENTRIES;
|
|
1757
|
+
for (; ; ) {
|
|
1758
|
+
const result = await drainHookInbox(captureHookInboxEntry, void 0, {
|
|
1759
|
+
maximumEntries: batch
|
|
1760
|
+
});
|
|
1761
|
+
if (result.malformed > 0) recordHookRejection("inbox_malformed");
|
|
1762
|
+
if (result.failed > 0) recordHookRejection("inbox_drain_failed");
|
|
1763
|
+
if (result.unsupported > 0) sawUnsupported = true;
|
|
1764
|
+
const handled = result.drained + result.rejected + result.malformed;
|
|
1765
|
+
if (handled === 0 || result.failed > 0 || Date.now() - startedAtMs >= HOOK_INBOX_DRAIN_BUDGET_MS) {
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
batch = handled >= batch ? HOOK_INBOX_DRAIN_BULK_ENTRIES : HOOK_INBOX_DRAIN_ENTRIES;
|
|
1769
|
+
}
|
|
1770
|
+
if (sawUnsupported && !unsupportedInboxSchemaReported) {
|
|
1688
1771
|
recordHookRejection("inbox_unsupported_schema");
|
|
1689
1772
|
unsupportedInboxSchemaReported = true;
|
|
1690
|
-
} else if (
|
|
1773
|
+
} else if (!sawUnsupported) {
|
|
1691
1774
|
unsupportedInboxSchemaReported = false;
|
|
1692
1775
|
}
|
|
1693
1776
|
})();
|
|
@@ -1800,6 +1883,9 @@ try {
|
|
|
1800
1883
|
if (!shuttingDown) scheduleFatalShutdown(error);
|
|
1801
1884
|
});
|
|
1802
1885
|
syncLoop = createObserverSyncLoop({
|
|
1886
|
+
// Clear at the cycle boundary, after any in-flight worker had a chance to
|
|
1887
|
+
// republish its older walk, so a hook nudge always discovers new files.
|
|
1888
|
+
beforeNudgedCycle: invalidateCliRawSyncDiscoveryCache,
|
|
1803
1889
|
onUpdate: (update) => {
|
|
1804
1890
|
updateState(update);
|
|
1805
1891
|
},
|
package/dist/sync-cycle.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runObserverSyncCycle
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-6BTC3S7C.js";
|
|
5
|
+
import "./chunk-5JJQWV4G.js";
|
|
6
|
+
import "./chunk-F57IFY3K.js";
|
|
6
7
|
import "./chunk-B3H2YWPJ.js";
|
|
7
8
|
import "./chunk-MLKGABMK.js";
|
|
8
9
|
|