@interactive-inc/claude-funnel 0.60.1 → 0.64.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 (88) hide show
  1. package/README.md +2 -2
  2. package/dist/bin.js +428 -761
  3. package/dist/{channels-2g_BU1N0.d.ts → channels-CRGb6B5_.d.ts} +17 -16
  4. package/dist/claude.d.ts +5 -7
  5. package/dist/claude.js +143 -36
  6. package/dist/{connector-descriptor-6SXJoszo.d.ts → connector-descriptor-BFIhyTfa.d.ts} +49 -10
  7. package/dist/connector-diagnostics-recorder-COtNEmUp.js +42 -0
  8. package/dist/connectors/discord.d.ts +31 -37
  9. package/dist/connectors/discord.js +3 -3
  10. package/dist/connectors/gh.d.ts +37 -33
  11. package/dist/connectors/gh.js +3 -3
  12. package/dist/connectors/schedule.d.ts +9 -57
  13. package/dist/connectors/schedule.js +3 -3
  14. package/dist/connectors/slack.d.ts +106 -132
  15. package/dist/connectors/slack.js +4 -3
  16. package/dist/diagnostics.d.ts +1 -1
  17. package/dist/diagnostics.js +1 -1
  18. package/dist/discord-connector-DIFkYBbi.js +250 -0
  19. package/dist/discord-connector-schema-D-bOVAKt.d.ts +22 -0
  20. package/dist/docs.js +1 -1
  21. package/dist/doctor.d.ts +1 -1
  22. package/dist/doctor.js +1 -1
  23. package/dist/{file-process-guard-C_PLxfUX.d.ts → file-process-guard-tVcgckH6.d.ts} +6 -6
  24. package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
  25. package/dist/flume-source-listener-BNyAII7N.d.ts +133 -0
  26. package/dist/{funnel-diagnostics-CSiJmPlZ.js → funnel-diagnostics-Cvk6Sk4x.js} +193 -43
  27. package/dist/{funnel-diagnostics-DpXOsCty.d.ts → funnel-diagnostics-b9ar0Ing.d.ts} +67 -5
  28. package/dist/{funnel-docs-BxXZ9Ksx.js → funnel-docs-C-ge0MuB.js} +42 -6
  29. package/dist/{funnel-doctor-CZf_0Luq.d.ts → funnel-doctor-CnRQi4kM.d.ts} +2 -2
  30. package/dist/{funnel-doctor-DiJCjHsg.js → funnel-doctor-XrI2GBH8.js} +1 -1
  31. package/dist/funnel-error-0t1MK1R6.js +75 -0
  32. package/dist/{funnel-recovery-DnLrdWO9.d.ts → funnel-recovery-CMhY8Jfk.d.ts} +1 -1
  33. package/dist/gateway/daemon.js +167 -527
  34. package/dist/gateway.d.ts +3 -3
  35. package/dist/gateway.js +3 -3
  36. package/dist/gh-connector-BUGCOEWS.js +187 -0
  37. package/dist/{gh-connector-schema-Rzwc1c1N.js → gh-connector-schema-CAqIhzGr.js} +7 -0
  38. package/dist/gh-connector-schema-DWQaB6gX.d.ts +16 -0
  39. package/dist/{index-CgY8NdMz.d.ts → index-Ds6sHhA-.d.ts} +37 -19
  40. package/dist/index.d.ts +182 -22
  41. package/dist/index.js +363 -173
  42. package/dist/{local-config-json-schema-JyLqOQNX.js → local-config-json-schema-DexV8vX3.js} +24 -4
  43. package/dist/local-config.d.ts +39 -2
  44. package/dist/local-config.js +53 -2
  45. package/dist/logger.js +1 -1
  46. package/dist/loopback-fetch-CVNuN3YZ.js +40 -0
  47. package/dist/{local-config-sync-Dh1Croqe.d.ts → memory-token-prompter-BoV8Hf-n.d.ts} +30 -3
  48. package/dist/node-file-system-BOXIHW_Q.js +174 -0
  49. package/dist/{profiles-DSzTeKQw.js → profiles-ZHLONml4.js} +49 -49
  50. package/dist/{profiles-Cy5wXQ0L.d.ts → profiles-cVZQkM69.d.ts} +3 -3
  51. package/dist/profiles.d.ts +1 -1
  52. package/dist/profiles.js +1 -1
  53. package/dist/recovery.d.ts +1 -1
  54. package/dist/recovery.js +1 -1
  55. package/dist/resolve-connector-token-DxDG9mhf.js +22 -0
  56. package/dist/{schedule-connector-L4uzg5M8.js → schedule-connector-9k3gOIgl.js} +54 -55
  57. package/dist/schedule-connector-schema-Z0RXLgPI.d.ts +49 -0
  58. package/dist/settings-reader-BNxjsxCB.d.ts +27 -0
  59. package/dist/{settings-store-CUKSeTXC.js → settings-store-C2QdOH-t.js} +23 -4
  60. package/dist/slack-connector-CxpWagbT.js +388 -0
  61. package/dist/slack-event-processor-BhCf5Wiy.d.ts +95 -0
  62. package/dist/slack-event-processor-xFDG3US0.js +176 -0
  63. package/dist/slot-fields-D-pvMgTK.js +249 -0
  64. package/dist/{memory-diagnostic-log-CI60kNfB.js → sqlite-diagnostic-log-DOTPW-tG.js} +373 -249
  65. package/dist/{yaml-render-93pX7EF7.js → yaml-render--J1_3BSA.js} +25 -21
  66. package/package.json +2 -4
  67. package/dist/discord-connector-BL36yvbL.js +0 -250
  68. package/dist/gateway-base-url-Dy4Ykuoh.js +0 -14
  69. package/dist/gh-connector-DpiixfQZ.js +0 -226
  70. package/dist/http-client-oICicjuO.d.ts +0 -18
  71. package/dist/memory-token-prompter-B4sjyaAq.d.ts +0 -57
  72. package/dist/memory-token-prompter-CZde7e6y.js +0 -61
  73. package/dist/node-file-system-Blr8pAir.js +0 -48
  74. package/dist/settings-reader-BIFB_j2f.d.ts +0 -18
  75. package/dist/slack-connector-DQIFPdBF.js +0 -484
  76. package/dist/slot-fields-CMoRpwuy.js +0 -45
  77. /package/dist/{connector-adapter-DU9Rvyec.js → connector-adapter-Dvs8N7ew.js} +0 -0
  78. /package/dist/{connector-listener-DR3aKOuK.js → connector-listener-mPGZYa8e.js} +0 -0
  79. /package/dist/{diagnostic-sql-reader-C9zR-Csp.js → diagnostic-sql-reader-oXZnWFf_.js} +0 -0
  80. /package/dist/{discord-connector-schema-B_N6IXLz.js → discord-connector-schema-B4YpWpR3.js} +0 -0
  81. /package/dist/{error-message-of-Byi4y0Uf.js → error-message-of-ColuYmAk.js} +0 -0
  82. /package/dist/{funnel-log-sqlite-sink-kqJbx2H7.js → funnel-log-sqlite-sink-DLYkY0pZ.js} +0 -0
  83. /package/dist/{funnel-recovery-BFdPjL6Z.js → funnel-recovery-DKnEutUS.js} +0 -0
  84. /package/dist/{node-http-client-lowp60Oa.js → node-http-client-u00atiKx.js} +0 -0
  85. /package/dist/{schedule-connector-schema-CfyuMCMh.js → schedule-connector-schema-DKEPZnVv.js} +0 -0
  86. /package/dist/{settings-reader-CtQ-Ix8_.js → settings-reader-9FcX3qS1.js} +0 -0
  87. /package/dist/{settings-schema-D1xcOqRu.d.ts → settings-schema-BL_c2Udm.d.ts} +0 -0
  88. /package/dist/{slack-connector-schema-C1zEf4TG.js → slack-connector-schema-Dem8to4P.js} +0 -0
