@noego/telemetry 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +6 -0
  2. package/dist/browser/index.d.cts +6 -5
  3. package/dist/browser/index.d.ts +6 -5
  4. package/dist/{browser_telemetry_client-BSAMZ8Qb.d.cts → browser_telemetry_client-ClZl3awO.d.cts} +2 -2
  5. package/dist/{browser_telemetry_client-DPBU6NGw.d.ts → browser_telemetry_client-t4dPp8Ku.d.ts} +2 -2
  6. package/dist/cli/telemetry_bin.cjs +393 -35
  7. package/dist/cli/telemetry_bin.cjs.map +1 -1
  8. package/dist/cli/telemetry_bin.js +378 -20
  9. package/dist/cli/telemetry_bin.js.map +1 -1
  10. package/dist/cli/telemetry_cli.cjs +393 -35
  11. package/dist/cli/telemetry_cli.cjs.map +1 -1
  12. package/dist/cli/telemetry_cli.d.cts +35 -5
  13. package/dist/cli/telemetry_cli.d.ts +35 -5
  14. package/dist/cli/telemetry_cli.js +378 -20
  15. package/dist/cli/telemetry_cli.js.map +1 -1
  16. package/dist/{client_defaults-ShVYQYmn.d.ts → client_defaults-CZ8PysZX.d.ts} +2 -2
  17. package/dist/{client_defaults-Dk0MMzvx.d.cts → client_defaults-lDbWYWWK.d.cts} +2 -2
  18. package/dist/core/index.cjs +5 -1
  19. package/dist/core/index.cjs.map +1 -1
  20. package/dist/core/index.d.cts +15 -12
  21. package/dist/core/index.d.ts +15 -12
  22. package/dist/core/index.js +5 -1
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/electron/index.d.cts +5 -4
  25. package/dist/electron/index.d.ts +5 -4
  26. package/dist/{envelope.types-rOLpWXRC.d.cts → envelope.types-DIvZD3xH.d.ts} +2 -6
  27. package/dist/{envelope.types-rOLpWXRC.d.ts → envelope.types-DyrlRG49.d.cts} +2 -6
  28. package/dist/{in_memory_batch_transport-DKW9EZnU.d.cts → in_memory_batch_transport-3T6gElXP.d.cts} +1 -1
  29. package/dist/{in_memory_batch_transport-oOwNxXnA.d.ts → in_memory_batch_transport-DpQU6JqM.d.ts} +1 -1
  30. package/dist/index.cjs +5 -1
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +14 -12
  33. package/dist/index.d.ts +14 -12
  34. package/dist/index.js +5 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/json.types-BqS77fcN.d.cts +7 -0
  37. package/dist/json.types-BqS77fcN.d.ts +7 -0
  38. package/dist/{maintenance_coordinator.types-C4ULi4A4.d.cts → maintenance_coordinator.types-Crhx9i9k.d.cts} +3 -3
  39. package/dist/{maintenance_coordinator.types-ikUrRXmO.d.ts → maintenance_coordinator.types-DprmgpVn.d.ts} +3 -3
  40. package/dist/metric.types-D8aOAnUm.d.ts +67 -0
  41. package/dist/metric.types-Dg4eZq6u.d.cts +67 -0
  42. package/dist/{metric_definition_contract-BYtnODhu.d.ts → metric_definition_contract-C6MERhJs.d.ts} +1 -1
  43. package/dist/{metric_definition_contract-DfhhmTZ1.d.cts → metric_definition_contract-DoAk7NOw.d.cts} +1 -1
  44. package/dist/node/index.cjs +5 -1
  45. package/dist/node/index.cjs.map +1 -1
  46. package/dist/node/index.d.cts +11 -9
  47. package/dist/node/index.d.ts +11 -9
  48. package/dist/node/index.js +5 -1
  49. package/dist/node/index.js.map +1 -1
  50. package/dist/node/producer/index.d.cts +5 -4
  51. package/dist/node/producer/index.d.ts +5 -4
  52. package/dist/projector/index.cjs +1370 -0
  53. package/dist/projector/index.cjs.map +1 -0
  54. package/dist/projector/index.d.cts +246 -0
  55. package/dist/projector/index.d.ts +246 -0
  56. package/dist/projector/index.js +1328 -0
  57. package/dist/projector/index.js.map +1 -0
  58. package/dist/{query.types-DU5v7vM0.d.ts → query.types-CRqAsGTX.d.cts} +4 -66
  59. package/dist/{query.types-DuYWbFFe.d.cts → query.types-DnOMYXpC.d.ts} +4 -66
  60. package/dist/sqlite/index.cjs +5 -1
  61. package/dist/sqlite/index.cjs.map +1 -1
  62. package/dist/sqlite/index.d.cts +7 -5
  63. package/dist/sqlite/index.d.ts +7 -5
  64. package/dist/sqlite/index.js +5 -1
  65. package/dist/sqlite/index.js.map +1 -1
  66. package/dist/{storage.types-k7oElEB3.d.ts → storage.types-BKWoIOHp.d.ts} +3 -2
  67. package/dist/{storage.types-Bji_pWqr.d.cts → storage.types-DHbAm3j_.d.cts} +3 -2
  68. package/dist/{telemetry_client-BO5Pjp41.d.cts → telemetry_client-Bg5tbj5E.d.cts} +3 -2
  69. package/dist/{telemetry_client-D8uZ-mTL.d.ts → telemetry_client-DWKgufmc.d.ts} +3 -2
  70. package/dist/{telemetry_configuration_parser-BZ8Nvnle.d.ts → telemetry_configuration_parser-6BR3_dD_.d.ts} +2 -2
  71. package/dist/{telemetry_configuration_parser-CgSN7R6A.d.cts → telemetry_configuration_parser-ybeweUPE.d.cts} +2 -2
  72. package/dist/testing/index.d.cts +7 -5
  73. package/dist/testing/index.d.ts +7 -5
  74. package/dist/trace-adapter/index.d.cts +4 -3
  75. package/dist/trace-adapter/index.d.ts +4 -3
  76. package/dist/{transport.types-BAl_TRtU.d.cts → transport.types-C2dP-eed.d.cts} +1 -1
  77. package/dist/{transport.types-DDnZoKWi.d.ts → transport.types-SA8KpLiK.d.ts} +1 -1
  78. package/native/noego_telemetry_native.node +0 -0
  79. package/package.json +11 -4
