@interactive-inc/claude-funnel 0.67.0 → 0.68.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.
Files changed (54) hide show
  1. package/README.md +24 -3
  2. package/dist/bin.js +276 -270
  3. package/dist/{channels-BBA3GTAa.d.ts → channels-B0cifCfw.d.ts} +2 -1
  4. package/dist/claude.d.ts +2 -2
  5. package/dist/claude.js +5 -5
  6. package/dist/connector-descriptor-D4XtEsH8.d.ts +215 -0
  7. package/dist/connectors/discord.d.ts +3 -2
  8. package/dist/connectors/discord.js +1 -1
  9. package/dist/connectors/gh.d.ts +3 -2
  10. package/dist/connectors/gh.js +1 -1
  11. package/dist/connectors/schedule.d.ts +13 -11
  12. package/dist/connectors/schedule.js +3 -3
  13. package/dist/connectors/slack.d.ts +3 -2
  14. package/dist/connectors/slack.js +1 -1
  15. package/dist/diagnostic-event-CxMM5Bl2.js +78 -0
  16. package/dist/diagnostic-log-BsAg5qZK.d.ts +186 -0
  17. package/dist/diagnostics.d.ts +2 -2
  18. package/dist/diagnostics.js +3 -2
  19. package/dist/{discord-connector-CdjwPoty.js → discord-connector-CAcWifJn.js} +1 -1
  20. package/dist/docs.js +1 -1
  21. package/dist/doctor.d.ts +1 -1
  22. package/dist/event-log.d.ts +2 -0
  23. package/dist/event-log.js +3 -0
  24. package/dist/{file-process-guard-DAYe9Ndt.d.ts → file-process-guard-D-QatrwL.d.ts} +1 -1
  25. package/dist/{flume-source-listener-CLtsdcT2.d.ts → flume-source-listener-Ba8_1Mg-.d.ts} +2 -1
  26. package/dist/{funnel-diagnostics-B-PCuv_E.js → funnel-diagnostics-7rV3HxY3.js} +100 -143
  27. package/dist/{funnel-diagnostics-CnKus3Jg.d.ts → funnel-diagnostics-CDrZznTB.d.ts} +10 -13
  28. package/dist/{funnel-docs-BFcqITFA.js → funnel-docs-CI4hMkhq.js} +8 -3
  29. package/dist/{funnel-doctor-CviUTINw.d.ts → funnel-doctor-CY4S5cnz.d.ts} +1 -1
  30. package/dist/gateway/daemon.js +63 -83
  31. package/dist/gateway.d.ts +2 -2
  32. package/dist/gateway.js +1 -1
  33. package/dist/{gh-connector-D-iyjl-d.js → gh-connector-DMwI6zJA.js} +1 -1
  34. package/dist/{index-B4RvEaW-.d.ts → index-fHrKn4jm.d.ts} +7 -2
  35. package/dist/index.d.ts +18 -14
  36. package/dist/index.js +124 -30
  37. package/dist/local-config.d.ts +1 -1
  38. package/dist/logger.d.ts +6 -270
  39. package/dist/logger.js +6 -141
  40. package/dist/memory-event-log-BY8XjFzi.js +147 -0
  41. package/dist/{memory-token-prompter-D3R_kgM_.d.ts → memory-token-prompter-B20tl_aU.d.ts} +1 -1
  42. package/dist/{schedule-connector-teeDMHUe.js → schedule-connector-BXUOYgwZ.js} +59 -95
  43. package/dist/schedule-connector-schema-CNV8PgWX.js +137 -0
  44. package/dist/schedule-connector-schema-Tq5kJul-.d.ts +127 -0
  45. package/dist/{slack-connector-BE0KAjCs.js → slack-connector-BTtUiXAU.js} +2 -2
  46. package/dist/{slot-fields-5ERhytIA.js → slot-fields-eCWizJqS.js} +1 -1
  47. package/dist/{sqlite-diagnostic-log-BDxfXGZy.js → sqlite-diagnostic-log-Qs4cf9I_.js} +53 -84
  48. package/dist/sqlite-event-log-BYgCFMxC.d.ts +284 -0
  49. package/dist/{funnel-log-sqlite-sink-CXbs3qQp.js → sqlite-event-log-eZuXKszJ.js} +52 -11
  50. package/package.json +16 -1
  51. package/dist/connector-descriptor-F75ZI8Fh.d.ts +0 -391
  52. package/dist/diagnostic-sql-reader-CoBk_EFF.js +0 -83
  53. package/dist/schedule-connector-schema-C2AChpwX.js +0 -31
  54. package/dist/schedule-connector-schema-DBko_Bf7.d.ts +0 -48
