@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
@@ -67,11 +67,15 @@ var MetricDefinitionContract = class {
67
67
  if (!eventSelector || !bracket) {
68
68
  return { ok: false, error: `Invalid metric selector ${selector}` };
69
69
  }
70
+ const groupBy = bracket === "*" ? [] : bracket.split(",").map((field) => field.trim());
71
+ if (groupBy.some((field) => field.length === 0)) {
72
+ return { ok: false, error: `Invalid metric selector ${selector}` };
73
+ }
70
74
  return {
71
75
  ok: true,
72
76
  value: {
73
77
  selector: eventSelector,
74
- groupBy: bracket === "*" ? [] : [bracket],
78
+ groupBy,
75
79
  wildcardUngrouped: bracket === "*"
76
80
  }
77
81
  };
@@ -1787,18 +1791,194 @@ var MetricGraphRenderer = class {
1787
1791
  }
1788
1792
  };
1789
1793
 
1794
+ // src/cli/history_metric_reader.ts
1795
+ import { existsSync } from "fs";
1796
+ import { dirname as dirname2, join } from "path";
1797
+ import BetterSqlite3 from "better-sqlite3";
1798
+ var HISTORY_DATABASE_NAME = "history.db";
1799
+ var AGGREGATES = /* @__PURE__ */ new Set(["count", "sum", "min", "max", "last", "avg"]);
1800
+ function historyDatabasePath(target) {
1801
+ return join(dirname2(target.path), HISTORY_DATABASE_NAME);
1802
+ }
1803
+ var HistoryMetricReader = class _HistoryMetricReader {
1804
+ constructor(db, definitions) {
1805
+ this.db = db;
1806
+ this.definitions = definitions;
1807
+ }
1808
+ db;
1809
+ definitions;
1810
+ static open(databasePath, definitions) {
1811
+ const db = new BetterSqlite3(databasePath, { readonly: true, fileMustExist: true });
1812
+ db.pragma("busy_timeout = 1000");
1813
+ db.pragma("query_only = ON");
1814
+ return new _HistoryMetricReader(db, definitions);
1815
+ }
1816
+ static exists(databasePath) {
1817
+ return existsSync(databasePath);
1818
+ }
1819
+ metrics(query, targetId) {
1820
+ if (query.toMs <= query.fromMs) {
1821
+ return { ok: false, error: { kind: "invalid-query", message: "--to must be after --from" } };
1822
+ }
1823
+ const invalidMeasurement = query.measurements.find((measurement) => !isMeasurement(measurement));
1824
+ if (invalidMeasurement !== void 0) {
1825
+ return {
1826
+ ok: false,
1827
+ error: {
1828
+ kind: "invalid-query",
1829
+ message: `Unknown measurement ${invalidMeasurement}; use count or <field>.<sum|min|max|last|count|avg>`
1830
+ }
1831
+ };
1832
+ }
1833
+ const versions = this.registeredVersions(query.metric);
1834
+ if (versions.length === 0) {
1835
+ return {
1836
+ ok: false,
1837
+ error: { kind: "invalid-query", message: `Metric ${query.metric} has no projection in history.db` }
1838
+ };
1839
+ }
1840
+ const resolutionMs = this.resolveResolution(query, versions);
1841
+ if (resolutionMs === null) {
1842
+ return {
1843
+ ok: false,
1844
+ error: { kind: "invalid-query", message: `Metric ${query.metric} is not projected at the requested resolution` }
1845
+ };
1846
+ }
1847
+ for (let index = versions.length - 1; index >= 0; index -= 1) {
1848
+ const version = versions[index];
1849
+ const buckets = this.readBuckets(query, version, resolutionMs);
1850
+ if (buckets.length === 0) continue;
1851
+ const olderVersions = versions.slice(0, index);
1852
+ return {
1853
+ ok: true,
1854
+ value: Object.freeze({
1855
+ targetId,
1856
+ metric: query.metric,
1857
+ segments: [Object.freeze({
1858
+ fromMs: query.fromMs,
1859
+ toMs: query.toMs,
1860
+ resolutionMs,
1861
+ isAggregated: true,
1862
+ definitionVersion: version,
1863
+ buckets
1864
+ })],
1865
+ gaps: [],
1866
+ nextCursor: null,
1867
+ partialFailures: olderVersions.map((older) => Object.freeze({
1868
+ targetId,
1869
+ reason: `version ${older} buckets omitted; only version ${version} returned`
1870
+ }))
1871
+ })
1872
+ };
1873
+ }
1874
+ return {
1875
+ ok: true,
1876
+ value: Object.freeze({
1877
+ targetId,
1878
+ metric: query.metric,
1879
+ segments: [],
1880
+ gaps: [Object.freeze({ fromMs: query.fromMs, toMs: query.toMs, reason: "not-materialized" })],
1881
+ nextCursor: null,
1882
+ partialFailures: []
1883
+ })
1884
+ };
1885
+ }
1886
+ close() {
1887
+ if (this.db.open) this.db.close();
1888
+ }
1889
+ registeredVersions(metric) {
1890
+ const rows = this.db.prepare(
1891
+ "SELECT projection_version FROM trace_projection_definitions WHERE projection_name = ? ORDER BY projection_version"
1892
+ ).all(metric);
1893
+ return rows.map((row) => row.projection_version);
1894
+ }
1895
+ resolveResolution(query, versions) {
1896
+ const available = this.definitions.filter((definition) => definition.name === query.metric && versions.includes(definition.version)).flatMap((definition) => [...definition.resolutionsMs]);
1897
+ const resolutions = [...new Set(available.length > 0 ? available : this.storedResolutions(query.metric))].sort((a, b) => a - b);
1898
+ if (resolutions.length === 0) return null;
1899
+ if (query.resolution !== "auto") {
1900
+ return resolutions.includes(query.resolution) ? query.resolution : null;
1901
+ }
1902
+ const span = query.toMs - query.fromMs;
1903
+ return resolutions.find((resolution) => Math.ceil(span / resolution) <= query.limit) ?? resolutions[resolutions.length - 1];
1904
+ }
1905
+ storedResolutions(metric) {
1906
+ const rows = this.db.prepare(
1907
+ "SELECT DISTINCT resolution_ms FROM trace_metric_buckets WHERE projection_name = ?"
1908
+ ).all(metric);
1909
+ return rows.map((row) => row.resolution_ms);
1910
+ }
1911
+ readBuckets(query, version, resolutionMs) {
1912
+ const rows = this.db.prepare(`
1913
+ SELECT projection_version, bucket_start_ms, dimensions_json, count, values_json
1914
+ FROM trace_metric_buckets
1915
+ WHERE projection_name = ? AND projection_version = ? AND resolution_ms = ?
1916
+ AND bucket_start_ms >= ? AND bucket_start_ms < ?
1917
+ ORDER BY bucket_start_ms ${query.order === "desc" ? "DESC" : "ASC"}, dimension_key
1918
+ `).all(query.metric, version, resolutionMs, query.fromMs, query.toMs);
1919
+ const buckets = [];
1920
+ for (const row of rows) {
1921
+ const dimensions = JSON.parse(row.dimensions_json);
1922
+ if (!matchesWhere(dimensions, query.where)) continue;
1923
+ const values = JSON.parse(row.values_json);
1924
+ buckets.push(Object.freeze({
1925
+ startMs: row.bucket_start_ms,
1926
+ endMs: row.bucket_start_ms + resolutionMs,
1927
+ dimensions,
1928
+ values: selectMeasurements(query.measurements, row.count, values)
1929
+ }));
1930
+ if (buckets.length >= query.limit) break;
1931
+ }
1932
+ return buckets;
1933
+ }
1934
+ };
1935
+ function isMeasurement(measurement) {
1936
+ if (measurement === "count") return true;
1937
+ const dot = measurement.lastIndexOf(".");
1938
+ return dot > 0 && AGGREGATES.has(measurement.slice(dot + 1));
1939
+ }
1940
+ function selectMeasurements(measurements, bucketCount, values) {
1941
+ const selected = {};
1942
+ for (const measurement of measurements) {
1943
+ if (measurement === "count") {
1944
+ selected.count = bucketCount;
1945
+ continue;
1946
+ }
1947
+ const dot = measurement.lastIndexOf(".");
1948
+ const field = measurement.slice(0, dot);
1949
+ const aggregate = measurement.slice(dot + 1);
1950
+ const state = values[field];
1951
+ if (state === void 0) continue;
1952
+ const value = aggregate === "avg" ? state.count > 0 ? state.sum / state.count : 0 : state[aggregate];
1953
+ if (!Number.isFinite(value)) continue;
1954
+ const existing = selected[field];
1955
+ selected[field] = { ...typeof existing === "object" ? existing : {}, [aggregate]: value };
1956
+ }
1957
+ return Object.freeze(selected);
1958
+ }
1959
+ function matchesWhere(dimensions, where) {
1960
+ for (const [key, filter] of Object.entries(where)) {
1961
+ const actual = dimensions[key];
1962
+ if (actual === void 0) return false;
1963
+ const accepted = Array.isArray(filter) ? filter : [filter];
1964
+ if (!accepted.some((candidate) => String(candidate) === actual)) return false;
1965
+ }
1966
+ return true;
1967
+ }
1968
+
1790
1969
  // src/cli/offline_sqlite_session.ts