@@ -35,7 +35,7 @@ __export(telemetry_cli_exports, {
35
35
  runTelemetryCli: () => runTelemetryCli
36
36
  });
37
37
  module.exports = __toCommonJS(telemetry_cli_exports);
38
- var import_node_fs3 = require("fs");
38
+ var import_node_fs5 = require("fs");
39
39
 
40
40
  // src/platform/node/telemetry_configuration_parser.ts
41
41
  var import_node_path = require("path");
@@ -100,11 +100,15 @@ var MetricDefinitionContract = class {
100
100
  if (!eventSelector || !bracket) {
101
101
  return { ok: false, error: `Invalid metric selector ${selector}` };
102
102
  }
103
+ const groupBy = bracket === "*" ? [] : bracket.split(",").map((field) => field.trim());
104
+ if (groupBy.some((field) => field.length === 0)) {
105
+ return { ok: false, error: `Invalid metric selector ${selector}` };
106
+ }
103
107
  return {
104
108
  ok: true,
105
109
  value: {
106
110
  selector: eventSelector,
107
- groupBy: bracket === "*" ? [] : [bracket],
111
+ groupBy,
108
112
  wildcardUngrouped: bracket === "*"
109
113
  }
110
114
  };
@@ -1820,24 +1824,200 @@ var MetricGraphRenderer = class {
1820
1824
  }
1821
1825
  };
1822
1826
 