@@ -1,9 +1,10 @@
1
- import { t as gatewayLoopbackUrl } from "./gateway-base-url-Dy4Ykuoh.js";
2
- import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
1
+ import { n as gatewayLoopbackUrl, t as loopbackFetch } from "./loopback-fetch-CVNuN3YZ.js";
2
+ import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
3
3
  import { t as NodeFunnelProcessRunner } from "./node-process-runner-DxTvycoK.js";
4
- import { n as FUNNEL_DIR, o as resolveFunnelPort } from "./settings-store-CUKSeTXC.js";
5
- import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-C9zR-Csp.js";
6
- import { t as FunnelLogSqliteSink } from "./funnel-log-sqlite-sink-kqJbx2H7.js";
4
+ import { n as FUNNEL_DIR, o as resolveFunnelPort } from "./settings-store-C2QdOH-t.js";
5
+ import { t as FunnelAuthFailedError } from "./funnel-error-0t1MK1R6.js";
6
+ import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-oXZnWFf_.js";
7
+ import { t as FunnelLogSqliteSink } from "./funnel-log-sqlite-sink-DLYkY0pZ.js";
7
8
  import { dirname, join } from "node:path";
8
9
  import { chmodSync, existsSync, mkdirSync } from "node:fs";
9
10
  import { homedir, tmpdir } from "node:os";
@@ -24,6 +25,174 @@ function funnelTmpDir() {
24
25
  return join(tmpdir(), "funnel");
25
26
  }
26
27
  //#endregion
28
+ //#region lib/engine/diagnostic-log/diagnostic-log.ts
29
+ /**
30
+ * Points in the listener's connection lifecycle. The single source of truth
31
+ * for the value set: the `status` column schema, the `ConnectorConnectionStatus`
32
+ * union, and the runtime Set used to narrow on read-back all derive from this
33
+ * array, so adding a status is a one-line change that cannot drift out of sync.
34
+ *
35
+ * started start() was called
36
+ * connected the socket opened and events can flow
37
+ * disconnected the socket was closed by a stop() call (a clean teardown)
38
+ * auth-failed the token was rejected before the socket opened
39
+ * stopped the listener was fully torn down (always follows a stop(),
40
+ * paired with the disconnected/error that preceded it)
41
+ * error start/stop threw, or Bolt surfaced an error frame — this is
42
+ * also where an unsolicited socket drop shows up when Bolt
43
+ * reports it (an `error` with no following `stopped` means the
44
+ * supervisor recycled the listener, not a clean stop)
45
+ *
46
+ * A connection row is independent of any single inbound event, so it carries
47
+ * no `eventId`. This is how "no notification arrived because the listener
48
+ * never connected (or dropped, or failed auth)" becomes visible: the
49
+ * raw/processed tables only hold events that *did* arrive.
50
+ */
51
+ const CONNECTOR_CONNECTION_STATUSES = [
52
+ "started",
53
+ "connected",
54
+ "disconnected",
55
+ "auth-failed",
56
+ "stopped",
57
+ "error"
58
+ ];
59
+ /**
60
+ * Rows stored in the diagnostic tables. Connector-agnostic on purpose: `type`
61
+ * carries the listener kind ("slack" | "discord" | "gh" | "schedule") so new
62
+ * connectors land in the same tables without a schema change. `event_id` is
63
+ * the correlation key the listener mints once per inbound event and stamps
64
+ * onto both the raw and processed rows, so the two are joinable even though
65
+ * they live in separate tables with independent `seq` counters.
66
+ *
67
+ * These schemas mirror the stored shape (snake_case columns) the way
68
+ * `FunnelEvent` does for the replay log; they exist for `z.infer` and to
69
+ * document the column set, not as a parse boundary.
70
+ */
71
+ const connectorRawEventSchema = z.object({
72
+ event_id: z.string(),
73
+ type: z.string(),
74
+ connector_id: z.string().nullable(),
75
+ channel_id: z.string().nullable(),
76
+ payload: z.string()
77
+ });
78
+ const connectorProcessedEventSchema = z.object({
79
+ event_id: z.string(),
80
+ type: z.string(),
81
+ connector_id: z.string().nullable(),
82
+ channel_id: z.string().nullable(),
83
+ outcome: z.string(),
84
+ payload: z.string()
85
+ });
86
+ const connectorConnectionEventSchema = z.object({
87
+ type: z.string(),
88
+ connector_id: z.string().nullable(),
89
+ channel_id: z.string().nullable(),
90
+ status: z.enum(CONNECTOR_CONNECTION_STATUSES),
91
+ detail: z.string()
92
+ });
93
+ /**
94
+ * Three-table diagnostic log of everything a connector listener does, so
95
+ * "why was there no notification?" is answerable whichever way it failed:
96
+ * - `raw` — every inbound event, before any filtering, with the listener's
97
+ * untouched payload (the Slack Bolt event, the GH webhook, …)
98
+ * - `processed` — the verdict for that event: `outcome` (emitted, or the
99
+ * reason it was dropped) and, when emitted, the body that was delivered.
100
+ * Shares an `eventId` with its raw row, so the two join into one story.
101
+ * - `connection` — the listener's lifecycle (started, connected, dropped,
102
+ * auth-failed, stopped, errored). This is the half the event tables can't
103
+ * show: an event that never arrived leaves no raw row, but a listener that
104
+ * never connected leaves a `connection` trail that says so.
105
+ *
106
+ * The three are physically separate (independent retention and payload-size
107
+ * policy) so a query never crosses them by accident and a huge raw payload
108
+ * never bloats the verdict or lifecycle trails. None flow to WS clients or the
109
+ * MCP channel — this is a separate store from `FunnelEventLog` (replay) and
110
+ * exists solely for debugging.
111
+ *
112
+ * Implementations:
113
+ * - `SqliteConnectorDiagnosticLog` — the default; survives daemon restarts,
114
+ * bounded by per-table row/age caps.
115
+ * - `MemoryConnectorDiagnosticLog` — an in-process double for tests.
116
+ */
117
+ var ConnectorDiagnosticLog = class {};
118
+ //#endregion
119
+ //#region lib/engine/diagnostic-log/memory-diagnostic-log.ts
120
+ /**
121
+ * In-process `ConnectorDiagnosticLog` backed by one array per table. Used by tests
122
+ * and embedders that do not need durability. Like the SQLite log it keeps
123
+ * `seq` per-table (each array's 1-based position) and returns the most recent
124
+ * `limit` rows oldest-first; unlike it, it never prunes and never offloads
125
+ * oversized payloads — it keeps whatever the caller hands it, which is fine
126
+ * for the bounded volumes a test produces. Payload-validity is therefore a
127
+ * SQLite-only guarantee; do not write a test that leans on this double
128
+ * rejecting a malformed payload.
129
+ */
130
+ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
131
+ raws = [];
132
+ processeds = [];
133
+ connections = [];
134
+ constructor(now = () => Date.now()) {
135
+ super();
136
+ this.now = now;
137
+ Object.freeze(this);
138
+ }
139
+ recordRaw(record) {
140
+ this.raws.push({
141
+ ...record,
142
+ seq: this.raws.length + 1,
143
+ ts: this.now()
144
+ });
145
+ }
146
+ recordProcessed(record) {
147
+ this.processeds.push({
148
+ ...record,
149
+ seq: this.processeds.length + 1,
150
+ ts: this.now()
151
+ });
152
+ }
153
+ recordConnection(record) {
154
+ this.connections.push({
155
+ ...record,
156
+ seq: this.connections.length + 1,
157
+ ts: this.now()
158
+ });
159
+ }
160
+ queryRaw(query) {
161
+ return takeRecent(this.raws.filter((event) => matches(event, query)), query.limit);
162
+ }
163
+ queryProcessed(query) {
164
+ return takeRecent(this.processeds.filter((event) => {
165
+ if (!matches(event, query)) return false;
166
+ if (query.outcome !== void 0 && event.outcome !== query.outcome) return false;
167
+ return true;
168
+ }), query.limit);
169
+ }
170
+ queryConnection(query) {
171
+ return takeRecent(this.connections.filter((event) => {
172
+ if (!matches(event, query)) return false;
173
+ if (query.status !== void 0 && event.status !== query.status) return false;
174
+ return true;
175
+ }), query.limit);
176
+ }
177
+ clear() {
178
+ this.raws.length = 0;
179
+ this.processeds.length = 0;
180
+ this.connections.length = 0;
181
+ }
182
+ close() {}
183
+ };
184
+ const matches = (event, query) => {
185
+ if (query.type !== void 0 && event.type !== query.type) return false;
186
+ if (query.connectorId !== void 0 && event.connectorId !== query.connectorId) return false;
187
+ if (query.channelId !== void 0 && event.channelId !== query.channelId) return false;
188
+ return true;
189
+ };
190
+ const takeRecent = (events, limit) => {
191
+ if (limit === void 0) return events;
192
+ if (limit <= 0) return [];
193
+ return events.slice(-limit);
194
+ };
195
+ //#endregion
27
196
  //#region lib/gateway/publish-schema.ts