1791
1970
  import { randomUUID } from "crypto";
1792
- import { resolve as resolve3 } from "path";
1971
+ import { existsSync as existsSync3, statfsSync, statSync } from "fs";
1972
+ import { dirname as dirname4, resolve as resolve3 } from "path";
1793
1973
  import { fileURLToPath } from "url";
1794
1974
  import { MigrationRunnerFactory } from "@noego/proper";
1795
- import BetterSqlite3 from "better-sqlite3";
1975
+ import BetterSqlite32 from "better-sqlite3";
1796
1976
  import { createSqliteDb } from "sqlstack/adapters";
1797
1977
 
1798
1978
  // src/storage/sqlite_ownership_lease.ts
1799
1979
  import {
1800
1980
  closeSync,
1801
- existsSync,
1981
+ existsSync as existsSync2,
1802
1982
  mkdirSync,
1803
1983
  openSync,
1804
1984
  readFileSync as readFileSync2,
@@ -1807,13 +1987,13 @@ import {
1807
1987
  rmSync,
1808
1988
  writeFileSync
1809
1989
  } from "fs";
1810
- import { dirname as dirname2, resolve as resolve2 } from "path";
1990
+ import { dirname as dirname3, resolve as resolve2 } from "path";
1811
1991
  var SqliteOwnershipLease = class {
1812
1992
  constructor(options) {
1813
1993
  this.options = options;
1814
- mkdirSync(dirname2(options.databasePath), { recursive: true });
1815
- const canonicalDirectory = realpathSync(dirname2(resolve2(options.databasePath)));
1816
- const databaseName = resolve2(options.databasePath).slice(dirname2(resolve2(options.databasePath)).length + 1);
1994
+ mkdirSync(dirname3(options.databasePath), { recursive: true });
1995
+ const canonicalDirectory = realpathSync(dirname3(resolve2(options.databasePath)));
1996
+ const databaseName = resolve2(options.databasePath).slice(dirname3(resolve2(options.databasePath)).length + 1);
1817
1997
  this.canonicalDatabasePath = resolve2(canonicalDirectory, databaseName);
1818
1998
  this.leasePath = `${this.canonicalDatabasePath}.telemetry-owner.json`;
1819
1999
  }
@@ -1857,7 +2037,7 @@ var SqliteOwnershipLease = class {
1857
2037
  });
1858
2038
  }