1827
+ // src/cli/history_metric_reader.ts
1828
+ var import_node_fs2 = require("fs");
1829
+ var import_node_path2 = require("path");
1830
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
1831
+ var HISTORY_DATABASE_NAME = "history.db";
1832
+ var AGGREGATES = /* @__PURE__ */ new Set(["count", "sum", "min", "max", "last", "avg"]);
1833
+ function historyDatabasePath(target) {
1834
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(target.path), HISTORY_DATABASE_NAME);
1835
+ }
1836
+ var HistoryMetricReader = class _HistoryMetricReader {
1837
+ constructor(db, definitions) {
1838
+ this.db = db;
1839
+ this.definitions = definitions;
1840
+ }
1841
+ db;
1842
+ definitions;
1843
+ static open(databasePath, definitions) {
1844
+ const db = new import_better_sqlite3.default(databasePath, { readonly: true, fileMustExist: true });
1845
+ db.pragma("busy_timeout = 1000");
1846
+ db.pragma("query_only = ON");
1847
+ return new _HistoryMetricReader(db, definitions);
1848
+ }
1849
+ static exists(databasePath) {
1850
+ return (0, import_node_fs2.existsSync)(databasePath);
1851
+ }
1852
+ metrics(query, targetId) {
1853
+ if (query.toMs <= query.fromMs) {
1854
+ return { ok: false, error: { kind: "invalid-query", message: "--to must be after --from" } };
1855
+ }
1856
+ const invalidMeasurement = query.measurements.find((measurement) => !isMeasurement(measurement));
1857
+ if (invalidMeasurement !== void 0) {
1858
+ return {
1859
+ ok: false,
1860
+ error: {
1861
+ kind: "invalid-query",
1862
+ message: `Unknown measurement ${invalidMeasurement}; use count or <field>.<sum|min|max|last|count|avg>`
1863
+ }
1864
+ };
1865
+ }
1866
+ const versions = this.registeredVersions(query.metric);
1867
+ if (versions.length === 0) {
1868
+ return {
1869
+ ok: false,
1870
+ error: { kind: "invalid-query", message: `Metric ${query.metric} has no projection in history.db` }
1871
+ };
1872
+ }
1873
+ const resolutionMs = this.resolveResolution(query, versions);
1874
+ if (resolutionMs === null) {
1875
+ return {
1876
+ ok: false,
1877
+ error: { kind: "invalid-query", message: `Metric ${query.metric} is not projected at the requested resolution` }
1878
+ };
1879
+ }
1880
+ for (let index = versions.length - 1; index >= 0; index -= 1) {
1881
+ const version = versions[index];
1882
+ const buckets = this.readBuckets(query, version, resolutionMs);
1883
+ if (buckets.length === 0) continue;
1884
+ const olderVersions = versions.slice(0, index);
1885
+ return {
1886
+ ok: true,
1887
+ value: Object.freeze({
1888
+ targetId,
1889
+ metric: query.metric,
1890
+ segments: [Object.freeze({
1891
+ fromMs: query.fromMs,
1892
+ toMs: query.toMs,
1893
+ resolutionMs,
1894
+ isAggregated: true,
1895
+ definitionVersion: version,
1896
+ buckets
1897
+ })],
1898
+ gaps: [],
1899
+ nextCursor: null,
1900
+ partialFailures: olderVersions.map((older) => Object.freeze({
1901
+ targetId,
1902
+ reason: `version ${older} buckets omitted; only version ${version} returned`
1903
+ }))
1904
+ })
1905
+ };
1906
+ }
1907
+ return {
1908
+ ok: true,
1909
+ value: Object.freeze({
1910
+ targetId,
1911
+ metric: query.metric,
1912
+ segments: [],
1913
+ gaps: [Object.freeze({ fromMs: query.fromMs, toMs: query.toMs, reason: "not-materialized" })],
1914
+ nextCursor: null,
1915
+ partialFailures: []
1916
+ })
1917
+ };
1918
+ }
1919
+ close() {
1920
+ if (this.db.open) this.db.close();
1921
+ }
1922
+ registeredVersions(metric) {
1923
+ const rows = this.db.prepare(
1924
+ "SELECT projection_version FROM trace_projection_definitions WHERE projection_name = ? ORDER BY projection_version"
1925
+ ).all(metric);
1926
+ return rows.map((row) => row.projection_version);
1927
+ }
1928
+ resolveResolution(query, versions) {
1929
+ const available = this.definitions.filter((definition) => definition.name === query.metric && versions.includes(definition.version)).flatMap((definition) => [...definition.resolutionsMs]);
1930
+ const resolutions = [...new Set(available.length > 0 ? available : this.storedResolutions(query.metric))].sort((a, b) => a - b);
1931
+ if (resolutions.length === 0) return null;
1932
+ if (query.resolution !== "auto") {
1933
+ return resolutions.includes(query.resolution) ? query.resolution : null;
1934
+ }
1935
+ const span = query.toMs - query.fromMs;
1936
+ return resolutions.find((resolution) => Math.ceil(span / resolution) <= query.limit) ?? resolutions[resolutions.length - 1];
1937
+ }
1938
+ storedResolutions(metric) {
1939
+ const rows = this.db.prepare(
1940
+ "SELECT DISTINCT resolution_ms FROM trace_metric_buckets WHERE projection_name = ?"
1941
+ ).all(metric);
1942
+ return rows.map((row) => row.resolution_ms);
1943
+ }
1944
+ readBuckets(query, version, resolutionMs) {
1945
+ const rows = this.db.prepare(`
1946
+ SELECT projection_version, bucket_start_ms, dimensions_json, count, values_json
1947
+ FROM trace_metric_buckets
1948
+ WHERE projection_name = ? AND projection_version = ? AND resolution_ms = ?
1949
+ AND bucket_start_ms >= ? AND bucket_start_ms < ?
1950
+ ORDER BY bucket_start_ms ${query.order === "desc" ? "DESC" : "ASC"}, dimension_key
1951
+ `).all(query.metric, version, resolutionMs, query.fromMs, query.toMs);
1952
+ const buckets = [];
1953
+ for (const row of rows) {
1954
+ const dimensions = JSON.parse(row.dimensions_json);
1955
+ if (!matchesWhere(dimensions, query.where)) continue;
1956
+ const values = JSON.parse(row.values_json);
1957
+ buckets.push(Object.freeze({
1958
+ startMs: row.bucket_start_ms,
1959
+ endMs: row.bucket_start_ms + resolutionMs,
1960
+ dimensions,
1961
+ values: selectMeasurements(query.measurements, row.count, values)
1962
+ }));
1963
+ if (buckets.length >= query.limit) break;
1964
+ }
1965
+ return buckets;
1966
+ }
1967
+ };
1968
+ function isMeasurement(measurement) {
1969
+ if (measurement === "count") return true;
1970
+ const dot = measurement.lastIndexOf(".");
1971
+ return dot > 0 && AGGREGATES.has(measurement.slice(dot + 1));
1972
+ }
1973
+ function selectMeasurements(measurements, bucketCount, values) {
1974
+ const selected = {};
1975
+ for (const measurement of measurements) {
1976
+ if (measurement === "count") {
1977
+ selected.count = bucketCount;
1978
+ continue;
1979
+ }
1980
+ const dot = measurement.lastIndexOf(".");
1981
+ const field = measurement.slice(0, dot);
1982
+ const aggregate = measurement.slice(dot + 1);
1983
+ const state = values[field];
1984
+ if (state === void 0) continue;
1985
+ const value = aggregate === "avg" ? state.count > 0 ? state.sum / state.count : 0 : state[aggregate];
1986
+ if (!Number.isFinite(value)) continue;
1987
+ const existing = selected[field];
1988
+ selected[field] = { ...typeof existing === "object" ? existing : {}, [aggregate]: value };
1989
+ }
1990
+ return Object.freeze(selected);
1991
+ }
1992
+ function matchesWhere(dimensions, where) {
1993
+ for (const [key, filter] of Object.entries(where)) {
1994
+ const actual = dimensions[key];
1995
+ if (actual === void 0) return false;
1996
+ const accepted = Array.isArray(filter) ? filter : [filter];
1997
+ if (!accepted.some((candidate) => String(candidate) === actual)) return false;
1998
+ }
1999
+ return true;
2000
+ }
2001
+
1823
2002
  // src/cli/offline_sqlite_session.ts
