@fml-inc/fml 0.8.9 → 0.8.10-dev.1

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,6 +1,6 @@
1
1
  {
2
2
  "name": "fml",
3
- "version": "0.8.9",
3
+ "version": "0.8.10-dev.1",
4
4
  "description": "FML agent tools for Claude Code",
5
5
  "mcpServers": {
6
6
  "fml": {
@@ -695,6 +695,7 @@ var MAX_HOOK_DESTINATION_IDENTITY_BYTES = 4 * 1024;
695
695
  var DEFAULT_OTLP_OUTBOX_BATCH_SIZE = 100;
696
696
  var MAX_OTLP_OUTBOX_BATCH_SIZE = 100;
697
697
  var MAX_OTLP_ENVELOPE_BYTES = 16 * 1024 * 1024;
698
+ var DEFAULT_OTLP_SPOOL_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
698
699
  var DEFAULT_OTLP_SPOOL_MAX_BYTES = 1024 * 1024 * 1024;
699
700
  var MAX_OTLP_ENVELOPE_ID_BYTES = 1024;
700
701
  var MAX_OTLP_OBSERVER_INSTANCE_ID_BYTES = 4 * 1024;
@@ -703,8 +704,6 @@ var MAX_OTLP_DROP_REASON_BYTES = 4 * 1024;
703
704
  var MAX_OTLP_DESTINATION_ORGANIZATION_ID_BYTES = 128;
704
705
  var MAX_OTLP_DESTINATION_ORGANIZATION_SLUG_BYTES = 4 * 1024;
705
706
  var OWNERLESS_OTLP_DROP_REASON = "ownerless";
706
- var OTLP_CAPACITY_DROP_REASON = "spool_capacity";
707
- var OTLP_ENTRY_TOO_LARGE_DROP_REASON = "spool_entry_too_large";
708
707
  var OTLP_ROW_FIXED_LOGICAL_BYTES = 256;
709
708
  var MAX_TIMESTAMP_MS = 864e13;
710
709
  var QUARANTINE_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
@@ -1002,15 +1001,23 @@ function enforceOwnerOnlyMode(filePath, mode) {
1002
1001
  function openObserverDatabase(databaseFile, options = {}) {
1003
1002
  const maxHookSpoolBytes = options.maxHookSpoolBytes;
1004
1003
  const maxOtlpSpoolBytes = options.maxOtlpSpoolBytes ?? DEFAULT_OTLP_SPOOL_MAX_BYTES;
1005
- const maxHookQuarantineAgeMs = options.maxHookQuarantineAgeMs;
1006
- if (maxHookSpoolBytes !== void 0 && (!Number.isSafeInteger(maxHookSpoolBytes) || maxHookSpoolBytes < 1)) {
1004
+ if (!Number.isSafeInteger(maxOtlpSpoolBytes) || maxOtlpSpoolBytes < 1) {
1007
1005
  throw new Error(
1008
- "Hook spool maximum must be a positive safe integer number of bytes"
1006
+ "OTLP spool maximum must be a positive safe integer number of bytes"
1009
1007
  );
1010
1008
  }
1011
- if (!Number.isSafeInteger(maxOtlpSpoolBytes) || maxOtlpSpoolBytes < 1) {
1009
+ const otlpLowWaterBytes = Math.floor(maxOtlpSpoolBytes * 0.9);
1010
+ let otlpCapacityCleanupActive = false;
1011
+ const maxOtlpSpoolAgeMs = options.maxOtlpSpoolAgeMs ?? DEFAULT_OTLP_SPOOL_MAX_AGE_MS;
1012
+ if (!Number.isSafeInteger(maxOtlpSpoolAgeMs) || maxOtlpSpoolAgeMs < 1) {
1012
1013
  throw new Error(
1013
- "OTLP spool maximum must be a positive safe integer number of bytes"
1014
+ "OTLP spool maximum age must be a positive safe integer number of milliseconds"
1015
+ );
1016
+ }
1017
+ const maxHookQuarantineAgeMs = options.maxHookQuarantineAgeMs;
1018
+ if (maxHookSpoolBytes !== void 0 && (!Number.isSafeInteger(maxHookSpoolBytes) || maxHookSpoolBytes < 1)) {
1019
+ throw new Error(
1020
+ "Hook spool maximum must be a positive safe integer number of bytes"
1014
1021
  );
1015
1022
  }
1016
1023
  if (maxHookQuarantineAgeMs !== void 0 && (!Number.isSafeInteger(maxHookQuarantineAgeMs) || maxHookQuarantineAgeMs < 1)) {
@@ -2031,27 +2038,24 @@ function openObserverDatabase(databaseFile, options = {}) {
2031
2038
  last_drop_reason = ?
2032
2039
  WHERE singleton = 1
2033
2040
  `);
2034
- const otlpCapacityEvictionPlan = database.prepare(`
2035
- WITH ordered AS (
2036
- SELECT
2037
- logical_bytes,
2038
- length(body) AS body_bytes,
2039
- SUM(logical_bytes) OVER (ORDER BY id ASC) - logical_bytes
2040
- AS preceding_bytes
2041
- FROM otlp_envelope_outbox
2042
- )
2043
- SELECT
2044
- COUNT(*) AS record_count,
2045
- COALESCE(SUM(logical_bytes), 0) AS logical_bytes,
2046
- COALESCE(SUM(body_bytes), 0) AS body_bytes
2047
- FROM ordered
2048
- WHERE preceding_bytes < ?
2041
+ const hasExpiredOtlpEnvelopes = database.prepare(`
2042
+ SELECT 1 FROM otlp_envelope_outbox WHERE captured_at_ms < ? LIMIT 1
2049
2043
  `);
2050
- const evictOldestPendingOtlpEnvelopes = database.prepare(`
2051
- DELETE FROM otlp_envelope_outbox
2052
- WHERE id IN (
2053
- SELECT id FROM otlp_envelope_outbox ORDER BY id ASC LIMIT ?
2054
- )
2044
+ const expiredOtlpEnvelopes = database.prepare(`
2045
+ SELECT id, logical_bytes, length(body) AS body_bytes
2046
+ FROM otlp_envelope_outbox
2047
+ WHERE captured_at_ms < ?
2048
+ ORDER BY captured_at_ms, id
2049
+ LIMIT 100
2050
+ `);
2051
+ const oldestOtlpEnvelopes = database.prepare(`
2052
+ SELECT id, logical_bytes, length(body) AS body_bytes
2053
+ FROM otlp_envelope_outbox
2054
+ ORDER BY captured_at_ms, id
2055
+ LIMIT 100
2056
+ `);
2057
+ const deleteExpiredOtlpEnvelope = database.prepare(`
2058
+ DELETE FROM otlp_envelope_outbox WHERE id = ?
2055
2059
  `);
2056
2060
  const getDestinationPendingOtlpEnvelopeAccounting = database.prepare(`
2057
2061
  SELECT logical_bytes, length(body) AS body_bytes
@@ -2652,42 +2656,6 @@ function openObserverDatabase(databaseFile, options = {}) {
2652
2656
  );
2653
2657
  return null;
2654
2658
  }
2655
- if (logicalBytes > maxOtlpSpoolBytes) {
2656
- recordOtlpDrop(
2657
- 1,
2658
- logicalBytes,
2659
- envelope.body.byteLength,
2660
- capturedAtMs,
2661
- OTLP_ENTRY_TOO_LARGE_DROP_REASON
2662
- );
2663
- return null;
2664
- }
2665
- const retainedBytes = requireSpoolInteger(
2666
- getOtlpSpoolState.get(),
2667
- "logical_bytes"
2668
- );
2669
- const bytesToFree = retainedBytes + logicalBytes - maxOtlpSpoolBytes;
2670
- if (bytesToFree > 0) {
2671
- const plan = otlpCapacityEvictionPlan.get(bytesToFree);
2672
- const evictedRecords = requireSpoolInteger(plan, "record_count");
2673
- const evictedLogicalBytes = requireSpoolInteger(
2674
- plan,
2675
- "logical_bytes"
2676
- );
2677
- const evictedBodyBytes = requireSpoolInteger(plan, "body_bytes");
2678
- if (evictedRecords === 0 || evictedLogicalBytes < bytesToFree || evictOldestPendingOtlpEnvelopes.run(evictedRecords).changes !== evictedRecords) {
2679
- throw new Error(
2680
- "FML observer OTLP spool accounting could not enforce its limit"
2681
- );
2682
- }
2683
- recordOtlpDrop(
2684
- evictedRecords,
2685
- evictedLogicalBytes,
2686
- evictedBodyBytes,
2687
- capturedAtMs,
2688
- OTLP_CAPACITY_DROP_REASON
2689
- );
2690
- }
2691
2659
  captureOtlpEnvelope.run(
2692
2660
  envelopeId,
2693
2661
  envelope.observerInstanceId,
@@ -2885,6 +2853,80 @@ function openObserverDatabase(databaseFile, options = {}) {
2885
2853
  return droppedRecords;
2886
2854
  });
2887
2855
  },
2856
+ pruneExpiredOtlpEnvelopes: (nowMs = Date.now()) => {
2857
+ requireTimestamp(nowMs, "OTLP retention timestamp");
2858
+ const cutoffMs = nowMs - maxOtlpSpoolAgeMs;
2859
+ if (!hasExpiredOtlpEnvelopes.get(cutoffMs)) return 0;
2860
+ return runImmediateTransaction(() => {
2861
+ const rows = expiredOtlpEnvelopes.all(cutoffMs);
2862
+ let logicalBytes = 0;
2863
+ let bodyBytes = 0;
2864
+ for (const row of rows) {
2865
+ logicalBytes += requireSpoolInteger(row, "logical_bytes");
2866
+ bodyBytes += requireSpoolInteger(row, "body_bytes");
2867
+ if (deleteExpiredOtlpEnvelope.run(row.id).changes !== 1) {
2868
+ throw new Error(
2869
+ "FML observer OTLP retention deletion was incomplete"
2870
+ );
2871
+ }
2872
+ }
2873
+ recordOtlpDrop(
2874
+ rows.length,
2875
+ logicalBytes,
2876
+ bodyBytes,
2877
+ nowMs,
2878
+ "spool_expired"
2879
+ );
2880
+ return rows.length;
2881
+ });
2882
+ },
2883
+ pruneOtlpCapacity: (nowMs = Date.now()) => {
2884
+ requireTimestamp(nowMs, "OTLP capacity cleanup timestamp");
2885
+ const retainedBytes = requireSpoolInteger(
2886
+ getOtlpSpoolState.get(),
2887
+ "logical_bytes"
2888
+ );
2889
+ if (retainedBytes <= otlpLowWaterBytes)
2890
+ otlpCapacityCleanupActive = false;
2891
+ if (retainedBytes > maxOtlpSpoolBytes) otlpCapacityCleanupActive = true;
2892
+ if (!otlpCapacityCleanupActive) return 0;
2893
+ return runImmediateTransaction(() => {
2894
+ const currentBytes = requireSpoolInteger(
2895
+ getOtlpSpoolState.get(),
2896
+ "logical_bytes"
2897
+ );
2898
+ if (currentBytes <= otlpLowWaterBytes) {
2899
+ otlpCapacityCleanupActive = false;
2900
+ return 0;
2901
+ }
2902
+ let records = 0;
2903
+ let logicalBytes = 0;
2904
+ let bodyBytes = 0;
2905
+ for (const row of oldestOtlpEnvelopes.all()) {
2906
+ logicalBytes += requireSpoolInteger(row, "logical_bytes");
2907
+ bodyBytes += requireSpoolInteger(row, "body_bytes");
2908
+ if (deleteExpiredOtlpEnvelope.run(row.id).changes !== 1) {
2909
+ throw new Error(
2910
+ "FML observer OTLP capacity deletion was incomplete"
2911
+ );
2912
+ }
2913
+ records += 1;
2914
+ if (currentBytes - logicalBytes <= otlpLowWaterBytes) break;
2915
+ }
2916
+ if (records === 0)
2917
+ throw new Error(
2918
+ "FML observer OTLP capacity accounting is inconsistent"
2919
+ );
2920
+ recordOtlpDrop(
2921
+ records,
2922
+ logicalBytes,
2923
+ bodyBytes,
2924
+ nowMs,
2925
+ "spool_capacity"
2926
+ );
2927
+ return records;
2928
+ });
2929
+ },
2888
2930
  getOtlpSpoolStatus: () => {
2889
2931
  const row = getOtlpSpoolState.get();
2890
2932
  const lastDroppedAtMs = row?.last_dropped_at_ms;
@@ -2898,6 +2940,7 @@ function openObserverDatabase(databaseFile, options = {}) {
2898
2940
  return {
2899
2941
  unit: "logical_bytes",
2900
2942
  maxBytes: maxOtlpSpoolBytes,
2943
+ maxAgeMs: maxOtlpSpoolAgeMs,
2901
2944
  logicalBytes: requireSpoolInteger(row, "logical_bytes"),
2902
2945
  bodyBytes: requireSpoolInteger(row, "body_bytes"),
2903
2946
  droppedRecords: requireSpoolInteger(row, "dropped_records"),
@@ -53,7 +53,7 @@ var PACKAGE_NAME = "@fml-inc/fml";
53
53
  var UPDATE_TIMEOUT_MS = 12e4;
54
54
  var SETUP_TIMEOUT_MS = 3e5;
55
55
  function getCurrentVersion() {
56
- return true ? "0.8.9+6863d154" : "unknown";
56
+ return true ? "0.8.10-dev.1+09fae5b6" : "unknown";
57
57
  }
58
58
  function parseVersion(version) {
59
59
  const match = SEMVER_PATTERN.exec(version);
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  execBinSync,
8
8
  handleUpdate,
9
9
  resolveBin
10
- } from "./chunk-TWJ62CZX.js";
10
+ } from "./chunk-5SI6S4HW.js";
11
11
  import {
12
12
  openObserverLog,
13
13
  readJsonFile,
@@ -29,7 +29,7 @@ import {
29
29
  readObserverAuthToken,
30
30
  readObserverStateStatus,
31
31
  removeObserverStateIfOwned
32
- } from "./chunk-H5LWJ5PH.js";
32
+ } from "./chunk-5PEFBQNX.js";
33
33
  import "./chunk-7V7G7NVF.js";
34
34
  import {
35
35
  claudeConfigDirectory,
@@ -132,7 +132,7 @@ var STOP_TIMEOUT_MS = 5e3;
132
132
  var FORCE_STOP_TIMEOUT_MS = 2e3;
133
133
  var POLL_INTERVAL_MS = 50;
134
134
  function expectedObserverVersion() {
135
- return true ? "0.8.9+6863d154" : "dev";
135
+ return true ? "0.8.10-dev.1+09fae5b6" : "dev";
136
136
  }
137
137
  function delay(ms) {
138
138
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -8162,6 +8162,7 @@ function otlpSpoolStatus(value) {
8162
8162
  const logicalBytes = nonNegativeInteger("logicalBytes");
8163
8163
  const bodyBytes = nonNegativeInteger("bodyBytes");
8164
8164
  const maxBytes = nonNegativeInteger("maxBytes");
8165
+ const maxAgeMs = nonNegativeInteger("maxAgeMs");
8165
8166
  const droppedRecords = nonNegativeInteger("droppedRecords");
8166
8167
  const droppedLogicalBytes = nonNegativeInteger("droppedLogicalBytes");
8167
8168
  const droppedBodyBytes = nonNegativeInteger("droppedBodyBytes");
@@ -8172,6 +8173,7 @@ function otlpSpoolStatus(value) {
8172
8173
  logicalBytes,
8173
8174
  bodyBytes,
8174
8175
  maxBytes,
8176
+ ...maxAgeMs !== void 0 && maxAgeMs > 0 ? { maxAgeMs } : {},
8175
8177
  droppedRecords,
8176
8178
  droppedLogicalBytes,
8177
8179
  droppedBodyBytes,
@@ -8363,9 +8365,14 @@ async function handleStatus() {
8363
8365
  console.log(
8364
8366
  ` Spool: ${formatBytes2(otlp.spool.logicalBytes)} / ${formatBytes2(otlp.spool.maxBytes)} (${usedPercent.toFixed(1)}%; ${formatBytes2(otlp.spool.bodyBytes)} encoded bodies)`
8365
8367
  );
8368
+ if (otlp.spool.maxAgeMs !== void 0) {
8369
+ console.log(
8370
+ ` Retention: ${otlp.spool.maxAgeMs / 36e5} hour(s) (background cleanup)`
8371
+ );
8372
+ }
8366
8373
  if (usedPercent >= 80) {
8367
8374
  console.log(
8368
- ` Attention: the oldest pending telemetry is dropped when the spool reaches ${formatBytes2(otlp.spool.maxBytes)}`
8375
+ ` Attention: background cleanup removes oldest telemetry toward 90% of the ${formatBytes2(otlp.spool.maxBytes)} byte target after overflow; checks run every 5 minutes`
8369
8376
  );
8370
8377
  }
8371
8378
  if (otlp.spool.droppedRecords > 0) {
@@ -9000,7 +9007,7 @@ async function handleConfigDetail(opts) {
9000
9007
  // src/commands/tools.ts
9001
9008
  import { readFileSync } from "fs";
9002
9009
  import path29 from "path";
9003
- var pluginVersion = true ? "0.8.9+6863d154" : void 0;
9010
+ var pluginVersion = true ? "0.8.10-dev.1+09fae5b6" : void 0;
9004
9011
  async function getApi() {
9005
9012
  const api = await getAuthenticatedClient();
9006
9013
  if (!api) {
@@ -9157,7 +9164,7 @@ if (process.argv.length <= 2 || process.argv.includes("--help") || process.argv.
9157
9164
  printBanner();
9158
9165
  }
9159
9166
  var program = new Command().name("fml").description("FML CLI and agent tools").version(
9160
- true ? "0.8.9+6863d154" : "dev"
9167
+ true ? "0.8.10-dev.1+09fae5b6" : "dev"
9161
9168
  );
9162
9169
  function formatCommandInventory(cmd, prefix = "") {
9163
9170
  const name = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
@@ -19,7 +19,7 @@ import {
19
19
  readObserverAuthToken,
20
20
  readObserverState,
21
21
  recordHookInboxLoss
22
- } from "./chunk-H5LWJ5PH.js";
22
+ } from "./chunk-5PEFBQNX.js";
23
23
  import {
24
24
  resolveRepositoryRootFromCwd,
25
25
  resolveRepositorySlugFromCwd
@@ -5,7 +5,7 @@ import {
5
5
  isWinShim,
6
6
  quoteWinArg,
7
7
  resolveBin
8
- } from "./chunk-TWJ62CZX.js";
8
+ } from "./chunk-5SI6S4HW.js";
9
9
  import {
10
10
  createHookSyncLoop,
11
11
  getActiveHookDestinationBinding
@@ -30,7 +30,7 @@ import {
30
30
  removeObserverStateIfOwned,
31
31
  retireHookInboxEntry,
32
32
  updateObserverState
33
- } from "./chunk-H5LWJ5PH.js";
33
+ } from "./chunk-5PEFBQNX.js";
34
34
  import "./chunk-7V7G7NVF.js";
35
35
  import {
36
36
  ObserverSyncCycleAbortedError,
@@ -225,7 +225,6 @@ var MAX_ERROR_TEXT_BYTES = 1e3;
225
225
  var MAX_REJECTION_REASON_BYTES = 3 * 1024;
226
226
  var MAX_OTLP_RESPONSE_BYTES = 64 * 1024;
227
227
  var MAX_OTLP_UPLOAD_ATTEMPTS = 10;
228
- var MAX_OTLP_RETRY_AGE_MS = 24 * 60 * 6e4;
229
228
  var OUTBOX_BATCH_SIZE = 1;
230
229
  var DRAIN_INTERVAL_MS = 25;
231
230
  function combineWarnings(...warnings) {
@@ -675,6 +674,7 @@ async function runOtlpSyncCycle(opts) {
675
674
  if (totalPending === 0) {
676
675
  return { kind: "drained", pending: 0, uploaded: 0 };
677
676
  }
677
+ const maxAgeMs = opts.database.getOtlpSpoolStatus().maxAgeMs;
678
678
  const destinationResolution = await resolveDestination();
679
679
  if (destinationResolution.kind !== "ready") {
680
680
  return {
@@ -736,7 +736,7 @@ async function runOtlpSyncCycle(opts) {
736
736
  };
737
737
  for (const envelope of envelopes) {
738
738
  if (opts.signal?.aborted) throw opts.signal.reason;
739
- if (cycleStartedAtMs - envelope.capturedAtMs >= MAX_OTLP_RETRY_AGE_MS) {
739
+ if (cycleStartedAtMs - envelope.capturedAtMs >= maxAgeMs) {
740
740
  dropped += opts.database.dropOtlpEnvelopes(
741
741
  destination,
742
742
  [
@@ -1046,6 +1046,17 @@ function parseOtlpSpoolMaxBytes(configured) {
1046
1046
  }
1047
1047
  return bytes;
1048
1048
  }
1049
+ function parseOtlpSpoolMaxAgeMs(configured) {
1050
+ if (configured === void 0) return void 0;
1051
+ const value = configured.trim();
1052
+ const milliseconds = Number(value) * 60 * 60 * 1e3;
1053
+ if (!POSITIVE_INTEGER_PATTERN.test(value) || !Number.isSafeInteger(milliseconds)) {
1054
+ throw new Error(
1055
+ "FML_OTLP_SPOOL_MAX_AGE_HOURS must be a positive safe integer number of hours"
1056
+ );
1057
+ }
1058
+ return milliseconds;
1059
+ }
1049
1060
 
1050
1061
  // src/observer/sync-loop.ts
1051
1062
  var OBSERVER_IDLE_INTERVAL_MS = 5 * 6e4;
@@ -1348,7 +1359,8 @@ var HOOK_INBOX_DRAIN_INTERVAL_MS = 15e3;
1348
1359
  var HOOK_INBOX_DRAIN_BUDGET_MS = 5e3;
1349
1360
  var HOOK_INBOX_DRAIN_ENTRIES = 100;
1350
1361
  var HOOK_INBOX_DRAIN_BULK_ENTRIES = 1e3;
1351
- var HOOK_QUARANTINE_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
1362
+ var SPOOL_CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
1363
+ var OTLP_CLEANUP_BATCH_DELAY_MS = 100;
1352
1364
  var OBSERVER_DIRECTORY_LEASE = {
1353
1365
  stateFile: FML_OBSERVER_STATE_FILE,
1354
1366
  lockFile: FML_OBSERVER_LOCK_FILE
@@ -1452,6 +1464,9 @@ var hookQuarantineMaxAgeMs = parseHookQuarantineMaxAgeMs(
1452
1464
  var otlpSpoolMaxBytes = parseOtlpSpoolMaxBytes(
1453
1465
  process.env.FML_OTLP_SPOOL_MAX_BYTES
1454
1466
  );
1467
+ var otlpSpoolMaxAgeMs = parseOtlpSpoolMaxAgeMs(
1468
+ process.env.FML_OTLP_SPOOL_MAX_AGE_HOURS
1469
+ );
1455
1470
  ensureDirs();
1456
1471
  var isolationWarning = observerOnlyOverrideWarning();
1457
1472
  if (isolationWarning) console.warn(isolationWarning);
@@ -1486,6 +1501,7 @@ var fatalShutdownScheduled = false;
1486
1501
  var heartbeatTimer = null;
1487
1502
  var hookInboxDrainTimer = null;
1488
1503
  var hookInboxDrainPromise = null;
1504
+ var otlpPruneTimer = null;
1489
1505
  var hookQuarantinePruneTimer = null;
1490
1506
  var stateUpdateRetryTimer = null;
1491
1507
  var syncLoop = null;
@@ -1510,9 +1526,10 @@ try {
1510
1526
  // FML_STATE_DIR can be caller-managed. Keep SQLite payload files private
1511
1527
  // without changing permissions on the shared parent directory.
1512
1528
  restrictDatabaseDirectory: false,
1529
+ maxOtlpSpoolAgeMs: otlpSpoolMaxAgeMs,
1530
+ maxOtlpSpoolBytes: otlpSpoolMaxBytes,
1513
1531
  ...hookSpoolMaxBytes === void 0 ? {} : { maxHookSpoolBytes: hookSpoolMaxBytes },
1514
- ...hookQuarantineMaxAgeMs === void 0 ? {} : { maxHookQuarantineAgeMs: hookQuarantineMaxAgeMs },
1515
- ...otlpSpoolMaxBytes === void 0 ? {} : { maxOtlpSpoolBytes: otlpSpoolMaxBytes }
1532
+ ...hookQuarantineMaxAgeMs === void 0 ? {} : { maxHookQuarantineAgeMs: hookQuarantineMaxAgeMs }
1516
1533
  });
1517
1534
  } catch (error) {
1518
1535
  removeInitialLeasesBestEffort(initial.instanceId);
@@ -1546,7 +1563,7 @@ var hookRejectionStatus = () => ({
1546
1563
  ...lastRejectedReason !== void 0 ? { lastRejectedReason } : {},
1547
1564
  ...lastRejectedTarget !== void 0 ? { lastRejectedTarget } : {}
1548
1565
  });
1549
- var packageVersion = true ? "0.8.9+6863d154" : "dev";
1566
+ var packageVersion = true ? "0.8.10-dev.1+09fae5b6" : "dev";
1550
1567
  var health = () => ({
1551
1568
  status: "ok",
1552
1569
  service: OBSERVER_SERVICE_NAME,
@@ -1611,6 +1628,7 @@ var shutdown = async () => {
1611
1628
  if (heartbeatTimer) clearInterval(heartbeatTimer);
1612
1629
  if (hookInboxDrainTimer) clearInterval(hookInboxDrainTimer);
1613
1630
  if (hookQuarantinePruneTimer) clearInterval(hookQuarantinePruneTimer);
1631
+ if (otlpPruneTimer) clearTimeout(otlpPruneTimer);
1614
1632
  if (stateUpdateRetryTimer) clearTimeout(stateUpdateRetryTimer);
1615
1633
  sessionEndSyncWakeScheduler.stop();
1616
1634
  autoUpdater?.stop();
@@ -1866,8 +1884,30 @@ try {
1866
1884
  } catch (error) {
1867
1885
  scheduleFatalShutdown(error);
1868
1886
  }
1869
- }, HOOK_QUARANTINE_PRUNE_INTERVAL_MS);
1887
+ }, SPOOL_CLEANUP_INTERVAL_MS);
1870
1888
  hookQuarantinePruneTimer.unref();
1889
+ let nextOtlpAgeCheckAtMs = 0;
1890
+ const pruneOtlp = () => {
1891
+ if (shuttingDown) return;
1892
+ try {
1893
+ const nowMs = Date.now();
1894
+ let removed = 0;
1895
+ if (nowMs >= nextOtlpAgeCheckAtMs) {
1896
+ removed = database.pruneExpiredOtlpEnvelopes(nowMs);
1897
+ if (removed < 100)
1898
+ nextOtlpAgeCheckAtMs = nowMs + SPOOL_CLEANUP_INTERVAL_MS;
1899
+ }
1900
+ if (removed === 0) removed = database.pruneOtlpCapacity(nowMs);
1901
+ otlpPruneTimer = setTimeout(
1902
+ pruneOtlp,
1903
+ removed > 0 ? OTLP_CLEANUP_BATCH_DELAY_MS : SPOOL_CLEANUP_INTERVAL_MS
1904
+ );
1905
+ otlpPruneTimer.unref();
1906
+ } catch (error) {
1907
+ scheduleFatalShutdown(error);
1908
+ }
1909
+ };
1910
+ pruneOtlp();
1871
1911
  hookSyncLoop = createHookSyncLoop({
1872
1912
  database,
1873
1913
  version: packageVersion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fml-inc/fml",
3
- "version": "0.8.9",
3
+ "version": "0.8.10-dev.1",
4
4
  "description": "FML CLI and agent tools",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "open": "^10.1.0",
34
34
  "ora": "^8.2.0",
35
35
  "picocolors": "^1.1.1",
36
- "smol-toml": "1.7.0",
36
+ "smol-toml": "1.7.1",
37
37
  "zod": "4.1.12"
38
38
  },
39
39
  "devDependencies": {