1859
2039
  release() {
1860
- if (!this.held || !existsSync(this.leasePath)) {
2040
+ if (!this.held || !existsSync2(this.leasePath)) {
1861
2041
  this.held = false;
1862
2042
  return;
1863
2043
  }
@@ -4484,6 +4664,87 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4484
4664
  this.verifySchema();
4485
4665
  return this.migrationStatus();
4486
4666
  }
4667
+ async compact() {
4668
+ this.requireMaintenanceMode();
4669
+ const connection = this.requireConnection();
4670
+ const maintenanceTable = this.requireTableMap().physical("maintenanceRuns");
4671
+ const runId = randomUUID();
4672
+ const startedAtMs = this.now();
4673
+ connection.prepare(
4674
+ `INSERT INTO "${maintenanceTable}" (
4675
+ target_id, run_id, state, stage, started_at_ms,
4676
+ completed_at_ms, affected_rows, affected_pages, detail
4677
+ ) VALUES (?, ?, 'started', 'compaction', ?, NULL, 0, 0, ?)`
4678
+ ).run(this.target.targetId, runId, startedAtMs, "offline-cli; outcome=started");
4679
+ try {
4680
+ const before = this.compactionSnapshot();
4681
+ connection.pragma("wal_checkpoint(TRUNCATE)");
4682
+ const checkpointed = this.compactionSnapshot();
4683
+ this.assertCompactionSpace(checkpointed);
4684
+ connection.pragma("auto_vacuum = INCREMENTAL");
4685
+ connection.exec("VACUUM");
4686
+ connection.pragma("wal_checkpoint(TRUNCATE)");
4687
+ const after = this.compactionSnapshot();
4688
+ if (after.autoVacuum !== 2) {
4689
+ throw new Error("Telemetry compaction did not enable auto_vacuum=INCREMENTAL");
4690
+ }
4691
+ const completedAtMs = this.now();
4692
+ const result = Object.freeze({
4693
+ targetId: this.target.targetId,
4694
+ autoVacuum: "incremental",
4695
+ pageSize: checkpointed.pageSize,
4696
+ pagesBefore: checkpointed.pageCount,
4697
+ pagesAfter: after.pageCount,
4698
+ freePagesBefore: checkpointed.freePages,
4699
+ freePagesAfter: after.freePages,
4700
+ reclaimedPages: Math.max(0, checkpointed.pageCount - after.pageCount),
4701
+ databaseBytesBefore: before.databaseBytes,
4702
+ databaseBytesAfter: after.databaseBytes,
4703
+ walBytesBefore: before.walBytes,
4704
+ walBytesAfter: after.walBytes,
4705
+ reclaimedBytes: Math.max(
4706
+ 0,
4707
+ before.databaseBytes + before.walBytes - after.databaseBytes - after.walBytes
4708
+ ),
4709
+ elapsedMs: completedAtMs - startedAtMs
4710
+ });
4711
+ connection.prepare(
4712
+ `UPDATE "${maintenanceTable}"
4713
+ SET state = 'completed', completed_at_ms = ?, affected_pages = ?, detail = ?
4714
+ WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
4715
+ ).run(
4716
+ completedAtMs,
4717
+ result.reclaimedPages,
4718
+ JSON.stringify({
4719
+ mode: "offline-cli",
4720
+ outcome: "completed",
4721
+ reclaimedBytes: result.reclaimedBytes,
4722
+ walBytesBefore: result.walBytesBefore,
4723
+ walBytesAfter: result.walBytesAfter,
4724
+ autoVacuum: result.autoVacuum
4725
+ }),
4726
+ this.target.targetId,
4727
+ runId
4728
+ );
4729
+ return result;
4730
+ } catch (cause) {
4731
+ const message = cause instanceof Error ? cause.message : String(cause);
4732
+ try {
4733
+ connection.prepare(
4734
+ `UPDATE "${maintenanceTable}"
4735
+ SET state = 'failed', completed_at_ms = ?, detail = ?
4736
+ WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
4737
+ ).run(
4738
+ this.now(),
4739
+ JSON.stringify({ mode: "offline-cli", outcome: "failed", error: message }),
4740
+ this.target.targetId,
4741
+ runId
4742
+ );
4743
+ } catch {
4744
+ }
4745
+ throw cause;
4746
+ }
4747
+ }
4487
4748
  async close() {
4488
4749
  try {
4489
4750
  if (this.connection?.open) {
@@ -4517,7 +4778,7 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4517
4778
  throw new Error(acquired.error);
4518
4779
  }
4519
4780
  }
4520
- const connection = new BetterSqlite3(this.target.path, {
4781
+ const connection = new BetterSqlite32(this.target.path, {
4521
4782
  readonly: this.mode === "query",
4522
4783
  fileMustExist: true
4523
4784
  });
@@ -4544,6 +4805,32 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4544
4805
  }
4545
4806
  }
4546
4807
  }