1824
2003
  var import_node_crypto2 = require("crypto");
1825
- var import_node_path3 = require("path");
2004
+ var import_node_fs4 = require("fs");
2005
+ var import_node_path4 = require("path");
1826
2006
  var import_node_url = require("url");
1827
2007
  var import_proper = require("@noego/proper");
1828
- var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
2008
+ var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
1829
2009
  var import_adapters = require("sqlstack/adapters");
1830
2010
 
1831
2011
  // src/storage/sqlite_ownership_lease.ts
1832
- var import_node_fs2 = require("fs");
1833
- var import_node_path2 = require("path");
2012
+ var import_node_fs3 = require("fs");
2013
+ var import_node_path3 = require("path");
1834
2014
  var SqliteOwnershipLease = class {
1835
2015
  constructor(options) {
1836
2016
  this.options = options;
1837
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(options.databasePath), { recursive: true });
1838
- const canonicalDirectory = (0, import_node_fs2.realpathSync)((0, import_node_path2.dirname)((0, import_node_path2.resolve)(options.databasePath)));
1839
- const databaseName = (0, import_node_path2.resolve)(options.databasePath).slice((0, import_node_path2.dirname)((0, import_node_path2.resolve)(options.databasePath)).length + 1);
1840
- this.canonicalDatabasePath = (0, import_node_path2.resolve)(canonicalDirectory, databaseName);
2017
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(options.databasePath), { recursive: true });
2018
+ const canonicalDirectory = (0, import_node_fs3.realpathSync)((0, import_node_path3.dirname)((0, import_node_path3.resolve)(options.databasePath)));
2019
+ const databaseName = (0, import_node_path3.resolve)(options.databasePath).slice((0, import_node_path3.dirname)((0, import_node_path3.resolve)(options.databasePath)).length + 1);
2020
+ this.canonicalDatabasePath = (0, import_node_path3.resolve)(canonicalDirectory, databaseName);
1841
2021
  this.leasePath = `${this.canonicalDatabasePath}.telemetry-owner.json`;
1842
2022
  }
1843
2023
  options;
@@ -1880,14 +2060,14 @@ var SqliteOwnershipLease = class {
1880
2060
  });
1881
2061
  }