28
197
  /**
29
198
  * Shared schema for `POST /channels/:channel/publish` — used by both the
@@ -67,8 +236,7 @@ var FunnelChannelPublisher = class {
67
236
  async publish(channelName, request) {
68
237
  if (!this.isDaemonRunning()) return OFFLINE;
69
238
  try {
70
- const url = `${gatewayLoopbackUrl(this.port)}/channels/${encodeURIComponent(channelName)}/publish`;
71
- const res = await fetch(url, {
239
+ const res = await loopbackFetch(`${gatewayLoopbackUrl(this.port)}/channels/${encodeURIComponent(channelName)}/publish`, {
72
240
  method: "POST",
73
241
  headers: {
74
242
  ...this.authHeaders(),
@@ -308,19 +476,35 @@ var FunnelBroadcaster = class {
308
476
  this.droppedSlowClients += 1;
309
477
  continue;
310
478
  }
311
- ws.send(payload);
479
+ try {
480
+ ws.send(payload);
481
+ } catch (error) {
482
+ const err = error instanceof Error ? error : new Error(String(error));
483
+ const data = this.clients.get(ws);
484
+ this.logger?.warn("ws.send failed; dropping client", {
485
+ channel: data?.channel,
486
+ error: err.message
487
+ });
488
+ this.clients.delete(ws);
489
+ }
312
490
  }
313
- for (const handler of this.subscribers) try {
314
- handler(event);
315
- } catch (error) {
316
- const err = error instanceof Error ? error : new Error(String(error));
317
- this.logger?.error("broadcast subscriber threw", { error: err.message });
318
- this.onError(err, {
319
- component: "broadcaster.subscriber",
320
- offset: event.offset,
321
- connector: event.meta?.connector ?? null,
322
- channel: event.meta?.channel ?? null
323
- });
491
+ for (const handler of this.subscribers) {
492
+ const captureError = (error) => {
493
+ const err = error instanceof Error ? error : new Error(String(error));
494
+ this.logger?.error("broadcast subscriber threw", { error: err.message });
495
+ this.onError(err, {
496
+ component: "broadcaster.subscriber",
497
+ offset: event.offset,
498
+ connector: event.meta?.connector ?? null,
499
+ channel: event.meta?.channel ?? null
500
+ });
501
+ };
502
+ try {
503
+ const result = handler(event);
504
+ if (result instanceof Promise) result.catch(captureError);
505
+ } catch (error) {
506
+ captureError(error);
507
+ }
324
508
  }
325
509
  return event;
326
510
  }
@@ -384,10 +568,12 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
384
568
  sink;
385
569
  now;
386
570
  logger;
571
+ onError;
387
572
  constructor(props) {
388
573
  super();
389
574
  this.now = props.now ?? (() => Date.now());
390
575
  this.logger = props.logger;
576
+ this.onError = props.onError;
391
577
  this.sink = new FunnelLogSqliteSink({
392
578
  path: props.path,
393
579
  indexes: ["channel_id", "connector_id"],
@@ -420,10 +606,19 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
420
606
  ts: this.now(),
421
607
  event
422
608
  });
423
- if (result instanceof Error) this.logger?.error("event log write failed", {
424
- offset: record.offset,
425
- error: result.message
426
- });
609
+ if (result instanceof Error) {
610
+ this.logger?.error("event log write failed", {
611
+ offset: record.offset,
612
+ error: result.message
613
+ });
614
+ this.onError?.(result, {
615
+ component: "sqlite-event-log",
616
+ op: "record",
617
+ offset: record.offset,
618
+ channelId: record.channelId,
619
+ connectorId: record.connectorId
620
+ });
621
+ }
427
622
  }
428
623
  /**
429
624
  * Returns events with offset > since. Filtering by channel/connector is
@@ -476,7 +671,7 @@ function truncate(content) {
476
671
  return `${content.slice(0, MAX_CONTENT_CHARS)}...`;
477
672
  }
478
673
  //#endregion
479
- //#region lib/gateway/listener-supervisor.ts
674
+ //#region lib/gateway/listener-registry.ts
480
675
  const defaultOnError$1 = () => {};
481
676
  const DEFAULT_HEALTH_INTERVAL_MS = 3e4;
482
677
  const DEFAULT_MAX_BACKOFF_MS = 6e4;
@@ -496,13 +691,14 @@ const defaultSleep$1 = (ms) => new Promise((r) => {
496
691
  * dead listeners with exponential backoff (1s, 2s, 4s, ... capped). Resets
497
692
  * the backoff counter on successful restart.
498
693
  */