4808
+ compactionSnapshot() {
4809
+ const connection = this.requireConnection();
4810
+ return Object.freeze({
4811
+ pageSize: Number(connection.pragma("page_size", { simple: true })),
4812
+ pageCount: Number(connection.pragma("page_count", { simple: true })),
4813
+ freePages: Number(connection.pragma("freelist_count", { simple: true })),
4814
+ autoVacuum: Number(connection.pragma("auto_vacuum", { simple: true })),
4815
+ databaseBytes: this.fileBytes(this.target.path),
4816
+ walBytes: this.fileBytes(`${this.target.path}-wal`)
4817
+ });
4818
+ }
4819
+ assertCompactionSpace(snapshot) {
4820
+ const liveBytes = Math.max(0, snapshot.pageCount - snapshot.freePages) * snapshot.pageSize;
4821
+ const reserveBytes = Math.max(64 * 1024 * 1024, Math.ceil(liveBytes * 0.05));
4822
+ const filesystem = statfsSync(dirname4(this.target.path));
4823
+ const availableBytes = Number(filesystem.bavail) * Number(filesystem.bsize);
4824
+ const requiredBytes = liveBytes + reserveBytes;
4825
+ if (availableBytes < requiredBytes) {
4826
+ throw new Error(
4827
+ `Telemetry compaction requires at least ${requiredBytes} free bytes; only ${availableBytes} are available`
4828
+ );
4829
+ }
4830
+ }
4831
+ fileBytes(path) {
4832
+ return existsSync3(path) ? statSync(path).size : 0;
4833
+ }
4547
4834
  verifyManifest() {
4548
4835
  const rows = this.requireConnection().prepare(
4549
4836
  `SELECT target_id, format_version, mapping_json, mapping_checksum,
@@ -4728,7 +5015,8 @@ var ALLOWED_OPTIONS = Object.freeze({
4728
5015
  "metric",
4729
5016
  "where",
4730
5017
  "measurement",
4731
- "resolution"
5018
+ "resolution",
5019
+ "source"
4732
5020
  ]),
4733
5021
  "metrics-graph": /* @__PURE__ */ new Set([
4734
5022
  ...COMMON_QUERY_OPTIONS,
@@ -4736,6 +5024,7 @@ var ALLOWED_OPTIONS = Object.freeze({
4736
5024
  "where",
4737
5025
  "measurement",
4738
5026
  "resolution",
5027
+ "source",
4739
5028
  "type",
4740
5029
  "output",
4741
5030
  "width",
@@ -4753,6 +5042,7 @@ var ALLOWED_OPTIONS = Object.freeze({
4753
5042
  "limit",
4754
5043
  "max-duration"
4755
5044
  ]),
5045
+ "sqlite-compact": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
4756
5046
  "sqlite-migration-status": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
4757
5047
  "sqlite-migration-up": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"])
4758
5048
  });
@@ -4762,6 +5052,7 @@ var COMMANDS = Object.freeze([
4762
5052
  { tokens: ["metrics", "query"], kind: "metrics-query" },
4763
5053
  { tokens: ["metrics", "graph"], kind: "metrics-graph" },
4764
5054
  { tokens: ["sqlite", "query"], kind: "sqlite-query" },
5055
+ { tokens: ["sqlite", "compact"], kind: "sqlite-compact" },
4765
5056
  { tokens: ["sqlite", "migration", "status"], kind: "sqlite-migration-status" },
4766
5057
  { tokens: ["sqlite", "migration", "up"], kind: "sqlite-migration-up" }
4767
5058
  ]);
@@ -5040,6 +5331,22 @@ var TelemetryCliFormatter = class {
5040
5331
  return `${lines.join("\n")}
5041
5332
  `;
5042
5333
  }
5334
+ compaction(result, format) {
5335
+ if (format === "json") {
5336
+ return this.json(result);
5337
+ }
5338
+ return [
5339
+ `Target: ${result.targetId}`,
5340
+ `Auto-vacuum: ${result.autoVacuum}`,
5341
+ `Pages: ${result.pagesBefore} -> ${result.pagesAfter} (${result.reclaimedPages} reclaimed)`,
5342
+ `Freelist: ${result.freePagesBefore} -> ${result.freePagesAfter}`,
5343
+ `Database bytes: ${result.databaseBytesBefore} -> ${result.databaseBytesAfter}`,
5344
+ `WAL bytes: ${result.walBytesBefore} -> ${result.walBytesAfter}`,
5345
+ `Filesystem bytes reclaimed: ${result.reclaimedBytes}`,
5346
+ `Elapsed ms: ${result.elapsedMs}`,
5347
+ ""
5348
+ ].join("\n");
5349
+ }
5043
5350
  migrations(result, format) {
5044
5351
  if (format === "json") {
5045
5352
  return this.json(result);
@@ -5147,6 +5454,10 @@ var TelemetryCli = class {
5147
5454
  storageBaseDirectory === null ? {} : { storageBaseDirectory }
5148
5455
  );
5149
5456
  const target = this.resolveTarget(configuration, this.optionalOption(command, "target"));
5457
+ if (this.metricSource(command, target) === "history") {
5458
+ await this.executeHistoryMetrics(command, target, configuration);
5459
+ return 0;
5460
+ }
5150
5461
  const mode = this.sessionMode(command);
5151
5462
  const session = await this.createSession(configuration, target, mode);
5152
5463
  this.activeSession = session;
@@ -5160,23 +5471,61 @@ var TelemetryCli = class {
5160
5471
  }
5161
5472
  }
5162
5473
  }
5474
+ /**
5475
+ * Which store answers a metric command. `history` is the projector's
5476
+ * pre-aggregated buckets (history.db, current whenever the app runs with
5477
+ * ingest-time extraction off); `legacy` is the collector's own rollups in
5478
+ * telemetry.db. `auto` prefers history.db when it exists beside the target —
5479
+ * since ingestion moved to the projector the legacy rollups stopped
5480
+ * advancing, so answering from them silently reports stale gaps.
5481
+ */
5482
+ metricSource(command, target) {
5483
+ if (command.kind !== "metrics-query" && command.kind !== "metrics-graph") return "legacy";
5484
+ const requested = this.optionalOption(command, "source") ?? "auto";
5485
+ if (requested === "history" || requested === "legacy") return requested;
5486
+ if (requested !== "auto") {
5487
+ throw new TelemetryCliUsageError("--source must be auto, history, or legacy");
5488
+ }
5489
+ return HistoryMetricReader.exists(historyDatabasePath(target)) ? "history" : "legacy";
5490
+ }
5491
+ async executeHistoryMetrics(command, target, configuration) {
5492
+ const databasePath = historyDatabasePath(target);
5493
+ if (!HistoryMetricReader.exists(databasePath)) {
5494
+ throw new TelemetryCliUsageError(`No projector history store at ${databasePath}`);
5495
+ }
5496
+ const reader = HistoryMetricReader.open(databasePath, configuration.metrics);
5497
+ try {
5498
+ const run = async (query) => reader.metrics(query, target.targetId);
5499
+ if (command.kind === "metrics-graph") {
5500
+ await this.metricGraph(command, target, run);
5501
+ } else {
5502
+ await this.metricQuery(command, target, run);
5503
+ }
5504
+ } finally {
5505
+ reader.close();
5506
+ }
5507
+ }
5163
5508
  async executeCommand(command, target, session) {
5164
5509
  if (command.kind === "timeline-query") {
5165
5510
  await this.timelineQuery(command, target, session);
5166
5511
  return;
5167
5512
  }
5168
5513
  if (command.kind === "metrics-query") {
5169
- await this.metricQuery(command, target, session);
5514
+ await this.metricQuery(command, target, (query) => session.metrics(query));
5170
5515
  return;
5171
5516
  }
5172
5517
  if (command.kind === "metrics-graph") {
5173
- await this.metricGraph(command, target, session);
5518
+ await this.metricGraph(command, target, (query) => session.metrics(query));
5174
5519
  return;
5175
5520
  }
5176
5521
  if (command.kind === "sqlite-query") {
5177
5522
  await this.sqliteQuery(command, target, session);
5178
5523
  return;
5179
5524
  }
5525
+ if (command.kind === "sqlite-compact") {
5526
+ await this.sqliteCompact(command, target, session);
5527
+ return;
5528
+ }
5180
5529
  if (command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up") {
5181
5530
  await this.sqliteMigration(command, target, session);
5182
5531
  return;
@@ -5206,8 +5555,8 @@ var TelemetryCli = class {
5206
5555
  const output = this.formatter.timeline(result.value, format, this.timezoneOption(command));
5207
5556
  this.writeBounded(output, target.query.maxResponseBytes);
5208
5557
  }
5209
- async metricQuery(command, target, session) {
5210
- const result = await session.metrics(this.metricQueryInput(command, target));
5558
+ async metricQuery(command, target, runMetrics) {
5559
+ const result = await runMetrics(this.metricQueryInput(command, target));
5211
5560
  if (!result.ok) {
5212
5561
  throw new Error(`${result.error.kind}: ${result.error.message}`);
5213
5562
  }
@@ -5218,12 +5567,12 @@ var TelemetryCli = class {
5218
5567
  );
5219
5568
  this.writeBounded(output, target.query.maxResponseBytes);
5220
5569
  }
5221
- async metricGraph(command, target, session) {
5570
+ async metricGraph(command, target, runMetrics) {
5222
5571
  const measurements = this.options(command, "measurement");
5223
5572
  if (measurements.length !== 1) {
5224
5573
  throw new TelemetryCliUsageError("metrics graph requires exactly one --measurement");
5225
5574
  }
5226
- const result = await session.metrics(this.metricQueryInput(command, target));
5575
+ const result = await runMetrics(this.metricQueryInput(command, target));
5227
5576
  if (!result.ok) {
5228
5577
  throw new Error(`${result.error.kind}: ${result.error.message}`);
5229
5578
  }
@@ -5274,6 +5623,14 @@ var TelemetryCli = class {
5274
5623
  }
5275
5624
  this.writeBounded(this.formatter.migrations(result, format), target.query.maxResponseBytes);
5276
5625
  }
5626
+ async sqliteCompact(command, target, session) {
5627
+ const format = this.optionalOption(command, "format") ?? "table";
5628
+ if (format !== "table" && format !== "json") {
5629
+ throw new TelemetryCliUsageError("Compaction format must be table or json");
5630
+ }
5631
+ const result = await session.compact();
5632
+ this.writeBounded(this.formatter.compaction(result, format), target.query.maxResponseBytes);
5633
+ }
5277
5634
  metricQueryInput(command, target) {
5278
5635
  const measurements = this.options(command, "measurement");
5279
5636
  if (measurements.length === 0) {
@@ -5308,7 +5665,7 @@ var TelemetryCli = class {
5308
5665
  return target;
5309
5666
  }
5310
5667
  sessionMode(command) {
5311
- return command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
5668
+ return command.kind === "sqlite-compact" || command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
5312
5669
  }
5313
5670
  structuredFormat(command) {
5314
5671
  const value = this.optionalOption(command, "format") ?? "table";
@@ -5475,9 +5832,10 @@ var TelemetryCli = class {
5475
5832
  help() {
5476
5833
  return `Usage:
5477
5834
  telemetry timeline query --config FILE --from TIME --to TIME [filters] [--format table|json|jsonl|csv]
5478
- telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE]
5479
- 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]
5835
+ telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE] [--source auto|history|legacy]
5836
+ 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]
5480
5837
  telemetry sqlite query --config FILE --sql SQL [--param NAME=VALUE] [--format table|json|jsonl|csv]
5838
+ telemetry sqlite compact --config FILE [--format table|json]
5481
5839
  telemetry sqlite migration status --config FILE [--format table|json]
5482
5840
  telemetry sqlite migration up --config FILE [--format table|json]
5483
5841