1882
2062
  release() {
1883
- if (!this.held || !(0, import_node_fs2.existsSync)(this.leasePath)) {
2063
+ if (!this.held || !(0, import_node_fs3.existsSync)(this.leasePath)) {
1884
2064
  this.held = false;
1885
2065
  return;
1886
2066
  }
1887
2067
  const current = this.readRecord();
1888
2068
  if (current.ok && current.value.processToken === this.options.processToken) {
1889
2069
  try {
1890
- (0, import_node_fs2.rmSync)(this.leasePath, { force: true });
2070
+ (0, import_node_fs3.rmSync)(this.leasePath, { force: true });
1891
2071
  } catch {
1892
2072
  }
1893
2073
  }
@@ -1916,14 +2096,14 @@ var SqliteOwnershipLease = class {
1916
2096
  writeExclusive(record) {
1917
2097
  let descriptor = null;
1918
2098
  try {
1919
- descriptor = (0, import_node_fs2.openSync)(this.leasePath, "wx", 384);
1920
- (0, import_node_fs2.writeFileSync)(descriptor, JSON.stringify(record), { encoding: "utf8" });
1921
- (0, import_node_fs2.closeSync)(descriptor);
2099
+ descriptor = (0, import_node_fs3.openSync)(this.leasePath, "wx", 384);
2100
+ (0, import_node_fs3.writeFileSync)(descriptor, JSON.stringify(record), { encoding: "utf8" });
2101
+ (0, import_node_fs3.closeSync)(descriptor);
1922
2102
  return { ok: true, value: void 0 };
1923
2103
  } catch (cause) {
1924
2104
  if (descriptor !== null) {
1925
2105
  try {
1926
- (0, import_node_fs2.closeSync)(descriptor);
2106
+ (0, import_node_fs3.closeSync)(descriptor);
1927
2107
  } catch {
1928
2108
  }
1929
2109
  }
@@ -1933,12 +2113,12 @@ var SqliteOwnershipLease = class {
1933
2113
  writeReplace(record) {
1934
2114
  const temporaryPath = `${this.leasePath}.${this.options.processToken}.tmp`;
1935
2115
  try {
1936
- (0, import_node_fs2.writeFileSync)(temporaryPath, JSON.stringify(record), { encoding: "utf8", mode: 384 });
1937
- (0, import_node_fs2.renameSync)(temporaryPath, this.leasePath);
2116
+ (0, import_node_fs3.writeFileSync)(temporaryPath, JSON.stringify(record), { encoding: "utf8", mode: 384 });
2117
+ (0, import_node_fs3.renameSync)(temporaryPath, this.leasePath);
1938
2118
  return { ok: true, value: void 0 };
1939
2119
  } catch (cause) {
1940
2120
  try {
1941
- (0, import_node_fs2.rmSync)(temporaryPath, { force: true });
2121
+ (0, import_node_fs3.rmSync)(temporaryPath, { force: true });
1942
2122
  } catch {
1943
2123
  }
1944
2124
  return { ok: false, error: this.errorMessage(cause) };
@@ -1946,7 +2126,7 @@ var SqliteOwnershipLease = class {
1946
2126
  }
1947
2127
  readRecord() {
1948
2128
  try {
1949
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(this.leasePath, "utf8"));
2129
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(this.leasePath, "utf8"));
1950
2130
  if (!this.isLeaseRecord(parsed)) {
1951
2131
  return { ok: false, error: `Invalid telemetry ownership lease at ${this.leasePath}` };
1952
2132
  }
@@ -1970,8 +2150,8 @@ var SqliteOwnershipLease = class {
1970
2150
  }
1971
2151
  try {
1972
2152
  const stalePath = `${this.leasePath}.stale.${current.value.processToken}`;
1973
- (0, import_node_fs2.renameSync)(this.leasePath, stalePath);
1974
- (0, import_node_fs2.rmSync)(stalePath, { force: true });
2153
+ (0, import_node_fs3.renameSync)(this.leasePath, stalePath);
2154
+ (0, import_node_fs3.rmSync)(stalePath, { force: true });
1975
2155
  return { ok: true, value: void 0 };
1976
2156
  } catch (cause) {
1977
2157
  return { ok: false, error: this.errorMessage(cause) };
@@ -4366,7 +4546,7 @@ var ReadOnlySqlGuard = class {
4366
4546
 
4367
4547
  // src/cli/offline_sqlite_session.ts
4368
4548
  var import_meta = {};
4369
- var MIGRATIONS_DIRECTORY = typeof __dirname === "string" ? (0, import_node_path3.resolve)(__dirname, "../../migrations") : (0, import_node_url.fileURLToPath)(new URL("../../migrations/", import_meta.url));
4549
+ var MIGRATIONS_DIRECTORY = typeof __dirname === "string" ? (0, import_node_path4.resolve)(__dirname, "../../migrations") : (0, import_node_url.fileURLToPath)(new URL("../../migrations/", import_meta.url));
4370
4550
  var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4371
4551
  constructor(target, mode, definitions, now, sqlGuard) {
4372
4552
  this.target = target;
@@ -4508,6 +4688,87 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4508
4688
  this.verifySchema();
4509
4689
  return this.migrationStatus();
4510
4690
  }
4691
+ async compact() {
4692
+ this.requireMaintenanceMode();
4693
+ const connection = this.requireConnection();
4694
+ const maintenanceTable = this.requireTableMap().physical("maintenanceRuns");
4695
+ const runId = (0, import_node_crypto2.randomUUID)();
4696
+ const startedAtMs = this.now();
4697
+ connection.prepare(
4698
+ `INSERT INTO "${maintenanceTable}" (
4699
+ target_id, run_id, state, stage, started_at_ms,
4700
+ completed_at_ms, affected_rows, affected_pages, detail
4701
+ ) VALUES (?, ?, 'started', 'compaction', ?, NULL, 0, 0, ?)`
4702
+ ).run(this.target.targetId, runId, startedAtMs, "offline-cli; outcome=started");
4703
+ try {
4704
+ const before = this.compactionSnapshot();
4705
+ connection.pragma("wal_checkpoint(TRUNCATE)");
4706
+ const checkpointed = this.compactionSnapshot();
4707
+ this.assertCompactionSpace(checkpointed);
4708
+ connection.pragma("auto_vacuum = INCREMENTAL");
4709
+ connection.exec("VACUUM");
4710
+ connection.pragma("wal_checkpoint(TRUNCATE)");
4711
+ const after = this.compactionSnapshot();
4712
+ if (after.autoVacuum !== 2) {
4713
+ throw new Error("Telemetry compaction did not enable auto_vacuum=INCREMENTAL");
4714
+ }
4715
+ const completedAtMs = this.now();
4716
+ const result = Object.freeze({
4717
+ targetId: this.target.targetId,
4718
+ autoVacuum: "incremental",
4719
+ pageSize: checkpointed.pageSize,
4720
+ pagesBefore: checkpointed.pageCount,
4721
+ pagesAfter: after.pageCount,
4722
+ freePagesBefore: checkpointed.freePages,
4723
+ freePagesAfter: after.freePages,
4724
+ reclaimedPages: Math.max(0, checkpointed.pageCount - after.pageCount),
4725
+ databaseBytesBefore: before.databaseBytes,
4726
+ databaseBytesAfter: after.databaseBytes,
4727
+ walBytesBefore: before.walBytes,
4728
+ walBytesAfter: after.walBytes,
4729
+ reclaimedBytes: Math.max(
4730
+ 0,
4731
+ before.databaseBytes + before.walBytes - after.databaseBytes - after.walBytes
4732
+ ),
4733
+ elapsedMs: completedAtMs - startedAtMs
4734
+ });
4735
+ connection.prepare(
4736
+ `UPDATE "${maintenanceTable}"
4737
+ SET state = 'completed', completed_at_ms = ?, affected_pages = ?, detail = ?
4738
+ WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
4739
+ ).run(
4740
+ completedAtMs,
4741
+ result.reclaimedPages,
4742
+ JSON.stringify({
4743
+ mode: "offline-cli",
4744
+ outcome: "completed",
4745
+ reclaimedBytes: result.reclaimedBytes,
4746
+ walBytesBefore: result.walBytesBefore,
4747
+ walBytesAfter: result.walBytesAfter,
4748
+ autoVacuum: result.autoVacuum
4749
+ }),
4750
+ this.target.targetId,
4751
+ runId
4752
+ );
4753
+ return result;
4754
+ } catch (cause) {
4755
+ const message = cause instanceof Error ? cause.message : String(cause);
4756
+ try {
4757
+ connection.prepare(
4758
+ `UPDATE "${maintenanceTable}"
4759
+ SET state = 'failed', completed_at_ms = ?, detail = ?
4760
+ WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
4761
+ ).run(
4762
+ this.now(),
4763
+ JSON.stringify({ mode: "offline-cli", outcome: "failed", error: message }),
4764
+ this.target.targetId,
4765
+ runId
4766
+ );
4767
+ } catch {
4768
+ }
4769
+ throw cause;
4770
+ }
4771
+ }
4511
4772
  async close() {
4512
4773
  try {
4513
4774
  if (this.connection?.open) {
@@ -4541,7 +4802,7 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4541
4802
  throw new Error(acquired.error);
4542
4803
  }
4543
4804
  }
4544
- const connection = new import_better_sqlite3.default(this.target.path, {
4805
+ const connection = new import_better_sqlite32.default(this.target.path, {
4545
4806
  readonly: this.mode === "query",
4546
4807
  fileMustExist: true
4547
4808
  });
@@ -4568,6 +4829,32 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4568
4829
  }
4569
4830
  }
4570
4831
  }
4832
+ compactionSnapshot() {
4833
+ const connection = this.requireConnection();
4834
+ return Object.freeze({
4835
+ pageSize: Number(connection.pragma("page_size", { simple: true })),
4836
+ pageCount: Number(connection.pragma("page_count", { simple: true })),
4837
+ freePages: Number(connection.pragma("freelist_count", { simple: true })),
4838
+ autoVacuum: Number(connection.pragma("auto_vacuum", { simple: true })),
4839
+ databaseBytes: this.fileBytes(this.target.path),
4840
+ walBytes: this.fileBytes(`${this.target.path}-wal`)
4841
+ });
4842
+ }
4843
+ assertCompactionSpace(snapshot) {
4844
+ const liveBytes = Math.max(0, snapshot.pageCount - snapshot.freePages) * snapshot.pageSize;
4845
+ const reserveBytes = Math.max(64 * 1024 * 1024, Math.ceil(liveBytes * 0.05));
4846
+ const filesystem = (0, import_node_fs4.statfsSync)((0, import_node_path4.dirname)(this.target.path));
4847
+ const availableBytes = Number(filesystem.bavail) * Number(filesystem.bsize);
4848
+ const requiredBytes = liveBytes + reserveBytes;
4849
+ if (availableBytes < requiredBytes) {
4850
+ throw new Error(
4851
+ `Telemetry compaction requires at least ${requiredBytes} free bytes; only ${availableBytes} are available`
4852
+ );
4853
+ }
4854
+ }
4855
+ fileBytes(path) {
4856
+ return (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.statSync)(path).size : 0;
4857
+ }
4571
4858
  verifyManifest() {
4572
4859
  const rows = this.requireConnection().prepare(
4573
4860
  `SELECT target_id, format_version, mapping_json, mapping_checksum,
@@ -4752,7 +5039,8 @@ var ALLOWED_OPTIONS = Object.freeze({
4752
5039
  "metric",
4753
5040
  "where",
4754
5041
  "measurement",
4755
- "resolution"
5042
+ "resolution",
5043
+ "source"
4756
5044
  ]),
4757
5045
  "metrics-graph": /* @__PURE__ */ new Set([
4758
5046
  ...COMMON_QUERY_OPTIONS,
@@ -4760,6 +5048,7 @@ var ALLOWED_OPTIONS = Object.freeze({
4760
5048
  "where",
4761
5049
  "measurement",
4762
5050
  "resolution",
5051
+ "source",
4763
5052
  "type",
4764
5053
  "output",
4765
5054
  "width",
@@ -4777,6 +5066,7 @@ var ALLOWED_OPTIONS = Object.freeze({
4777
5066
  "limit",
4778
5067
  "max-duration"
4779
5068
  ]),
5069
+ "sqlite-compact": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
4780
5070
  "sqlite-migration-status": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
4781
5071
  "sqlite-migration-up": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"])
4782
5072
  });
@@ -4786,6 +5076,7 @@ var COMMANDS = Object.freeze([
4786
5076
  { tokens: ["metrics", "query"], kind: "metrics-query" },
4787
5077
  { tokens: ["metrics", "graph"], kind: "metrics-graph" },
4788
5078
  { tokens: ["sqlite", "query"], kind: "sqlite-query" },
5079
+ { tokens: ["sqlite", "compact"], kind: "sqlite-compact" },
4789
5080
  { tokens: ["sqlite", "migration", "status"], kind: "sqlite-migration-status" },
4790
5081
  { tokens: ["sqlite", "migration", "up"], kind: "sqlite-migration-up" }
4791
5082
  ]);
@@ -5064,6 +5355,22 @@ var TelemetryCliFormatter = class {
5064
5355
  return `${lines.join("\n")}
5065
5356
  `;
5066
5357
  }
5358
+ compaction(result, format) {
5359
+ if (format === "json") {
5360
+ return this.json(result);
5361
+ }
5362
+ return [
5363
+ `Target: ${result.targetId}`,
5364
+ `Auto-vacuum: ${result.autoVacuum}`,
5365
+ `Pages: ${result.pagesBefore} -> ${result.pagesAfter} (${result.reclaimedPages} reclaimed)`,
5366
+ `Freelist: ${result.freePagesBefore} -> ${result.freePagesAfter}`,
5367
+ `Database bytes: ${result.databaseBytesBefore} -> ${result.databaseBytesAfter}`,
5368
+ `WAL bytes: ${result.walBytesBefore} -> ${result.walBytesAfter}`,
5369
+ `Filesystem bytes reclaimed: ${result.reclaimedBytes}`,
5370
+ `Elapsed ms: ${result.elapsedMs}`,
5371
+ ""
5372
+ ].join("\n");
5373
+ }
5067
5374
  migrations(result, format) {
5068
5375
  if (format === "json") {
5069
5376
  return this.json(result);
@@ -5140,7 +5447,7 @@ var TelemetryCli = class {
5140
5447
  this.configurationParser = options.configurationParser ?? new NodeTelemetryConfigurationParser();
5141
5448
  this.stdout = options.stdout ?? ((text) => process.stdout.write(text));
5142
5449
  this.stderr = options.stderr ?? ((text) => process.stderr.write(text));
5143
- this.writeOutputFile = options.writeOutputFile ?? ((path, content) => (0, import_node_fs3.writeFileSync)(path, content, "utf8"));
5450
+ this.writeOutputFile = options.writeOutputFile ?? ((path, content) => (0, import_node_fs5.writeFileSync)(path, content, "utf8"));
5144
5451
  this.now = options.now ?? (() => Date.now());
5145
5452
  this.createSession = options.createSession ?? ((configuration, target, mode) => OfflineSqliteTelemetrySession.create(target, mode, configuration.metrics, this.now));
5146
5453
  }
@@ -5171,6 +5478,10 @@ var TelemetryCli = class {
5171
5478
  storageBaseDirectory === null ? {} : { storageBaseDirectory }
5172
5479
  );
5173
5480
  const target = this.resolveTarget(configuration, this.optionalOption(command, "target"));
5481
+ if (this.metricSource(command, target) === "history") {
5482
+ await this.executeHistoryMetrics(command, target, configuration);
5483
+ return 0;
5484
+ }
5174
5485
  const mode = this.sessionMode(command);
5175
5486
  const session = await this.createSession(configuration, target, mode);
5176
5487
  this.activeSession = session;
@@ -5184,23 +5495,61 @@ var TelemetryCli = class {
5184
5495
  }
5185
5496
  }
5186
5497
  }
5498
+ /**
5499
+ * Which store answers a metric command. `history` is the projector's
5500
+ * pre-aggregated buckets (history.db, current whenever the app runs with
5501
+ * ingest-time extraction off); `legacy` is the collector's own rollups in
5502
+ * telemetry.db. `auto` prefers history.db when it exists beside the target —
5503
+ * since ingestion moved to the projector the legacy rollups stopped
5504
+ * advancing, so answering from them silently reports stale gaps.
5505
+ */
5506
+ metricSource(command, target) {
5507
+ if (command.kind !== "metrics-query" && command.kind !== "metrics-graph") return "legacy";
5508
+ const requested = this.optionalOption(command, "source") ?? "auto";
5509
+ if (requested === "history" || requested === "legacy") return requested;
5510
+ if (requested !== "auto") {
5511
+ throw new TelemetryCliUsageError("--source must be auto, history, or legacy");
5512
+ }
5513
+ return HistoryMetricReader.exists(historyDatabasePath(target)) ? "history" : "legacy";
5514
+ }
5515
+ async executeHistoryMetrics(command, target, configuration) {
5516
+ const databasePath = historyDatabasePath(target);
5517
+ if (!HistoryMetricReader.exists(databasePath)) {
5518
+ throw new TelemetryCliUsageError(`No projector history store at ${databasePath}`);
5519
+ }
5520
+ const reader = HistoryMetricReader.open(databasePath, configuration.metrics);
5521
+ try {
5522
+ const run = async (query) => reader.metrics(query, target.targetId);
5523
+ if (command.kind === "metrics-graph") {
5524
+ await this.metricGraph(command, target, run);
5525
+ } else {
5526
+ await this.metricQuery(command, target, run);
5527
+ }
5528
+ } finally {
5529
+ reader.close();
5530
+ }
5531
+ }
5187
5532
  async executeCommand(command, target, session) {
5188
5533
  if (command.kind === "timeline-query") {
5189
5534
  await this.timelineQuery(command, target, session);
5190
5535
  return;
5191
5536
  }
5192
5537
  if (command.kind === "metrics-query") {
5193
- await this.metricQuery(command, target, session);
5538
+ await this.metricQuery(command, target, (query) => session.metrics(query));
5194
5539
  return;
5195
5540
  }
5196
5541
  if (command.kind === "metrics-graph") {
5197
- await this.metricGraph(command, target, session);
5542
+ await this.metricGraph(command, target, (query) => session.metrics(query));
5198
5543
  return;
5199
5544
  }
5200
5545
  if (command.kind === "sqlite-query") {
5201
5546
  await this.sqliteQuery(command, target, session);
5202
5547
  return;
5203
5548
  }
5549
+ if (command.kind === "sqlite-compact") {
5550
+ await this.sqliteCompact(command, target, session);
5551
+ return;
5552
+ }
5204
5553
  if (command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up") {
5205
5554
  await this.sqliteMigration(command, target, session);
5206
5555
  return;
@@ -5230,8 +5579,8 @@ var TelemetryCli = class {
5230
5579
  const output = this.formatter.timeline(result.value, format, this.timezoneOption(command));
5231
5580
  this.writeBounded(output, target.query.maxResponseBytes);
5232
5581
  }
5233
- async metricQuery(command, target, session) {
5234
- const result = await session.metrics(this.metricQueryInput(command, target));
5582
+ async metricQuery(command, target, runMetrics) {
5583
+ const result = await runMetrics(this.metricQueryInput(command, target));
5235
5584
  if (!result.ok) {
5236
5585
  throw new Error(`${result.error.kind}: ${result.error.message}`);
5237
5586
  }
@@ -5242,12 +5591,12 @@ var TelemetryCli = class {
5242
5591
  );
5243
5592
  this.writeBounded(output, target.query.maxResponseBytes);
5244
5593
  }
5245
- async metricGraph(command, target, session) {
5594
+ async metricGraph(command, target, runMetrics) {
5246
5595
  const measurements = this.options(command, "measurement");
5247
5596
  if (measurements.length !== 1) {
5248
5597
  throw new TelemetryCliUsageError("metrics graph requires exactly one --measurement");
5249
5598
  }
5250
- const result = await session.metrics(this.metricQueryInput(command, target));
5599
+ const result = await runMetrics(this.metricQueryInput(command, target));
5251
5600
  if (!result.ok) {
5252
5601
  throw new Error(`${result.error.kind}: ${result.error.message}`);
5253
5602
  }
@@ -5298,6 +5647,14 @@ var TelemetryCli = class {
5298
5647
  }
5299
5648
  this.writeBounded(this.formatter.migrations(result, format), target.query.maxResponseBytes);
5300
5649
  }
5650
+ async sqliteCompact(command, target, session) {
5651
+ const format = this.optionalOption(command, "format") ?? "table";
5652
+ if (format !== "table" && format !== "json") {
5653
+ throw new TelemetryCliUsageError("Compaction format must be table or json");
5654
+ }
5655
+ const result = await session.compact();
5656
+ this.writeBounded(this.formatter.compaction(result, format), target.query.maxResponseBytes);
5657
+ }
5301
5658
  metricQueryInput(command, target) {
5302
5659
  const measurements = this.options(command, "measurement");
5303
5660
  if (measurements.length === 0) {
@@ -5332,7 +5689,7 @@ var TelemetryCli = class {
5332
5689
  return target;
5333
5690
  }
5334
5691
  sessionMode(command) {
5335
- return command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
5692
+ return command.kind === "sqlite-compact" || command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
5336
5693
  }
5337
5694
  structuredFormat(command) {
5338
5695
  const value = this.optionalOption(command, "format") ?? "table";
@@ -5499,9 +5856,10 @@ var TelemetryCli = class {
5499
5856
  help() {
5500
5857
  return `Usage:
5501
5858
  telemetry timeline query --config FILE --from TIME --to TIME [filters] [--format table|json|jsonl|csv]
5502
- telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE]
5503
- telemetry metrics graph --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--type line|bar|stacked-bar] [--format terminal|svg|html]
5859
+ telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE] [--source auto|history|legacy]
5860
+ telemetry metrics graph --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--type line|bar|stacked-bar] [--format terminal|svg|html] [--source auto|history|legacy]
5504
5861
  telemetry sqlite query --config FILE --sql SQL [--param NAME=VALUE] [--format table|json|jsonl|csv]
5862
+ telemetry sqlite compact --config FILE [--format table|json]
5505
5863
  telemetry sqlite migration status --config FILE [--format table|json]
5506
5864
  telemetry sqlite migration up --config FILE [--format table|json]
5507
5865