@@ -3,8 +3,8 @@ import { t as NodeFunnelFileSystem } from "./node-file-system-xJx7dymE.js";
3
3
  import { t as NodeFunnelProcessRunner } from "./node-process-runner-D4rzPl8Q.js";
4
4
  import { n as FUNNEL_DIR, s as resolveFunnelPort } from "./settings-store-ZFWBhOnp.js";
5
5
  import { t as FunnelAuthFailedError } from "./funnel-error-DoSBCogD.js";
6
- import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-CoBk_EFF.js";
7
- import { t as FunnelLogSqliteSink } from "./funnel-log-sqlite-sink-CXbs3qQp.js";
6
+ import { n as diagnosticEventOfProcessed, t as diagnosticConnectionEventOf } from "./diagnostic-event-CxMM5Bl2.js";
7
+ import { t as SqliteEventLog } from "./sqlite-event-log-eZuXKszJ.js";
8
8
  import { dirname, join } from "node:path";
9
9
  import { chmodSync, existsSync, mkdirSync } from "node:fs";
10
10
  import { homedir, tmpdir } from "node:os";
@@ -159,13 +159,19 @@ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
159
159
  });
160
160
  }
161
161
  queryRaw(query) {
162
- const matched = this.raws.filter((event) => matches(event, query));
162
+ const matched = this.raws.filter((event) => {
163
+ if (!matches(event, query)) return false;
164
+ if (query.eventId !== void 0 && event.eventId !== query.eventId) return false;
165
+ return true;
166
+ });
163
167
  return takeRecent(matched, query.limit);
164
168
  }