499
- var FunnelListenerSupervisor = class FunnelListenerSupervisor {
694
+ var FunnelListenerRegistry = class FunnelListenerRegistry {
500
695
  channels;
501
696
  notify;
502
697
  logger;
503
698
  onError;
504
699
  running = /* @__PURE__ */ new Map();
505
700
  failureCounts = /* @__PURE__ */ new Map();
701
+ starting = /* @__PURE__ */ new Set();
506
702
  stats = /* @__PURE__ */ new Map();
507
703
  healthCheckIntervalMs;
508
704
  maxBackoffMs;
@@ -528,7 +724,7 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
528
724
  return `${channelName}/${connectorName}`;
529
725
  }
530
726
  isRunning(channelName, connectorName) {
531
- return this.running.has(FunnelListenerSupervisor.keyOf(channelName, connectorName));
727
+ return this.running.has(FunnelListenerRegistry.keyOf(channelName, connectorName));
532
728
  }
533
729
  list() {
534
730
  return [...this.running.entries()].map(([key, entry]) => {
@@ -547,11 +743,24 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
547
743
  });
548
744
  }
549
745
  async start(channelName, connectorName) {
550
- const key = FunnelListenerSupervisor.keyOf(channelName, connectorName);
746
+ const key = FunnelListenerRegistry.keyOf(channelName, connectorName);
551
747
  if (this.running.has(key)) return {
552
748
  ok: true,
553
749
  reason: "already running"
554
750
  };
751
+ if (this.starting.has(key)) return {
752
+ ok: false,
753
+ reason: "already starting",
754
+ retriable: true
755
+ };
756
+ this.starting.add(key);
757
+ try {
758
+ return await this.startLocked(channelName, connectorName, key);
759
+ } finally {
760
+ this.starting.delete(key);
761
+ }
762
+ }
763
+ async startLocked(channelName, connectorName, key) {
555
764
  const created = this.channels.createListener(channelName, connectorName);
556
765
  if (!created) return {
557
766
  ok: false,
@@ -585,25 +794,30 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
585
794
  return { ok: true };
586
795
  } catch (error) {
587
796
  const err = error instanceof Error ? error : new Error(String(error));
797
+ try {
798
+ await created.listener.stop();
799
+ } catch {}
588
800
  this.logger?.error(`${created.config.type} listener failed to start`, {
589
801
  channel: channelName,
590
802
  connector: connectorName,
591
803
  error: err.message
592
804
  });
593
805
  this.onError(err, {
594
- component: "listener-supervisor.start",
806
+ component: "listener-registry.start",
595
807
  channel: channelName,
596
808
  connector: connectorName,
597
809
  type: created.config.type
598
810
  });
811
+ const retriable = !(err instanceof FunnelAuthFailedError);
599
812
  return {
600
813
  ok: false,
601
- reason: err.message
814
+ reason: err.message,
815
+ retriable
602
816
  };
603
817
  }
604
818
  }
605
819
  async stop(channelName, connectorName) {
606
- const key = FunnelListenerSupervisor.keyOf(channelName, connectorName);
820
+ const key = FunnelListenerRegistry.keyOf(channelName, connectorName);
607
821
  const entry = this.running.get(key);
608
822
  if (!entry) return {
609
823
  ok: true,
@@ -624,7 +838,7 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
624
838
  error: err.message
625
839
  });
626
840
  this.onError(err, {
627
- component: "listener-supervisor.stop",
841
+ component: "listener-registry.stop",
628
842
  channel: channelName,
629
843
  connector: connectorName,
630
844
  type: entry.config.type
@@ -649,8 +863,17 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
649
863
  for (let i = 0; i < results.length; i++) {
650
864
  const result = results[i];
651
865
  const view = all[i];
652
- if (result.status === "rejected" || result.status === "fulfilled" && !result.value.ok) {
653
- const key = FunnelListenerSupervisor.keyOf(view.channelName, view.name);
866
+ if (result.status === "rejected") {
867
+ const key = FunnelListenerRegistry.keyOf(view.channelName, view.name);
868
+ this.pendingRetry.set(key, {
869
+ channelName: view.channelName,
870
+ connectorName: view.name
871
+ });
872
+ continue;
873
+ }
874
+ if (result.status === "fulfilled" && !result.value.ok) {
875
+ if (result.value.retriable === false) continue;
876
+ const key = FunnelListenerRegistry.keyOf(view.channelName, view.name);
654
877
  this.pendingRetry.set(key, {
655
878
  channelName: view.channelName,
656
879
  connectorName: view.name
@@ -687,7 +910,11 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
687
910
  startHealthCheck() {
688
911
  if (this.healthCheckTimer) return;
689
912
  this.healthCheckTimer = setInterval(() => {
690
- this.runHealthCheck();
913
+ this.runHealthCheck().catch((error) => {
914
+ const err = error instanceof Error ? error : new Error(String(error));
915
+ this.logger?.error("health check pass failed", { error: err.message });
916
+ this.onError(err, { component: "listener-registry.health-check" });
917
+ });
691
918
  }, this.healthCheckIntervalMs);
692
919
  this.healthCheckTimer.unref();
693
920
  }
@@ -704,36 +931,64 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
704
931
  if (this.healthCheckInFlight) return;
705
932
  this.healthCheckInFlight = true;
706
933
  try {
934
+ const dead = [];
707
935
  for (const [key, entry] of [...this.running.entries()]) {
708
936
  if (entry.listener.isAlive()) {
709
937
  this.failureCounts.delete(key);
710
938
  continue;
711
939
  }
712
- await this.recoverDead(entry.channelName, entry.config.name, entry.config.type);
940
+ dead.push({
941
+ channelName: entry.channelName,
942
+ connectorName: entry.config.name,
943
+ type: entry.config.type
944
+ });
713
945
  }
946
+ await Promise.all(dead.map((target) => this.recoverDead(target.channelName, target.connectorName, target.type)));
947
+ const retries = [];
714
948
  for (const [key, pending] of [...this.pendingRetry.entries()]) {
715
949
  if (this.running.has(key)) {
716
950
  this.pendingRetry.delete(key);
717
951
  continue;
718
952
  }
719
- this.logger?.info("retrying failed listener", {
720
- channel: pending.channelName,
721
- connector: pending.connectorName
953
+ retries.push({
954
+ key,
955
+ channelName: pending.channelName,
956
+ connectorName: pending.connectorName
722
957
  });
723
- const failureCount = this.failureCounts.get(key) ?? 0;
724
- const backoffMs = Math.min(1e3 * 2 ** failureCount, this.maxBackoffMs);
725
- await this.sleep(backoffMs);
726
- if ((await this.start(pending.channelName, pending.connectorName)).ok) {
727
- this.pendingRetry.delete(key);
728
- this.failureCounts.delete(key);
729
- } else this.failureCounts.set(key, failureCount + 1);
730
958
  }
959
+ await Promise.all(retries.map((retry) => this.attemptRetry(retry)));
731
960
  } finally {
732
961
  this.healthCheckInFlight = false;
733
962
  }
734
963
  }
964
+ async attemptRetry(retry) {
965
+ this.logger?.info("retrying failed listener", {
966
+ channel: retry.channelName,
967
+ connector: retry.connectorName
968
+ });
969
+ const failureCount = this.failureCounts.get(retry.key) ?? 0;
970
+ const backoffMs = Math.min(1e3 * 2 ** failureCount, this.maxBackoffMs);
971
+ await this.sleep(backoffMs);
972
+ const result = await this.start(retry.channelName, retry.connectorName);
973
+ if (result.ok) {
974
+ this.pendingRetry.delete(retry.key);
975
+ this.failureCounts.delete(retry.key);
976
+ return;
977
+ }
978
+ if (result.retriable === false) {
979
+ this.pendingRetry.delete(retry.key);
980
+ this.failureCounts.delete(retry.key);
981
+ this.logger?.warn("dropping listener from retry queue (non-retriable)", {
982
+ channel: retry.channelName,
983
+ connector: retry.connectorName,
984
+ reason: result.reason
985
+ });
986
+ return;
987
+ }
988
+ this.failureCounts.set(retry.key, failureCount + 1);
989
+ }
735
990
  async recoverDead(channelName, connectorName, type) {
736
- const key = FunnelListenerSupervisor.keyOf(channelName, connectorName);
991
+ const key = FunnelListenerRegistry.keyOf(channelName, connectorName);
737
992
  const failureCount = this.failureCounts.get(key) ?? 0;
738
993
  const backoffMs = Math.min(1e3 * 2 ** failureCount, this.maxBackoffMs);
739
994
  this.logger?.warn(`${type} listener unhealthy, restarting`, {
@@ -744,12 +999,20 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
744
999
  });
745
1000
  await this.stop(channelName, connectorName);
746
1001
  await this.sleep(backoffMs);
747
- if ((await this.start(channelName, connectorName)).ok) {
1002
+ const result = await this.start(channelName, connectorName);
1003
+ if (result.ok) {
748
1004
  this.failureCounts.delete(key);
749
1005
  this.logger?.info(`${type} listener recovered`, {
750
1006
  channel: channelName,
751
1007
  connector: connectorName
752
1008
  });
1009
+ } else if (result.retriable === false) {
1010
+ this.failureCounts.delete(key);
1011
+ this.logger?.warn(`${type} listener cannot recover (non-retriable)`, {
1012
+ channel: channelName,
1013
+ connector: connectorName,
1014
+ reason: result.reason
1015
+ });
753
1016
  } else this.failureCounts.set(key, failureCount + 1);
754
1017
  }
755
1018
  };
@@ -859,7 +1122,12 @@ const channelsConnectorsCallHandler = factory.createHandlers(zParam(z.object({
859
1122
  connector: z.string().min(1)
860
1123
  })), async (c) => {
861
1124
  const param = c.req.valid("param");
862
- const raw = await c.req.json().catch(() => null);
1125
+ let raw = null;
1126
+ try {
1127
+ raw = await c.req.json();
1128
+ } catch {
1129
+ raw = null;
1130
+ }
863
1131
  const parsed = bodySchema.safeParse(raw);
864
1132
  if (!parsed.success) throw new HTTPException(400, { message: parsed.error.issues[0]?.message ?? "invalid body" });
865
1133
  const result = await c.var.deps.channels.call(param.channel, param.connector, {
@@ -971,7 +1239,7 @@ const debugHandler = factory.createHandlers(async (c) => {
971
1239
  const channelFilter = c.req.query("channel") ?? null;
972
1240
  const allChannels = deps.channels.list();
973
1241
  const targetChannels = channelFilter ? allChannels.filter((ch) => ch.name === channelFilter || ch.id === channelFilter) : allChannels;
974
- const gatewayListeners = deps.supervisor.list();
1242
+ const gatewayListeners = deps.registry.list();
975
1243
  const gatewayClients = deps.broadcaster.listChannels();
976
1244
  const metrics = deps.broadcaster.getMetrics();
977
1245
  const tmpDir = funnelTmpDir();
@@ -1074,14 +1342,14 @@ const healthHandler = factory.createHandlers((c) => {
1074
1342
  pid: deps.selfPid,
1075
1343
  funnelDir: deps.dir,
1076
1344
  clients: deps.broadcaster.getClientCount(),
1077
- listeners: deps.supervisor.list()
1345
+ listeners: deps.registry.list()
1078
1346
  });
1079
1347
  });
1080
1348
  //#endregion
1081
1349
  //#region lib/gateway/routes/listeners.list.ts
1082
1350
  /** GET /listeners — running connector listeners with alive/dead status. */
1083
1351
  const listenersListHandler = factory.createHandlers((c) => {
1084
- return c.json({ listeners: c.var.deps.supervisor.list() });
1352
+ return c.json({ listeners: c.var.deps.registry.list() });
1085
1353
  });
1086
1354
  //#endregion
1087
1355
  //#region lib/gateway/routes/listeners.restart.ts
@@ -1091,7 +1359,7 @@ const listenersRestartHandler = factory.createHandlers(zParam(z.object({
1091
1359
  connector: z.string().min(1)
1092
1360
  })), async (c) => {
1093
1361
  const param = c.req.valid("param");
1094
- const result = await c.var.deps.supervisor.restart(param.channel, param.connector);
1362
+ const result = await c.var.deps.registry.restart(param.channel, param.connector);
1095
1363
  return c.json(result, result.ok ? 200 : 400);
1096
1364
  });
1097
1365
  //#endregion
@@ -1102,7 +1370,7 @@ const listenersStartHandler = factory.createHandlers(zParam(z.object({
1102
1370
  connector: z.string().min(1)
1103
1371
  })), async (c) => {
1104
1372
  const param = c.req.valid("param");
1105
- const result = await c.var.deps.supervisor.start(param.channel, param.connector);
1373
+ const result = await c.var.deps.registry.start(param.channel, param.connector);
1106
1374
  return c.json(result, result.ok ? 200 : 400);
1107
1375
  });
1108
1376
  //#endregion
@@ -1113,7 +1381,7 @@ const listenersStopHandler = factory.createHandlers(zParam(z.object({
1113
1381
  connector: z.string().min(1)
1114
1382
  })), async (c) => {
1115
1383
  const param = c.req.valid("param");
1116
- const result = await c.var.deps.supervisor.stop(param.channel, param.connector);
1384
+ const result = await c.var.deps.registry.stop(param.channel, param.connector);
1117
1385
  return c.json(result, result.ok ? 200 : 400);
1118
1386
  });
1119
1387
  //#endregion
@@ -1127,7 +1395,7 @@ const statusHandler = factory.createHandlers((c) => {
1127
1395
  funnelDir: deps.dir,
1128
1396
  uptimeMs: deps.uptimeMs(),
1129
1397
  clients: deps.broadcaster.listChannels(),
1130
- listeners: deps.supervisor.list(),
1398
+ listeners: deps.registry.list(),
1131
1399
  broadcaster: deps.broadcaster.getMetrics()
1132
1400
  });
1133
1401
  });
@@ -1154,7 +1422,7 @@ const defaultDbPath = () => join(funnelTmpDir(), "events.db");
1154
1422
  const defaultOnError = () => {};
1155
1423
  /**
1156
1424
  * In-process gateway: runs `Bun.serve` (HTTP + WebSocket /ws), boots connector
1157
- * listeners through `FunnelListenerSupervisor`, fans events out via
1425
+ * listeners through `FunnelListenerRegistry`, fans events out via
1158
1426
  * `FunnelBroadcaster`, and persists them via a `FunnelEventLog` (SQLite by default).
1159
1427
  * System events (gateway lifecycle, connect/disconnect) flow to `FunnelLogger`
1160
1428
  * instead — keeping the SQLite seq space exclusive to broadcaster traffic so
@@ -1177,11 +1445,13 @@ var FunnelGatewayServer = class {
1177
1445
  allowInsecureHost;
1178
1446
  broadcaster;
1179
1447
  eventLog;
1180
- supervisor;
1448
+ registry;
1181
1449
  nowMs;
1182
1450
  extraRoutes;
1451
+ ownsEventLog;
1183
1452
  startedAt = null;
1184
1453
  server = null;
1454
+ disposed = false;
1185
1455
  constructor(deps) {
1186
1456
  this.channels = deps.channels;
1187
1457
  this.configuredPort = deps.port ?? resolveFunnelPort();
@@ -1198,15 +1468,19 @@ var FunnelGatewayServer = class {
1198
1468
  this.extraRoutes = deps.extraRoutes ?? null;
1199
1469
  const clock = deps.clock;
1200
1470
  this.nowMs = clock ? () => clock.millis() : () => Date.now();
1201
- if (deps.eventLog) this.eventLog = deps.eventLog;
1202
- else {
1471
+ if (deps.eventLog) {
1472
+ this.eventLog = deps.eventLog;
1473
+ this.ownsEventLog = false;
1474
+ } else {
1203
1475
  const dbDir = dirname(this.dbPath);
1204
1476
  if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
1205
1477
  this.eventLog = new SqliteFunnelEventLog({
1206
1478
  path: this.dbPath,
1207
1479
  now: this.nowMs,
1208
- logger: this.logger
1480
+ logger: this.logger,
1481
+ onError: this.onError
1209
1482
  });
1483
+ this.ownsEventLog = true;
1210
1484
  }
1211
1485
  this.broadcaster = new FunnelBroadcaster({
1212
1486
  logger: this.logger,
@@ -1215,7 +1489,7 @@ var FunnelGatewayServer = class {
1215
1489
  persistentReplay: this.eventLog
1216
1490
  });
1217
1491
  this.broadcaster.seedLatestOffset(this.eventLog.findMaxOffset());
1218
- this.supervisor = new FunnelListenerSupervisor({
1492
+ this.registry = new FunnelListenerRegistry({
1219
1493
  channels: this.channels,
1220
1494
  logger: this.logger,
1221
1495
  onError: this.onError,
@@ -1242,6 +1516,7 @@ var FunnelGatewayServer = class {
1242
1516
  return this.server?.hostname ?? this.configuredHostname;
1243
1517
  }
1244
1518
  async start() {
1519
+ if (this.disposed) throw new Error("FunnelGatewayServer is single-use: construct a new instance to start again");
1245
1520
  if (this.server) return;
1246
1521
  if (!this.token && !LOOPBACK_HOSTS.has(this.configuredHostname) && !this.allowInsecureHost) throw new Error(`refusing to start gateway: hostname "${this.configuredHostname}" is reachable off-box but no token is set. Set a token, bind to loopback (127.0.0.1), or pass allowInsecureHost: true.`);
1247
1522
  const app = this.buildApp();
@@ -1259,14 +1534,22 @@ var FunnelGatewayServer = class {
1259
1534
  }
1260
1535
  });
1261
1536
  this.logServerStarted();
1262
- await this.bootListeners();
1537
+ try {
1538
+ await this.bootListeners();
1539
+ } catch (error) {
1540
+ this.server.stop();
1541
+ this.server = null;
1542
+ throw error;
1543
+ }
1263
1544
  }
1264
1545
  async stop() {
1265
- await this.supervisor.stopAll();
1546
+ await this.registry.stopAll();
1266
1547
  if (this.server) {
1267
1548
  this.server.stop();
1268
1549
  this.server = null;
1269
1550
  }
1551
+ if (this.ownsEventLog) this.eventLog.close();
1552
+ this.disposed = true;
1270
1553
  }
1271
1554
  getStatus() {
1272
1555
  return {
@@ -1277,8 +1560,8 @@ var FunnelGatewayServer = class {
1277
1560
  getBroadcaster() {
1278
1561
  return this.broadcaster;
1279
1562
  }
1280
- getSupervisor() {
1281
- return this.supervisor;
1563
+ getRegistry() {
1564
+ return this.registry;
1282
1565
  }
1283
1566
  getEventLog() {
1284
1567
  return this.eventLog;
@@ -1323,7 +1606,14 @@ var FunnelGatewayServer = class {
1323
1606
  handleWsOpen(ws) {
1324
1607
  if (typeof ws.data.since === "number") {
1325
1608
  const replay = this.broadcaster.replaySince(ws.data.since, ws.data);
1326
- for (const event of replay) ws.send(JSON.stringify(event));
1609
+ try {
1610
+ for (const event of replay) ws.send(JSON.stringify(event));
1611
+ } catch (error) {
1612
+ const err = error instanceof Error ? error : new Error(String(error));
1613
+ this.logger?.warn("replay send failed during ws.open", { error: err.message });
1614
+ this.onError(err, { component: "gateway-server.replay" });
1615
+ return;
1616
+ }
1327
1617
  }
1328
1618
  this.broadcaster.addClient(ws, ws.data);
1329
1619
  this.logger?.info("channel connected", {
@@ -1365,7 +1655,7 @@ var FunnelGatewayServer = class {
1365
1655
  selfPid: this.selfPid,
1366
1656
  dir: this.dir,
1367
1657
  broadcaster: this.broadcaster,
1368
- supervisor: this.supervisor,
1658
+ registry: this.registry,
1369
1659
  channels: this.channels,
1370
1660
  uptimeMs: () => this.startedAt ? this.nowMs() - this.startedAt : 0,
1371
1661
  emit: (input) => this.emit(input)
@@ -1419,8 +1709,8 @@ var FunnelGatewayServer = class {
1419
1709
  });
1420
1710
  }
1421
1711
  async bootListeners() {
1422
- await this.supervisor.startAll();
1423
- for (const entry of this.supervisor.list()) this.logger?.info(`${entry.type} listener started: ${entry.name}`, {
1712
+ await this.registry.startAll();
1713
+ for (const entry of this.registry.list()) this.logger?.info(`${entry.type} listener started: ${entry.name}`, {
1424
1714
  event_type: "system",
1425
1715
  action: `${entry.type}_connect`,
1426
1716
  channel: entry.channelName,
@@ -1501,20 +1791,22 @@ var FunnelGatewayToken = class {
1501
1791
  return value.length > 0 ? value : null;
1502
1792
  }
1503
1793
  /**
1504
- * Returns the existing token or, if missing, generates one and writes it with mode 0600.
1505
- *
1506
- * NOTE: not atomic — two concurrent `ensure()` calls (e.g., `fnl gateway start` racing
1507
- * itself before the PID lock is acquired) could each generate independent tokens. The
1508
- * gateway PID file makes this practically a non-issue; if you need stronger guarantees,
1509
- * take a file lock around this call externally.
1794
+ * Returns the existing token or, if missing, generates one and writes it
1795
+ * with mode 0600. Read+write runs inside an exclusive lock so two
1796
+ * concurrent `ensure()` calls (a daemon spawn racing a CLI helper that
1797
+ * reads the token before the gateway PID lock is acquired) cannot each
1798
+ * persist a different token and leave one side authenticating against a
1799
+ * value the other never sees.
1510
1800
  */
1511
1801
  ensure() {
1512
- const existing = this.read();
1513
- if (existing) return existing;
1514
- const token = this.generate();
1515
1802
  this.fs.mkdirSync(dirname(this.path), { recursive: true });
1516
- this.fs.writeSecretFileSync(this.path, `${token}\n`);
1517
- return token;
1803
+ return this.fs.withFileLock(`${this.path}.lock`, () => {
1804
+ const existing = this.read();
1805
+ if (existing) return existing;
1806
+ const token = this.generate();
1807
+ this.fs.writeSecretFileSync(this.path, `${token}\n`);
1808
+ return token;
1809
+ });
1518
1810
  }
1519
1811
  getPath() {
1520
1812
  return this.path;
@@ -1586,97 +1878,6 @@ var MemoryFunnelEventLog = class extends FunnelEventLog {
1586
1878
  close() {}
1587
1879
  };
1588
1880
  //#endregion
1589
- //#region lib/engine/diagnostic-log/diagnostic-log.ts
1590
- /**
1591
- * Points in the listener's connection lifecycle. The single source of truth
1592
- * for the value set: the `status` column schema, the `ConnectorConnectionStatus`
1593
- * union, and the runtime Set used to narrow on read-back all derive from this
1594
- * array, so adding a status is a one-line change that cannot drift out of sync.
1595
- *
1596
- * started start() was called
1597
- * connected the socket opened and events can flow
1598
- * disconnected the socket was closed by a stop() call (a clean teardown)
1599
- * auth-failed the token was rejected before the socket opened
1600
- * stopped the listener was fully torn down (always follows a stop(),
1601
- * paired with the disconnected/error that preceded it)
1602
- * error start/stop threw, or Bolt surfaced an error frame — this is
1603
- * also where an unsolicited socket drop shows up when Bolt
1604
- * reports it (an `error` with no following `stopped` means the
1605
- * supervisor recycled the listener, not a clean stop)
1606
- *
1607
- * A connection row is independent of any single inbound event, so it carries
1608
- * no `eventId`. This is how "no notification arrived because the listener
1609
- * never connected (or dropped, or failed auth)" becomes visible: the
1610
- * raw/processed tables only hold events that *did* arrive.
1611
- */
1612
- const CONNECTOR_CONNECTION_STATUSES = [
1613
- "started",
1614
- "connected",
1615
- "disconnected",
1616
- "auth-failed",
1617
- "stopped",
1618
- "error"
1619
- ];
1620
- /**
1621
- * Rows stored in the diagnostic tables. Connector-agnostic on purpose: `type`
1622
- * carries the listener kind ("slack" | "discord" | "gh" | "schedule") so new
1623
- * connectors land in the same tables without a schema change. `event_id` is
1624
- * the correlation key the listener mints once per inbound event and stamps
1625
- * onto both the raw and processed rows, so the two are joinable even though
1626
- * they live in separate tables with independent `seq` counters.
1627
- *
1628
- * These schemas mirror the stored shape (snake_case columns) the way
1629
- * `FunnelEvent` does for the replay log; they exist for `z.infer` and to
1630
- * document the column set, not as a parse boundary.
1631
- */
1632
- const connectorRawEventSchema = z.object({
1633
- event_id: z.string(),
1634
- type: z.string(),
1635
- connector_id: z.string().nullable(),
1636
- channel_id: z.string().nullable(),
1637
- payload: z.string()
1638
- });
1639
- const connectorProcessedEventSchema = z.object({
1640
- event_id: z.string(),
1641
- type: z.string(),
1642
- connector_id: z.string().nullable(),
1643
- channel_id: z.string().nullable(),
1644
- outcome: z.string(),
1645
- payload: z.string()
1646
- });
1647
- const connectorConnectionEventSchema = z.object({
1648
- type: z.string(),
1649
- connector_id: z.string().nullable(),
1650
- channel_id: z.string().nullable(),
1651
- status: z.enum(CONNECTOR_CONNECTION_STATUSES),
1652
- detail: z.string()
1653
- });
1654
- /**
1655
- * Three-table diagnostic log of everything a connector listener does, so
1656
- * "why was there no notification?" is answerable whichever way it failed:
1657
- * - `raw` — every inbound event, before any filtering, with the listener's
1658
- * untouched payload (the Slack Bolt event, the GH webhook, …)
1659
- * - `processed` — the verdict for that event: `outcome` (emitted, or the
1660
- * reason it was dropped) and, when emitted, the body that was delivered.
1661
- * Shares an `eventId` with its raw row, so the two join into one story.
1662
- * - `connection` — the listener's lifecycle (started, connected, dropped,
1663
- * auth-failed, stopped, errored). This is the half the event tables can't
1664
- * show: an event that never arrived leaves no raw row, but a listener that
1665
- * never connected leaves a `connection` trail that says so.
1666
- *
1667
- * The three are physically separate (independent retention and payload-size
1668
- * policy) so a query never crosses them by accident and a huge raw payload
1669
- * never bloats the verdict or lifecycle trails. None flow to WS clients or the
1670
- * MCP channel — this is a separate store from `FunnelEventLog` (replay) and
1671
- * exists solely for debugging.
1672
- *
1673
- * Implementations:
1674
- * - `SqliteConnectorDiagnosticLog` — the default; survives daemon restarts,
1675
- * bounded by per-table row/age caps.
1676
- * - `MemoryConnectorDiagnosticLog` — an in-process double for tests.
1677
- */
1678
- var ConnectorDiagnosticLog = class {};
1679
- //#endregion
1680
1881
  //#region lib/engine/diagnostic-log/sqlite-diagnostic-log.ts
1681
1882
  /**
1682
1883
  * Cap on a raw payload kept verbatim. The point of the raw table is to see
@@ -1933,81 +2134,4 @@ const headFields = (payload) => {
1933
2134
  }
1934
2135
  };
1935
2136
  //#endregion
1936
- //#region lib/engine/diagnostic-log/memory-diagnostic-log.ts
1937
- /**
1938
- * In-process `ConnectorDiagnosticLog` backed by one array per table. Used by tests
1939
- * and embedders that do not need durability. Like the SQLite log it keeps
1940
- * `seq` per-table (each array's 1-based position) and returns the most recent
1941
- * `limit` rows oldest-first; unlike it, it never prunes and never offloads
1942
- * oversized payloads — it keeps whatever the caller hands it, which is fine
1943
- * for the bounded volumes a test produces. Payload-validity is therefore a
1944
- * SQLite-only guarantee; do not write a test that leans on this double
1945
- * rejecting a malformed payload.
1946
- */
1947
- var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
1948
- raws = [];
1949
- processeds = [];
1950
- connections = [];
1951
- constructor(now = () => Date.now()) {
1952
- super();
1953
- this.now = now;
1954
- Object.freeze(this);
1955
- }
1956
- recordRaw(record) {
1957
- this.raws.push({
1958
- ...record,
1959
- seq: this.raws.length + 1,
1960
- ts: this.now()
1961
- });
1962
- }
1963
- recordProcessed(record) {
1964
- this.processeds.push({
1965
- ...record,
1966
- seq: this.processeds.length + 1,
1967
- ts: this.now()
1968
- });
1969
- }
1970
- recordConnection(record) {
1971
- this.connections.push({
1972
- ...record,
1973
- seq: this.connections.length + 1,
1974
- ts: this.now()
1975
- });
1976
- }
1977
- queryRaw(query) {
1978
- return takeRecent(this.raws.filter((event) => matches(event, query)), query.limit);
1979
- }
1980
- queryProcessed(query) {
1981
- return takeRecent(this.processeds.filter((event) => {
1982
- if (!matches(event, query)) return false;
1983
- if (query.outcome !== void 0 && event.outcome !== query.outcome) return false;
1984
- return true;
1985
- }), query.limit);
1986
- }
1987
- queryConnection(query) {
1988
- return takeRecent(this.connections.filter((event) => {
1989
- if (!matches(event, query)) return false;
1990
- if (query.status !== void 0 && event.status !== query.status) return false;
1991
- return true;
1992
- }), query.limit);
1993
- }
1994
- clear() {
1995
- this.raws.length = 0;
1996
- this.processeds.length = 0;
1997
- this.connections.length = 0;
1998
- }
1999
- close() {}
2000
- };
2001
- const matches = (event, query) => {
2002
- if (query.type !== void 0 && event.type !== query.type) return false;
2003
- if (query.connectorId !== void 0 && event.connectorId !== query.connectorId) return false;
2004
- if (query.channelId !== void 0 && event.channelId !== query.channelId) return false;
2005
- return true;
2006
- };
2007
- const takeRecent = (events, limit) => {
2008
- if (limit === void 0) return events;
2009
- if (limit <= 0) return [];
2010
- return events.slice(-limit);
2011
- };
2012
- //#endregion
2013
- export { funnelTmpDir as C, publishResponseSchema as S, funnelEventSchema as _, connectorConnectionEventSchema as a, FunnelChannelPublisher as b, MemoryFunnelEventLog as c, DEFAULT_GATEWAY_TOKEN_PATH as d, FunnelGatewayToken as f, FunnelEventLog as g, SqliteFunnelEventLog as h, ConnectorDiagnosticLog as i, channelWsProtocols as l, FunnelListenerSupervisor as m, SqliteConnectorDiagnosticLog as n, connectorProcessedEventSchema as o, FunnelGatewayServer as p, CONNECTOR_CONNECTION_STATUSES as r, connectorRawEventSchema as s, MemoryConnectorDiagnosticLog as t, channelWsUrl as u, FunnelBroadcaster as v, publishRequestSchema as x, requireBearerToken as y };
2137
+ export { funnelTmpDir as C, connectorRawEventSchema as S, MemoryConnectorDiagnosticLog as _, DEFAULT_GATEWAY_TOKEN_PATH as a, connectorConnectionEventSchema as b, FunnelListenerRegistry as c, funnelEventSchema as d, FunnelBroadcaster as f, publishResponseSchema as g, publishRequestSchema as h, channelWsUrl as i, SqliteFunnelEventLog as l, FunnelChannelPublisher as m, MemoryFunnelEventLog as n, FunnelGatewayToken as o, requireBearerToken as p, channelWsProtocols as r, FunnelGatewayServer as s, SqliteConnectorDiagnosticLog as t, FunnelEventLog as u, CONNECTOR_CONNECTION_STATUSES as v, connectorProcessedEventSchema as x, ConnectorDiagnosticLog as y };