@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.
@@ -1,38 +1,171 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- hookCaptureIsDisabled
4
- } from "./chunk-2DQQXBWM.js";
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
- } from "./chunk-5TOOBX7I.js";
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 fs2 from "fs";
42
+ import fs3 from "fs";
31
43
 
32
- // src/hooks/context.ts
33
- import { createHash } from "crypto";
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 path.join(stateDir, kind, digest);
209
+ return path2.join(stateDir, kind, digest);
77
210
  }
78
211
  function ensureMarkerDirectory(filePath) {
79
212
  try {
80
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 448 });
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
- fs.writeFileSync(filePath, eventSyncId, { flag: "wx", mode: 384 });
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 fs.readFileSync(filePath, "utf8") !== eventSyncId;
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 fs.existsSync(
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
- fs.writeFileSync(claim, "", { flag: "wx", mode: 384 });
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 = path.join(directory, kind);
256
+ const markerDirectory = path2.join(directory, kind);
124
257
  let names;
125
258
  try {
126
- names = fs.readdirSync(markerDirectory).slice(0, 1e3);
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 = path.join(markerDirectory, name);
264
+ const filePath = path2.join(markerDirectory, name);
132
265
  try {
133
- if (nowMs - fs.statSync(filePath).mtimeMs > STATE_MAX_AGE_MS) {
134
- fs.rmSync(filePath, { force: true });
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 ? path.win32 : path.posix;
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(fs2.readFileSync(endpointFile, "utf8"));
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
  }
@@ -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
- removeObserverStateIfOwned,
12
- resolveBin,
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
- retireHookInboxEntry
33
- } from "./chunk-5TOOBX7I.js";
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-YFHZJPTC.js";
38
+ } from "./chunk-6BTC3S7C.js";
39
39
  import {
40
- createCliRawSync
41
- } from "./chunk-V2XC7JTB.js";
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 = 3e4;
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 = 5e3;
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 = 1e3;
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.6+f7c7191d" : "dev";
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 result = await drainHookInbox(captureHookInboxEntry);
1685
- if (result.malformed > 0) recordHookRejection("inbox_malformed");
1686
- if (result.failed > 0) recordHookRejection("inbox_drain_failed");
1687
- if (result.unsupported > 0 && !unsupportedInboxSchemaReported) {
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 (result.unsupported === 0) {
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
  },
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runObserverSyncCycle
4
- } from "./chunk-YFHZJPTC.js";
5
- import "./chunk-V2XC7JTB.js";
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fml-inc/fml",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "FML CLI and agent tools",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",