165
169
  queryProcessed(query) {
166
170
  const matched = this.processeds.filter((event) => {
167
171
  if (!matches(event, query)) return false;
172
+ if (query.eventId !== void 0 && event.eventId !== query.eventId) return false;
168
173
  if (query.outcome !== void 0 && event.outcome !== query.outcome) return false;
174
+ if (query.outcomePrefix !== void 0 && !event.outcome.startsWith(query.outcomePrefix)) return false;
169
175
  return true;
170
176
  });
171
177
  return takeRecent(matched, query.limit);
@@ -174,6 +180,7 @@ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
174
180
  const matched = this.connections.filter((event) => {
175
181
  if (!matches(event, query)) return false;
176
182
  if (query.status !== void 0 && event.status !== query.status) return false;
183
+ if (query.statuses !== void 0 && !query.statuses.includes(event.status)) return false;
177
184
  return true;
178
185
  });
179
186
  return takeRecent(matched, query.limit);
@@ -189,6 +196,7 @@ const matches = (event, query) => {
189
196
  if (query.type !== void 0 && event.type !== query.type) return false;
190
197
  if (query.connectorId !== void 0 && event.connectorId !== query.connectorId) return false;
191
198
  if (query.channelId !== void 0 && event.channelId !== query.channelId) return false;
199
+ if ("seq" in event && query.seq !== void 0 && event.seq !== query.seq) return false;
192
200
  return true;
193
201
  };
194
202
  const takeRecent = (events, limit) => {
@@ -452,7 +460,6 @@ var FunnelBroadcaster = class {
452
460
  offset: this.latestOffset
453
461
  };
454
462
  const payload = JSON.stringify(event);
455
- meta?.connector;
456
463
  this.eventsBroadcast += 1;
457
464
  this.lastBroadcastAt = this.now();
458
465
  if (this.replayBufferSize > 0) {
@@ -579,7 +586,7 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
579
586
  this.now = props.now ?? (() => Date.now());
580
587
  this.logger = props.logger;
581
588
  this.onError = props.onError;
582
- this.sink = new FunnelLogSqliteSink({
589
+ this.sink = new SqliteEventLog({
583
590
  path: props.path,
584
591
  indexes: ["channel_id", "connector_id"],
585
592
  extractIndexes: (event) => ({
@@ -893,7 +900,7 @@ var FunnelListenerRegistry = class FunnelListenerRegistry {
893
900
  async stopAll() {
894
901
  this.stopHealthCheck();
895
902
  this.pendingRetry.clear();
896
- for (const [, entry] of [...this.running.entries()]) await this.stop(entry.channelName, entry.config.name);
903
+ for (const [, entry] of this.running.entries()) await this.stop(entry.channelName, entry.config.name);
897
904
  }
898
905
  ensureStats(key) {
899
906
  const existing = this.stats.get(key);
@@ -940,7 +947,7 @@ var FunnelListenerRegistry = class FunnelListenerRegistry {
940
947
  this.healthCheckInFlight = true;
941
948
  try {
942
949
  const dead = [];
943
- for (const [key, entry] of [...this.running.entries()]) {
950
+ for (const [key, entry] of this.running.entries()) {
944
951
  if (entry.listener.isAlive()) {
945
952
  this.failureCounts.delete(key);
946
953
  continue;
@@ -953,7 +960,7 @@ var FunnelListenerRegistry = class FunnelListenerRegistry {
953
960
  }
954
961
  await Promise.all(dead.map((target) => this.recoverDead(target.channelName, target.connectorName, target.type)));
955
962
  const retries = [];
956
- for (const [key, pending] of [...this.pendingRetry.entries()]) {
963
+ for (const [key, pending] of this.pendingRetry.entries()) {
957
964
  if (this.running.has(key)) {
958
965
  this.pendingRetry.delete(key);
959
966
  continue;
@@ -1189,19 +1196,6 @@ const channelsPublishHandler = factory.createHandlers(zParam(z.object({ channel:
1189
1196
  });
1190
1197
  //#endregion
1191
1198
  //#region lib/gateway/routes/debug.ts
1192
- const extractPreview = (payload) => {
1193
- if (typeof payload !== "string" || payload.length === 0) return null;
1194
- try {
1195
- const parsed = JSON.parse(payload);
1196
- if (parsed !== null && typeof parsed === "object" && "text" in parsed) {
1197
- const text = String(parsed.text);
1198
- return text.length > 80 ? `${text.slice(0, 80)}…` : text;
1199
- }
1200
- } catch {
1201
- return payload.length > 80 ? `${payload.slice(0, 80)}…` : payload;
1202
- }
1203
- return payload.length > 80 ? `${payload.slice(0, 80)}…` : payload;
1204
- };
1205
1199
  const buildChannelDiagnosis = (channel) => {
1206
1200
  const rootCause = (channel.connectionErrors[channel.connectionErrors.length - 1] ?? null)?.detail ?? null;
1207
1201
  if (channel.connectors.length === 0) return {
@@ -1250,11 +1244,6 @@ const debugHandler = factory.createHandlers(async (c) => {
1250
1244
  const gatewayListeners = deps.registry.list();
1251
1245
  const gatewayClients = deps.broadcaster.listChannels();
1252
1246
  const metrics = deps.broadcaster.getMetrics();
1253
- const tmpDir = funnelTmpDir();
1254
- const rawPath = join(tmpDir, "connector-raw.db");
1255
- const processedPath = join(tmpDir, "connector-processed.db");
1256
- const connectionPath = join(tmpDir, "connector-connection.db");
1257
- const hasStore = existsSync(rawPath) && existsSync(processedPath) && existsSync(connectionPath);
1258
1247
  const channels = targetChannels.map((ch) => {
1259
1248
  const listenerEntry = gatewayListeners.find((l) => l.channelName === ch.name) ?? null;
1260
1249
  const listener = listenerEntry ? {
@@ -1264,61 +1253,30 @@ const debugHandler = factory.createHandlers(async (c) => {
1264
1253
  lastEventAt: listenerEntry.lastEventAt
1265
1254
  } : null;
1266
1255
  const claudeClients = gatewayClients.filter((cl) => cl.channel === ch.id || cl.channel === ch.name).length;
1267
- const recentEvents = [];
1268
- const connectionErrors = [];
1269
- if (hasStore) {
1270
- const reader = new ConnectorDiagnosticSqlReader({
1271
- rawPath,
1272
- processedPath,
1273
- connectionPath
1274
- });
1275
- const rows = (() => {
1276
- try {
1277
- return reader.query("SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT 10", [ch.id]);
1278
- } finally {
1279
- reader.close();
1280
- }
1281
- })();
1282
- if (!(rows instanceof Error)) for (const row of [...rows].reverse()) {
1283
- const rawPayload = typeof row.payload === "string" ? row.payload : null;
1284
- let payloadParsed = null;
1285
- if (rawPayload) try {
1286
- const parsed = JSON.parse(rawPayload);
1287
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) payloadParsed = parsed;
1288
- } catch {
1289
- payloadParsed = null;
1290
- }
1291
- recentEvents.push({
1292
- seq: typeof row.seq === "number" ? row.seq : null,
1293
- ts: typeof row.ts === "number" ? row.ts : null,
1294
- type: typeof row.type === "string" ? row.type : "?",
1295
- outcome: typeof row.outcome === "string" ? row.outcome : "?",
1296
- payload: rawPayload,
1297
- payloadParsed,
1298
- preview: extractPreview(row.payload)
1299
- });
1256
+ const recentEvents = (() => {
1257
+ if (!deps.diagnosticLog) return [];
1258
+ try {
1259
+ return deps.diagnosticLog.queryProcessed({
1260
+ channelId: ch.id,
1261
+ limit: 10
1262
+ }).map(diagnosticEventOfProcessed);
1263
+ } catch {
1264
+ return [];
1300
1265
  }
1301
- if (listener && (!listener.alive || listener.errors > 0) || !listener) {
1302
- const errReader = new ConnectorDiagnosticSqlReader({
1303
- rawPath,
1304
- processedPath,
1305
- connectionPath
1306
- });
1307
- const errRows = (() => {
1308
- try {
1309
- return errReader.query("SELECT ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT 3", [ch.id]);
1310
- } finally {
1311
- errReader.close();
1312
- }
1313
- })();
1314
- if (!(errRows instanceof Error)) for (const row of [...errRows].reverse()) connectionErrors.push({
1315
- ts: typeof row.ts === "number" ? row.ts : null,
1316
- type: typeof row.type === "string" ? row.type : "?",
1317
- status: typeof row.status === "string" ? row.status : "?",
1318
- detail: typeof row.detail === "string" && row.detail.length > 0 ? row.detail : null
1319
- });
1266
+ })();
1267
+ const needsConnErrors = listener && (!listener.alive || listener.errors > 0) || !listener;
1268
+ const connectionErrors = (() => {
1269
+ if (!needsConnErrors || !deps.diagnosticLog) return [];
1270
+ try {
1271
+ return deps.diagnosticLog.queryConnection({
1272
+ channelId: ch.id,
1273
+ statuses: ["auth-failed", "error"],
1274
+ limit: 3
1275
+ }).map(diagnosticConnectionEventOf);
1276
+ } catch {
1277
+ return [];
1320
1278
  }
1321
- }
1279
+ })();
1322
1280
  const base = {
1323
1281
  id: ch.id,
1324
1282
  name: ch.name,
@@ -1468,6 +1426,7 @@ var FunnelGatewayServer = class {
1468
1426
  nowMs;
1469
1427
  extraRoutes;
1470
1428
  ownsEventLog;
1429
+ diagnosticLog;
1471
1430
  startedAt = null;
1472
1431
  server = null;
1473
1432
  disposed = false;
@@ -1489,6 +1448,7 @@ var FunnelGatewayServer = class {
1489
1448
  this.token = deps.token ?? "";
1490
1449
  this.allowInsecureHost = deps.allowInsecureHost ?? false;
1491
1450
  this.extraRoutes = deps.extraRoutes ?? null;
1451
+ this.diagnosticLog = deps.diagnosticLog;
1492
1452
  const clock = deps.clock;
1493
1453
  this.nowMs = clock ? () => clock.millis() : () => Date.now();
1494
1454
  if (deps.eventLog) {
@@ -1681,7 +1641,8 @@ var FunnelGatewayServer = class {
1681
1641
  registry: this.registry,
1682
1642
  channels: this.channels,
1683
1643
  uptimeMs: () => this.startedAt ? this.nowMs() - this.startedAt : 0,
1684
- emit: (input) => this.emit(input)
1644
+ emit: (input) => this.emit(input),
1645
+ diagnosticLog: this.diagnosticLog
1685
1646
  });
1686
1647
  return next();
1687
1648
  });
@@ -1946,7 +1907,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
1946
1907
  ...ageCap,
1947
1908
  ...rawMax !== void 0 ? { maxRows: rawMax } : {}
1948
1909
  };
1949
- this.raw = new FunnelLogSqliteSink({
1910
+ this.raw = new SqliteEventLog({
1950
1911
  path: props.rawPath,
1951
1912
  indexes: [
1952
1913
  "event_id",
@@ -1960,7 +1921,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
1960
1921
  }),
1961
1922
  ...rawCap
1962
1923
  });
1963
- this.processed = new FunnelLogSqliteSink({
1924
+ this.processed = new SqliteEventLog({
1964
1925
  path: props.processedPath,
1965
1926
  indexes: [
1966
1927
  "event_id",
@@ -1976,7 +1937,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
1976
1937
  }),
1977
1938
  ...verdictCap
1978
1939
  });
1979
- this.connection = new FunnelLogSqliteSink({
1940
+ this.connection = new SqliteEventLog({
1980
1941
  path: props.connectionPath,
1981
1942
  indexes: [
1982
1943
  "connector_id",
@@ -2042,10 +2003,13 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
2042
2003
  });
2043
2004
  }
2044
2005
  queryRaw(query) {
2006
+ const where = buildWhere(query);
2007
+ if (query.eventId !== void 0) where.event_id = query.eventId;
2045
2008
  return this.raw.query({
2046
2009
  ...query.type !== void 0 ? { type: query.type } : {},
2010
+ ...query.seq !== void 0 ? { seq: query.seq } : {},
2047
2011
  ...query.limit !== void 0 ? { limit: query.limit } : {},
2048
- where: buildWhere(query),
2012
+ where,
2049
2013
  order: "desc"
2050
2014
  }).map((record) => ({
2051
2015
  seq: record.seq,
@@ -2059,11 +2023,14 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
2059
2023
  }
2060
2024
  queryProcessed(query) {
2061
2025
  const where = buildWhere(query);
2026
+ if (query.eventId !== void 0) where.event_id = query.eventId;
2062
2027
  if (query.outcome !== void 0) where.outcome = query.outcome;
2063
2028
  return this.processed.query({
2064
2029
  ...query.type !== void 0 ? { type: query.type } : {},
2030
+ ...query.seq !== void 0 ? { seq: query.seq } : {},
2065
2031
  ...query.limit !== void 0 ? { limit: query.limit } : {},
2066
2032
  where,
2033
+ ...query.outcomePrefix !== void 0 ? { wherePrefix: { outcome: query.outcomePrefix } } : {},
2067
2034
  order: "desc"
2068
2035
  }).map((record) => ({
2069
2036
  seq: record.seq,
@@ -2081,8 +2048,10 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
2081
2048
  if (query.status !== void 0) where.status = query.status;
2082
2049
  return this.connection.query({
2083
2050
  ...query.type !== void 0 ? { type: query.type } : {},
2051
+ ...query.seq !== void 0 ? { seq: query.seq } : {},
2084
2052
  ...query.limit !== void 0 ? { limit: query.limit } : {},
2085
2053
  where,
2054
+ ...query.statuses !== void 0 ? { whereIn: { status: query.statuses } } : {},
2086
2055
  order: "desc"
2087
2056
  }).map((record) => ({
2088
2057
  seq: record.seq,
@@ -0,0 +1,284 @@
1
+ //#region lib/event-log/event-log-entry.d.ts
2
+ /**
3
+ * Wrapper that `EventLog.append` puts around every event before handing it
4
+ * to a sink. `seq` is monotonic across the lifetime of the underlying store —
5
+ * sinks persist it as the primary key so replay (and broadcaster seeding
6
+ * after restart) is an indexed range scan, not a full table walk. `ts` is
7
+ * epoch milliseconds. `event` is the caller-defined payload validated by the
8
+ * validation function passed to the log.
9
+ */
10
+ type EventLogEntry<E> = {
11
+ seq: number;
12
+ ts: number;
13
+ event: E;
14
+ };
15
+ //#endregion
16
+ //#region lib/event-log/event-log-store.d.ts
17
+ /**
18
+ * Relay sink. Receives records that already have a `seq` assigned by the
19
+ * primary and stores or forwards them — memory ring, stdout, network push,
20
+ * a second SQLite mirror, etc. Does not generate seq itself, so any number
21
+ * can be attached and they all observe the same monotonic stream.
22
+ *
23
+ * `write` returns `void` on success or an `Error` the `EventLog` surfaces via
24
+ * `onSinkError`. Throwing is also tolerated (`EventLog` catches it), but
25
+ * returning is preferred so the failure path is part of the type.
26
+ */
27
+ type EventLogRelay<E> = {
28
+ write(record: EventLogEntry<E>): void | Error;
29
+ close?(): void;
30
+ };
31
+ /**
32
+ * Primary sink. Owns the canonical seq sequence for the log. `insert` is
33
+ * the atomic boundary — it assigns a seq strictly greater than every
34
+ * previously assigned one, persists the record, and returns it. SQLite
35
+ * implementations get atomicity for free by delegating to `INTEGER PRIMARY
36
+ * KEY` so two processes sharing one database file see one monotonic
37
+ * stream without coordinating through `EventLog`.
38
+ *
39
+ * `getMaxSeq` is the highest seq currently in the sink — used for
40
+ * observability and for replay seeding by clients reading the store.
41
+ */
42
+ type EventLogStore<E> = {
43
+ insert(input: {
44
+ ts: number;
45
+ event: E;
46
+ }): EventLogEntry<E> | Error;
47
+ getMaxSeq(): number;
48
+ close?(): void;
49
+ };
50
+ //#endregion
51
+ //#region lib/event-log/event-log.d.ts
52
+ type Listener<E> = (record: EventLogEntry<E>) => void;
53
+ type SinkErrorHandler<E> = (error: Error, record: EventLogEntry<E>, sink: EventLogRelay<E>) => void;
54
+ type EventLogValidator<E> = (event: unknown) => {
55
+ success: true;
56
+ data: E;
57
+ } | {
58
+ success: false;
59
+ error: Error;
60
+ };
61
+ type EventLogProps<E> = {
62
+ /** Validates each event before emission. Use `schema.safeParse` from any validation library, or a plain function. */
63
+ validate: EventLogValidator<E>;
64
+ /** Owns seq assignment + durability. Use `SqliteEventLog` for multi-process safety. */
65
+ primary: EventLogStore<E>;
66
+ /** Optional fanout for already-sequenced records (memory ring, stdout, network mirror). */
67
+ relays?: ReadonlyArray<EventLogRelay<E>>;
68
+ /** Override for tests. Defaults to `Date.now`. */
69
+ now?: () => number;
70
+ /** Observer for relay failures. Default: silently swallow. */
71
+ onSinkError?: SinkErrorHandler<E>;
72
+ };
73
+ /**
74
+ * Validated event log. Three responsibilities and nothing else:
75
+ * validate the event, delegate seq + persistence to the primary sink, and
76
+ * fan the resulting record out to relays and live subscribers.
77
+ *
78
+ * Splitting "primary" from "relays" makes the seq invariant honest: there
79
+ * is exactly one source of truth (the primary's atomic insert). Two
80
+ * `EventLog` instances pointed at the same SQLite file therefore see
81
+ * one monotonic stream without coordinating with each other. Relays mirror
82
+ * already-sequenced records, so they can be added or removed without
83
+ * affecting correctness.
84
+ *
85
+ * Failure isolation:
86
+ * - Primary failure short-circuits append and is returned to the caller.
87
+ * - Relay failures never block the primary path — they surface via the
88
+ * optional `onSinkError` callback so the caller can observe without
89
+ * being interrupted.
90
+ * - A subscriber that throws is contained; the rest of the fanout
91
+ * completes normally.
92
+ */
93
+ declare class EventLog<E> {
94
+ private readonly validate;
95
+ private readonly primary;
96
+ private readonly relays;
97
+ private readonly now;
98
+ private readonly onSinkError;
99
+ private readonly listeners;
100
+ constructor(props: EventLogProps<E>);
101
+ append(event: E): EventLogEntry<E> | Error;
102
+ subscribe(listener: Listener<E>): () => void;
103
+ getMaxSeq(): number;
104
+ close(): void;
105
+ private callPrimary;
106
+ private fanOutToRelays;
107
+ private callRelay;
108
+ private fanOutToListeners;
109
+ private callClose;
110
+ }
111
+ //#endregion
112
+ //#region lib/event-log/memory-event-log.d.ts
113
+ type MemoryEventLogProps = {
114
+ /** Hard cap on retained records. The oldest is evicted on overflow. 0 disables retention. */
115
+ capacity?: number;
116
+ };
117
+ /**
118
+ * In-memory ring buffer that doubles as primary or relay. As primary it
119
+ * owns its own seq counter (single-process only — for multi-process
120
+ * safety, use `SqliteEventLog` as primary and place this as a
121
+ * relay). As relay it accepts whatever seq the primary assigned and
122
+ * advances its own counter to match, so `getMaxSeq` stays meaningful.
123
+ *
124
+ * Useful as a test double, as a short-window replay buffer paired with a
125
+ * persistent primary (covering reconnects without round-tripping disk),
126
+ * or as a backing store for live subscribers.
127
+ */
128
+ declare class MemoryEventLog<E> implements EventLogStore<E>, EventLogRelay<E> {
129
+ private readonly capacity;
130
+ private readonly buffer;
131
+ private seq;
132
+ constructor(props?: MemoryEventLogProps);
133
+ insert(input: {
134
+ ts: number;
135
+ event: E;
136
+ }): EventLogEntry<E>;
137
+ write(record: EventLogEntry<E>): void;
138
+ getMaxSeq(): number;
139
+ query(): ReadonlyArray<EventLogEntry<E>>;
140
+ clear(): void;
141
+ private append;
142
+ }
143
+ //#endregion
144
+ //#region lib/event-log/sqlite-event-log.d.ts
145
+ type IndexValues<I extends ReadonlyArray<string>> = Record<I[number], string | null>;
146
+ type IndexPrefixes<I extends ReadonlyArray<string>> = Partial<Record<I[number], string>>;
147
+ type IndexSets<I extends ReadonlyArray<string>> = Partial<Record<I[number], ReadonlyArray<string | null>>>;
148
+ /**
149
+ * Constructor props. The shape narrows on `I`: when no indexes are
150
+ * declared (the default), `extractIndexes` is forbidden; when indexes
151
+ * are declared, both `indexes` and `extractIndexes` are required and
152
+ * `extractIndexes` is type-checked against the index keys.
153
+ */
154
+ type SqliteEventLogProps<E, I extends ReadonlyArray<string>> = I extends readonly [] ? {
155
+ path: string;
156
+ maxRows?: number;
157
+ maxAgeMs?: number;
158
+ maxBytes?: number;
159
+ targetBytes?: number;
160
+ now?: () => number;
161
+ indexes?: I;
162
+ extractIndexes?: never;
163
+ } : {
164
+ path: string;
165
+ maxRows?: number;
166
+ maxAgeMs?: number;
167
+ maxBytes?: number;
168
+ targetBytes?: number;
169
+ now?: () => number;
170
+ indexes: I;
171
+ extractIndexes: (event: E) => IndexValues<I>;
172
+ };
173
+ type SqliteEventLogQuery<I extends ReadonlyArray<string>> = {
174
+ /** Return only records with seq strictly greater than this. */
175
+ sinceSeq?: number;
176
+ /** Match one sequence number exactly instead of using `sinceSeq`. */
177
+ seq?: number;
178
+ /** Filter by the top-level `event.type` discriminator. */
179
+ type?: string;
180
+ /** Filter by indexed columns. Keys are constrained to the declared `indexes`. */
181
+ where?: Partial<IndexValues<I>>;
182
+ /** Prefix-match indexed text columns. */
183
+ wherePrefix?: IndexPrefixes<I>;
184
+ /** Match any value in a set for indexed columns. */
185
+ whereIn?: IndexSets<I>;
186
+ /** Maximum rows returned. Default 1000. */
187
+ limit?: number;
188
+ /**
189
+ * Which end of the seq range to take when `limit` clips the result.
190
+ * "asc" (default) returns the oldest matching rows; "desc" returns the
191
+ * newest. Rows are always sorted ascending by seq before returning, so the
192
+ * caller sees a chronological slice either way — "desc" just picks the tail.
193
+ */
194
+ order?: "asc" | "desc";
195
+ };
196
+ /**
197
+ * SQLite-backed sink built on `bun:sqlite`. Implements both primary and
198
+ * relay roles so the same instance can own seq generation for one log and
199
+ * mirror records from another (e.g. cross-process replication, restore
200
+ * from a backup stream).
201
+ *
202
+ * Concurrency model: seq is `INTEGER PRIMARY KEY`, so SQLite assigns it
203
+ * atomically via `lastInsertRowid`. Two `EventLog` instances pointed
204
+ * at the same database file therefore see one monotonically increasing
205
+ * seq stream without coordinating through `EventLog` — the database itself is
206
+ * the synchronization point.
207
+ *
208
+ * Schema is version-managed via `PRAGMA user_version`. Migrations are
209
+ * append-only and run in a transaction on every construct so a partial
210
+ * upgrade rolls back cleanly. Caller-defined `indexes` are layered on top
211
+ * via `ALTER TABLE ADD COLUMN` + `CREATE INDEX IF NOT EXISTS`, so adding
212
+ * a new index to an existing database is a no-downtime operation.
213
+ *
214
+ * Type safety: the second generic parameter `I` is the literal tuple of
215
+ * index column names. `extractIndexes` and `query({ where })` are
216
+ * both type-checked against this tuple, so a typo at the call site is a
217
+ * compile-time error rather than a silent miss at runtime.
218
+ *
219
+ * Retention is bounded by `maxRows` and/or `maxAgeMs`. Both run on every
220
+ * insert as a single indexed DELETE that no-ops below the cap.
221
+ *
222
+ * Bulk inserts use `insertMany`, which wraps the batch in one transaction
223
+ * for ~10–100x throughput at the cost of one fsync per batch instead of
224
+ * one per row.
225
+ */
226
+ declare class SqliteEventLog<E, const I extends ReadonlyArray<string> = readonly []> implements EventLogStore<E>, EventLogRelay<E> {
227
+ private readonly db;
228
+ private readonly maxRows;
229
+ private readonly maxAgeMs;
230
+ private readonly maxBytes;
231
+ private readonly targetBytes;
232
+ private readonly now;
233
+ private readonly indexes;
234
+ private readonly extractIndexes;
235
+ private readonly insertStmt;
236
+ private readonly insertWithSeqStmt;
237
+ private readonly maxSeqStmt;
238
+ private readonly countStmt;
239
+ private readonly trimRowsStmt;
240
+ private readonly trimAgeStmt;
241
+ private readonly trimOldestStmt;
242
+ private insertsSinceByteCheck;
243
+ constructor(props: SqliteEventLogProps<E, I>);
244
+ insert(input: {
245
+ ts: number;
246
+ event: E;
247
+ }): EventLogEntry<E> | Error;
248
+ insertMany(inputs: ReadonlyArray<{
249
+ ts: number;
250
+ event: E;
251
+ }>): EventLogEntry<E>[] | Error;
252
+ write(record: EventLogEntry<E>): void | Error;
253
+ getMaxSeq(): number;
254
+ query(props?: SqliteEventLogQuery<I>): EventLogEntry<E>[];
255
+ /**
256
+ * Current schema version. Useful for diagnostics and for tests that want
257
+ * to verify migrations ran. Reads `PRAGMA user_version` once per call.
258
+ */
259
+ getSchemaVersion(): number;
260
+ close(): void;
261
+ private buildInsertParams;
262
+ private appendWhereConditions;
263
+ private appendPrefixConditions;
264
+ private appendSetConditions;
265
+ private trim;
266
+ private indexKeys;
267
+ /**
268
+ * Throttled byte-size enforcement. Only every BYTE_CHECK_INTERVAL inserts do
269
+ * we measure the file; on overflow we estimate how many of the oldest rows to
270
+ * drop to land near targetBytes (by the byte/row ratio), delete them in one
271
+ * statement, then VACUUM once to return the freed pages to the filesystem (a
272
+ * plain DELETE only frees pages inside the file). One DELETE + one VACUUM per
273
+ * overflow keeps the expensive rewrite rare — the file must refill the whole
274
+ * maxBytes→targetBytes delta before the next overflow can trigger.
275
+ */
276
+ private maybeTrimBytes;
277
+ private byteSize;
278
+ /** Drop every row and reclaim the file space. Used by `<log>.clear()`. */
279
+ clear(): void;
280
+ private syncIndexColumns;
281
+ private migrate;
282
+ }
283
+ //#endregion
284
+ export { MemoryEventLogProps as a, EventLogValidator as c, EventLogEntry as d, MemoryEventLog as i, EventLogRelay as l, SqliteEventLogProps as n, EventLog as o, SqliteEventLogQuery as r, EventLogProps as s, SqliteEventLog as t, EventLogStore as u };