@interactive-inc/claude-funnel 0.25.2 → 0.26.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.
package/dist/index.d.ts CHANGED
@@ -64,7 +64,7 @@ type SlackListenerOptions = {
64
64
  type ScheduleListenerOptions = {
65
65
  onFired?: ScheduleOnFired;
66
66
  };
67
- type Deps$16 = {
67
+ type Deps$15 = {
68
68
  fs?: FunnelFileSystem;
69
69
  process?: FunnelProcessRunner;
70
70
  logger?: FunnelLogger;
@@ -91,7 +91,7 @@ declare class FunnelConnectorFactory {
91
91
  private readonly dir;
92
92
  private readonly slackListenerOptions;
93
93
  private readonly scheduleListenerOptions;
94
- constructor(deps?: Deps$16);
94
+ constructor(deps?: Deps$15);
95
95
  createListener(channelId: string, config: ConnectorConfig): FunnelConnectorListener;
96
96
  createAdapter(config: ConnectorConfig): FunnelConnectorAdapter | null;
97
97
  connectorDir(channelId: string, connectorId: string): string;
@@ -195,12 +195,14 @@ declare const channelConfigSchema: z.ZodObject<{
195
195
  }, z.core.$strip>;
196
196
  type ChannelConfig = z.infer<typeof channelConfigSchema>;
197
197
  declare const profileConfigSchema: z.ZodObject<{
198
+ id: z.ZodString;
198
199
  name: z.ZodString;
199
200
  path: z.ZodString;
200
201
  channelId: z.ZodString;
201
202
  options: z.ZodDefault<z.ZodArray<z.ZodString>>;
202
203
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
203
204
  resume: z.ZodDefault<z.ZodBoolean>;
205
+ sessionId: z.ZodOptional<z.ZodString>;
204
206
  }, z.core.$strip>;
205
207
  type ProfileConfig = z.infer<typeof profileConfigSchema>;
206
208
  declare const SETTINGS_VERSION = 1;
@@ -256,12 +258,14 @@ declare const settingsSchema: z.ZodObject<{
256
258
  }, z.core.$strip>], "type">>>;
257
259
  }, z.core.$strip>>>;
258
260
  profiles: z.ZodDefault<z.ZodArray<z.ZodObject<{
261
+ id: z.ZodString;
259
262
  name: z.ZodString;
260
263
  path: z.ZodString;
261
264
  channelId: z.ZodString;
262
265
  options: z.ZodDefault<z.ZodArray<z.ZodString>>;
263
266
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
264
267
  resume: z.ZodDefault<z.ZodBoolean>;
268
+ sessionId: z.ZodOptional<z.ZodString>;
265
269
  }, z.core.$strip>>>;
266
270
  }, z.core.$strip>;
267
271
  type Settings = z.infer<typeof settingsSchema>;
@@ -273,7 +277,7 @@ declare abstract class FunnelSettingsReader {
273
277
  }
274
278
  //#endregion
275
279
  //#region lib/engine/channels/channels.d.ts
276
- type Deps$15 = {
280
+ type Deps$14 = {
277
281
  store: FunnelSettingsReader;
278
282
  factory: FunnelConnectorFactory;
279
283
  profileChecker: ProfileChannelChecker;
@@ -317,7 +321,7 @@ declare class FunnelChannels {
317
321
  private readonly profileChecker;
318
322
  private readonly clock;
319
323
  private readonly idGenerator;
320
- constructor(deps: Deps$15);
324
+ constructor(deps: Deps$14);
321
325
  list(): ChannelConfig[];
322
326
  get(name: string): ChannelConfig | null;
323
327
  getById(id: string): ChannelConfig | null;
@@ -375,7 +379,7 @@ type GatewayController = {
375
379
  //#region lib/engine/mcp/mcp.d.ts
376
380
  declare const FUNNEL_MCP_COMMAND = "funnel";
377
381
  declare const FUNNEL_MCP_NAME = "funnel";
378
- type Deps$14 = {
382
+ type Deps$13 = {
379
383
  fs?: FunnelFileSystem;
380
384
  };
381
385
  /**
@@ -385,7 +389,7 @@ type Deps$14 = {
385
389
  */
386
390
  declare class FunnelMcp {
387
391
  private readonly fs;
388
- constructor(deps?: Deps$14);
392
+ constructor(deps?: Deps$13);
389
393
  install(repoPath: string): void;
390
394
  uninstall(repoPath: string): void;
391
395
  findInstalledName(cwd: string): string | null;
@@ -394,46 +398,53 @@ declare class FunnelMcp {
394
398
  private writeConfig;
395
399
  }
396
400
  //#endregion
397
- //#region lib/engine/sessions/sessions.d.ts
398
- type Deps$13 = {
399
- fs: FunnelFileSystem;
401
+ //#region lib/engine/profiles/profiles.d.ts
402
+ type Deps$12 = {
403
+ store: FunnelSettingsReader;
400
404
  idGenerator: FunnelIdGenerator;
401
- dir: string;
402
405
  };
403
406
  /**
404
- * Per-channel persistent Claude Code session IDs, keyed by the cwd the
405
- * channel was launched from. The whole point is to give each (channel, cwd)
406
- * its own stable conversation: relaunching from the same path resumes the
407
- * previous claude session via `--resume <uuid>`, while a different cwd (or
408
- * a different channel) gets an independent one so sessions never silently
409
- * bleed across workspaces the way claude's `-c` does.
407
+ * Named launch presets for `fnl claude`. Each profile bundles a working
408
+ * directory, the channel id its Claude instance subscribes to, and the launch
409
+ * recipe (`options` prepended to the claude argv, `env` layered under the
410
+ * process, `resume` toggling session reuse). Implements ProfileChannelChecker
411
+ * so FunnelChannels can refuse to remove a channel that is still referenced.
410
412
  *
411
- * `get` and `create` are intentionally separate: claude's `--session-id`
412
- * only accepts a fresh UUID (it errors if the session jsonl already
413
- * exists), so callers must check `get` first and fall back to `create`
414
- * only when there is nothing to resume.
413
+ * Each profile has a stable `id` (uuid) minted at `add`. That id is the unit
414
+ * everything internal keys on the PID file, the resumable session id — so a
415
+ * rename never strands either. `name` is purely the CLI/TUI handle; the CRUD
416
+ * methods here take it because that is what the user types, but resolve to the
417
+ * id before touching id-keyed state. The first array entry is the default
418
+ * profile; `asDefault` reorders to put one first.
415
419
  *
416
- * Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
417
- * id, not name, so renames don't lose history). The file is a flat
418
- * `{ cwd: uuid }` map; the channel directory itself is created lazily.
420
+ * `channelId` always stores the channel's stable id (uuid). CLI surfaces
421
+ * resolve channel name id before calling `add`/`update` here.
419
422
  */
420
- declare class FunnelSessions {
421
- private readonly fs;
423
+ declare class FunnelProfiles {
424
+ private readonly store;
422
425
  private readonly idGenerator;
423
- private readonly dir;
424
- constructor(deps: Deps$13);
425
- /** Returns the existing session id for (channelId, cwd) or null. */
426
- get(channelId: string, cwd: string): string | null;
427
- /** Generates a new session id for (channelId, cwd) and persists it, overwriting any prior entry. */
428
- create(channelId: string, cwd: string): string;
429
- /** Drops the recorded session id for (channelId, cwd). No-op if absent. */
430
- clear(channelId: string, cwd: string): void;
431
- /** Drops the whole session map for the channel (e.g. when the channel is deleted). */
432
- clearAll(channelId: string): void;
433
- private readMap;
434
- private writeMap;
435
- private channelDir;
436
- private pathFor;
426
+ constructor(deps: Deps$12);
427
+ list(): ProfileConfig[];
428
+ get(name: string): ProfileConfig | null;
429
+ getById(id: string): ProfileConfig | null;
430
+ getDefault(): ProfileConfig | null;
431
+ add(input: {
432
+ name: string;
433
+ path: string;
434
+ channelId: string;
435
+ options?: string[];
436
+ env?: Record<string, string>;
437
+ resume?: boolean;
438
+ }): void;
439
+ remove(name: string): void;
440
+ rename(oldName: string, newName: string): void;
441
+ asDefault(name: string): void;
442
+ hasChannelRef(channelId: string): boolean;
443
+ /** Resumable claude session id last launched by this profile (by id), or null. */
444
+ getSessionId(id: string): string | null;
445
+ /** Records the claude session id this profile launched, overwriting any prior one. */
446
+ setSessionId(id: string, sessionId: string): void;
447
+ update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void;
437
448
  }
438
449
  //#endregion
439
450
  //#region lib/engine/claude/claude.d.ts
@@ -441,9 +452,16 @@ type LaunchOptions = {
441
452
  channel: string;
442
453
  cwd?: string;
443
454
  userArgs?: string[];
444
- profileName?: string; /** Args prepended to the claude argv (typically a profile's recipe). Defaults to none. */
455
+ /** Stable id of the launching profile (uuid). Keys the singleton PID file and
456
+ * the resumable session. Absent for a profile-less launch (raw `--channel`),
457
+ * which never enforces singleton-ness and never resumes. */
458
+ profileId?: string; /** Args prepended to the claude argv (typically a profile's recipe). Defaults to none. */
445
459
  options?: string[]; /** Env vars layered under the launched claude process. process.env wins on collision. */
446
- env?: Record<string, string>; /** Whether to inject a `--session-id`/`--resume` for the (channel, cwd). Defaults to true. */
460
+ env?: Record<string, string>;
461
+ /** Whether to inject a `--session-id`/`--resume` for this profile.
462
+ * Defaults to false: resuming is opt-in and only meaningful for a profile,
463
+ * since the persisted session is owned by the profile (by id). A launch
464
+ * without a profile always starts a fresh session regardless of this flag. */
447
465
  resume?: boolean;
448
466
  /** Invoked synchronously after the child claude process has been spawned, with its PID.
449
467
  * Useful for hosts that need to register the spawned process before it exits
@@ -454,13 +472,14 @@ type LaunchOptions = {
454
472
  * does not need the funnel binary as an MCP endpoint. */
455
473
  installMcp?: boolean;
456
474
  };
457
- type Deps$12 = {
475
+ type Deps$11 = {
458
476
  channels: FunnelChannels;
459
477
  mcp: FunnelMcp;
460
478
  gateway: GatewayController;
461
- sessions: FunnelSessions;
479
+ profiles: FunnelProfiles;
462
480
  process?: FunnelProcessRunner;
463
481
  fs?: FunnelFileSystem;
482
+ idGenerator?: FunnelIdGenerator;
464
483
  logger?: FunnelLogger;
465
484
  dir?: string;
466
485
  };
@@ -474,14 +493,15 @@ declare class FunnelClaude {
474
493
  private readonly channels;
475
494
  private readonly mcp;
476
495
  private readonly gateway;
477
- private readonly sessions;
496
+ private readonly profiles;
478
497
  private readonly process;
479
498
  private readonly fs;
499
+ private readonly idGenerator;
480
500
  private readonly logger;
481
501
  private readonly pidDir;
482
- constructor(deps: Deps$12);
502
+ constructor(deps: Deps$11);
483
503
  launch(options: LaunchOptions): Promise<number>;
484
- isRunning(profileName: string): boolean;
504
+ isRunning(profileId: string): boolean;
485
505
  private pidPath;
486
506
  private readPid;
487
507
  private writePidFile;
@@ -495,10 +515,16 @@ declare class FunnelClaude {
495
515
  * session-shaping flag, since combining them would either confuse claude
496
516
  * or override the explicit user intent.
497
517
  *
518
+ * The session is owned by the profile (by id), not by cwd: two profiles
519
+ * pointing at the same repo each keep their own conversation, and a launch
520
+ * with no profile never resumes — so an unrelated session in the same repo
521
+ * can't bleed in. The channel never enters into it; sessions belong to the
522
+ * launch layer (profiles), keeping the transport layer ignorant of them.
523
+ *
498
524
  * A persisted id is only resumed when its session jsonl still exists on
499
525
  * disk. claude errors out on `--resume <id>` for a missing conversation, and
500
526
  * a persisted id can outlive its jsonl (claude pruned it, or the very first
501
- * launch was aborted after `create` wrote the id but before the jsonl
527
+ * launch was aborted after the id was written but before the jsonl
502
528
  * appeared). When the file is gone we mint a fresh session instead, which
503
529
  * overwrites the dangling entry — so the store self-heals.
504
530
  */
@@ -527,7 +553,7 @@ declare class FunnelClaude {
527
553
  type OnFunnelError = (error: Error, context?: Record<string, unknown>) => void;
528
554
  //#endregion
529
555
  //#region lib/engine/local-config/dotenv-reader.d.ts
530
- type Deps$11 = {
556
+ type Deps$10 = {
531
557
  fs: FunnelFileSystem;
532
558
  };
533
559
  /**
@@ -538,7 +564,7 @@ type Deps$11 = {
538
564
  */
539
565
  declare class FunnelDotenvReader {
540
566
  private readonly fs;
541
- constructor(deps: Deps$11);
567
+ constructor(deps: Deps$10);
542
568
  read(cwd: string): Record<string, string>;
543
569
  }
544
570
  //#endregion
@@ -599,7 +625,6 @@ declare const channelSpecSchema: z.ZodObject<{
599
625
  }, z.core.$strip>;
600
626
  type ChannelSpec = z.infer<typeof channelSpecSchema>;
601
627
  declare const profileSpecSchema: z.ZodObject<{
602
- name: z.ZodString;
603
628
  channel: z.ZodString;
604
629
  options: z.ZodOptional<z.ZodArray<z.ZodString>>;
605
630
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -637,7 +662,6 @@ declare const localConfigSchema: z.ZodObject<{
637
662
  }, z.core.$strip>], "type">>>;
638
663
  }, z.core.$strip>>;
639
664
  profiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
640
- name: z.ZodString;
641
665
  channel: z.ZodString;
642
666
  options: z.ZodOptional<z.ZodArray<z.ZodString>>;
643
667
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -649,7 +673,7 @@ declare const LOCAL_CONFIG_FILENAME = "funnel.json";
649
673
  declare const LOCAL_ENV_FILENAME = ".env.local";
650
674
  //#endregion
651
675
  //#region lib/engine/local-config/local-config.d.ts
652
- type Deps$10 = {
676
+ type Deps$9 = {
653
677
  fs: FunnelFileSystem;
654
678
  };
655
679
  /**
@@ -660,8 +684,9 @@ type Deps$10 = {
660
684
  */
661
685
  declare class FunnelLocalConfig {
662
686
  private readonly fs;
663
- constructor(deps: Deps$10);
687
+ constructor(deps: Deps$9);
664
688
  read(cwd: string): LocalConfig | null;
689
+ private assertProfilesValid;
665
690
  }
666
691
  //#endregion
667
692
  //#region lib/engine/token-prompter/token-prompter.d.ts
@@ -676,7 +701,7 @@ declare abstract class FunnelTokenPrompter {
676
701
  }
677
702
  //#endregion
678
703
  //#region lib/engine/local-config/local-config-sync.d.ts
679
- type Deps$9 = {
704
+ type Deps$8 = {
680
705
  channels: FunnelChannels;
681
706
  dotenv: FunnelDotenvReader;
682
707
  prompter: FunnelTokenPrompter;
@@ -714,7 +739,7 @@ declare class FunnelLocalConfigSync {
714
739
  private readonly dotenv;
715
740
  private readonly prompter;
716
741
  private readonly env;
717
- constructor(deps: Deps$9);
742
+ constructor(deps: Deps$8);
718
743
  ensure(channel: ChannelSpec, cwd: string): Promise<LocalConfigSyncResult>;
719
744
  private ensureConnector;
720
745
  private ensureSlack;
@@ -729,44 +754,6 @@ declare class FunnelLocalConfigSync {
729
754
  private resolveField;
730
755
  }
731
756
  //#endregion
732
- //#region lib/engine/profiles/profiles.d.ts
733
- type Deps$8 = {
734
- store: FunnelSettingsReader;
735
- };
736
- /**
737
- * Named launch presets for `fnl claude`. Each profile bundles a working
738
- * directory, the channel id its Claude instance subscribes to, and the launch
739
- * recipe (`options` prepended to the claude argv, `env` layered under the
740
- * process, `resume` toggling session reuse). Implements ProfileChannelChecker
741
- * so FunnelChannels can refuse to remove a channel that is still referenced.
742
- *
743
- * The first entry in the persisted array is treated as the default profile;
744
- * `asDefault` reorders the array to put a named profile first.
745
- *
746
- * `channelId` always stores the channel's stable id (uuid). CLI surfaces
747
- * resolve channel name → id before calling `add`/`update` here.
748
- */
749
- declare class FunnelProfiles {
750
- private readonly store;
751
- constructor(deps: Deps$8);
752
- list(): ProfileConfig[];
753
- get(name: string): ProfileConfig | null;
754
- getDefault(): ProfileConfig | null;
755
- add(input: {
756
- name: string;
757
- path: string;
758
- channelId: string;
759
- options?: string[];
760
- env?: Record<string, string>;
761
- resume?: boolean;
762
- }): void;
763
- remove(name: string): void;
764
- rename(oldName: string, newName: string): void;
765
- asDefault(name: string): void;
766
- hasChannelRef(channelId: string): boolean;
767
- update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void;
768
- }
769
- //#endregion
770
757
  //#region lib/gateway/publish-schema.d.ts
771
758
  /**
772
759
  * Shared schema for `POST /channels/:channel/publish` — used by both the
@@ -832,9 +819,9 @@ type ReplayableEvent = BroadcastEvent & {
832
819
  };
833
820
  type BroadcastSubscriber = (event: ReplayableEvent) => void;
834
821
  /**
835
- * Optional persistent replay source. Wired in by the gateway-server with
836
- * `FunnelEventStore` (SQLite-backed) so reconnects across daemon restarts
837
- * can recover events older than the in-memory buffer via an indexed
822
+ * Optional persistent replay source. Wired in by the gateway-server with a
823
+ * `FunnelEventLog` (SQLite-backed by default) so reconnects across daemon
824
+ * restarts can recover events older than the in-memory buffer via an indexed
838
825
  * `seq > since` range scan.
839
826
  */
840
827
  type ReplaySource = {
@@ -873,8 +860,7 @@ type BroadcasterMetrics = {
873
860
  * `replayBufferSize` events are kept in memory; reconnecting WS clients can
874
861
  * pass `?since=<offset>` and the broadcaster resends matching events before
875
862
  * resuming the live stream. The in-memory ring covers short reconnects;
876
- * older history is served from the SQLite event store wired in as
877
- * `persistentReplay`.
863
+ * older history is served from the event log wired in as `persistentReplay`.
878
864
  */
879
865
  declare class FunnelBroadcaster {
880
866
  private readonly clients;
@@ -902,8 +888,8 @@ declare class FunnelBroadcaster {
902
888
  * Two-tier lookup:
903
889
  * 1. The in-memory ring buffer (covers short reconnects, last `replayBufferSize` events).
904
890
  * 2. If `since` predates the oldest in-memory entry and a persistent replay source
905
- * is wired in (SQLite), the gap is filled from disk. This covers reconnects across
906
- * daemon restarts where the in-memory buffer was lost.
891
+ * is wired in (SQLite by default), the gap is filled from it. This covers reconnects
892
+ * across daemon restarts where the in-memory buffer was lost.
907
893
  *
908
894
  * Result is sorted ascending by offset and de-duplicated against the in-memory buffer.
909
895
  */
@@ -1038,6 +1024,51 @@ type Env$1 = {
1038
1024
  };
1039
1025
  };
1040
1026
  //#endregion
1027
+ //#region lib/gateway/funnel-event-log.d.ts
1028
+ /**
1029
+ * Replayable event payload persisted by the gateway. Domain events the
1030
+ * broadcaster emits to WS clients land here so reconnects across daemon
1031
+ * restarts can be served from disk. System events (gateway start, channel
1032
+ * connected, etc.) are routed to `FunnelLogger` instead — they never go
1033
+ * through this log, which keeps the offset space clean for replay.
1034
+ */
1035
+ declare const funnelEventSchema: z.ZodObject<{
1036
+ type: z.ZodString;
1037
+ content: z.ZodString;
1038
+ channel_id: z.ZodNullable<z.ZodString>;
1039
+ connector_id: z.ZodNullable<z.ZodString>;
1040
+ meta: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodString>>;
1041
+ }, z.core.$strip>;
1042
+ type FunnelEvent = z.infer<typeof funnelEventSchema>;
1043
+ /** One broadcast event to persist, carrying the offset the broadcaster assigned. */
1044
+ type FunnelEventRecord = {
1045
+ content: string;
1046
+ channelId: string | null;
1047
+ connectorId: string | null;
1048
+ meta: Record<string, string> | null;
1049
+ offset: number;
1050
+ };
1051
+ /**
1052
+ * Durable, append-only log of broadcaster events keyed by the offset the
1053
+ * broadcaster assigns. The gateway persists every domain event here, and
1054
+ * across restarts it both seeds the broadcaster's offset counter
1055
+ * (`findMaxOffset`) and serves reconnect replay (`loadSince`) from it.
1056
+ *
1057
+ * `loadSince` is the only method the broadcaster itself needs, which makes
1058
+ * any implementation assignable to the broadcaster's narrow `ReplaySource`.
1059
+ *
1060
+ * Implementations:
1061
+ * - `SqliteFunnelEventLog` — the default; durable across daemon restarts.
1062
+ * - `MemoryFunnelEventLog` — an in-process double for tests and embedders
1063
+ * that do not need durability (replay is lost when the process exits).
1064
+ */
1065
+ declare abstract class FunnelEventLog {
1066
+ abstract record(record: FunnelEventRecord): void;
1067
+ abstract loadSince(since: number): ReplayableEvent[];
1068
+ abstract findMaxOffset(): number;
1069
+ abstract close(): void;
1070
+ }
1071
+ //#endregion
1041
1072
  //#region lib/gateway/gateway.d.ts
1042
1073
  type Deps$4 = {
1043
1074
  process?: FunnelProcessRunner;
@@ -1093,88 +1124,13 @@ declare class FunnelGateway {
1093
1124
  private isProcessAlive;
1094
1125
  }
1095
1126
  //#endregion
1096
- //#region lib/gateway/funnel-event-store.d.ts
1097
- /**
1098
- * Replayable event payload persisted by the gateway. Domain events the
1099
- * broadcaster emits to WS clients land here so reconnects across daemon
1100
- * restarts can be served from disk. System events (gateway start, channel
1101
- * connected, etc.) are routed to `FunnelLogger` instead — they never go
1102
- * through this store, which keeps the seq space clean for replay.
1103
- */
1104
- declare const funnelEventSchema: z.ZodObject<{
1105
- type: z.ZodString;
1106
- content: z.ZodString;
1107
- channel_id: z.ZodNullable<z.ZodString>;
1108
- connector_id: z.ZodNullable<z.ZodString>;
1109
- meta: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodString>>;
1110
- }, z.core.$strip>;
1111
- type FunnelEvent = z.infer<typeof funnelEventSchema>;
1112
- type Props$6 = {
1113
- /** SQLite database file path. Created on first write. ":memory:" for tests. */path: string; /** Override for tests. Defaults to `Date.now`. */
1114
- now?: () => number; /** Optional row cap. Pruned on every insert. */
1115
- maxRows?: number; /** Optional age cap in ms. Pruned on every insert. */
1116
- maxAgeMs?: number;
1117
- };
1118
- /**
1119
- * SQLite-backed event store. One indexed table holds every broadcaster
1120
- * event with `channel_id` and `connector_id` as dedicated columns, so
1121
- * per-channel and per-connector replay is an indexed range scan.
1122
- *
1123
- * Concurrency: `seq` is `INTEGER PRIMARY KEY`, so SQLite assigns it
1124
- * atomically. The broadcaster owns its own offset counter at runtime
1125
- * (seeded from `findMaxOffset()` at startup); each broadcaster event
1126
- * flows in here via `record()` with that pre-assigned offset, which the
1127
- * sink stores via `write()` — PK uniqueness catches double-emit bugs.
1128
- *
1129
- * System events (gateway lifecycle, channel connect/disconnect, etc.) do
1130
- * NOT go through this store. They are diagnostic only and live in
1131
- * `FunnelLogger`'s file so the seq space here stays exclusive to
1132
- * broadcaster traffic. This is what makes the broadcaster's seq seeding
1133
- * (`getMaxSeq()` at startup) correct without per-event coordination.
1134
- */
1135
- declare class FunnelEventStore {
1136
- private readonly sink;
1137
- private readonly now;
1138
- constructor(props: Props$6);
1139
- /**
1140
- * Persist a broadcaster-driven event with its assigned offset. Caller
1141
- * (the gateway-server) supplies the offset from `broadcaster.broadcast()`
1142
- * so this store and the broadcaster's in-memory ring stay aligned.
1143
- */
1144
- record(props: {
1145
- content: string;
1146
- channelId: string | null;
1147
- connectorId: string | null;
1148
- meta: Record<string, string> | null;
1149
- offset: number;
1150
- }): void;
1151
- /**
1152
- * Returns events with offset > since. Filtering by channel/connector is
1153
- * the broadcaster's responsibility (it knows the client's subscription),
1154
- * so this returns the full slice and lets the caller filter.
1155
- */
1156
- loadSince(since: number): ReplayableEvent[];
1157
- /**
1158
- * Returns events for one channel (and optionally one connector). Used
1159
- * by the gateway logs CLI for scoped queries. Channel/connector filters
1160
- * are indexed columns, so this is an indexed range scan.
1161
- */
1162
- loadForChannel(props: {
1163
- channelId: string;
1164
- connectorId?: string;
1165
- sinceSeq?: number;
1166
- limit?: number;
1167
- }): ReplayableEvent[];
1168
- findMaxOffset(): number;
1169
- close(): void;
1170
- }
1171
- //#endregion
1172
1127
  //#region lib/gateway/gateway-server.d.ts
1173
1128
  type Deps$3 = {
1174
1129
  channels: FunnelChannels;
1175
1130
  settings: FunnelSettingsReader;
1176
- port?: number; /** SQLite event store file path. Parent directory is created on demand. Defaults to `<os.tmpdir()>/funnel/events.db`. */
1177
- dbPath?: string;
1131
+ port?: number; /** SQLite event store file path. Parent directory is created on demand. Defaults to `<os.tmpdir()>/funnel/events.db`. Ignored when `eventLog` is supplied. */
1132
+ dbPath?: string; /** Durable replay log. Defaults to a `SqliteFunnelEventLog` at `dbPath`. Inject a `MemoryFunnelEventLog` (or any `FunnelEventLog`) to swap or disable persistence. */
1133
+ eventLog?: FunnelEventLog;
1178
1134
  process?: FunnelProcessRunner;
1179
1135
  clock?: FunnelClock;
1180
1136
  logger?: FunnelLogger; /** Host hook for surfacing internal exceptions (broadcaster / supervisor). Defaults to no-op. */
@@ -1202,7 +1158,7 @@ type WsData = {
1202
1158
  /**
1203
1159
  * In-process gateway: runs `Bun.serve` (HTTP + WebSocket /ws), boots connector
1204
1160
  * listeners through `FunnelListenerSupervisor`, fans events out via
1205
- * `FunnelBroadcaster`, and persists them via `FunnelEventStore` (SQLite).
1161
+ * `FunnelBroadcaster`, and persists them via a `FunnelEventLog` (SQLite by default).
1206
1162
  * System events (gateway lifecycle, connect/disconnect) flow to `FunnelLogger`
1207
1163
  * instead — keeping the SQLite seq space exclusive to broadcaster traffic so
1208
1164
  * the broadcaster's offset counter and `getMaxSeq()` stay aligned without
@@ -1222,7 +1178,7 @@ declare class FunnelGatewayServer {
1222
1178
  private readonly killCompetingSlack;
1223
1179
  private readonly token;
1224
1180
  private readonly broadcaster;
1225
- private readonly eventStore;
1181
+ private readonly eventLog;
1226
1182
  private readonly supervisor;
1227
1183
  private readonly nowMs;
1228
1184
  private readonly extraRoutes;
@@ -1240,7 +1196,15 @@ declare class FunnelGatewayServer {
1240
1196
  };
1241
1197
  getBroadcaster(): FunnelBroadcaster;
1242
1198
  getSupervisor(): FunnelListenerSupervisor;
1243
- getEventStore(): FunnelEventStore;
1199
+ getEventLog(): FunnelEventLog;
1200
+ /**
1201
+ * Register an in-process observer for every broadcast event. Fires after
1202
+ * the event is fanned out to WS clients and recorded in the event log.
1203
+ * Returns an unsubscribe function. Only meaningful in-process (embedded
1204
+ * hosts / `new Funnel(...)` running their own gateway-server); a separate
1205
+ * daemon process cannot be observed this way — use a WS client for that.
1206
+ */
1207
+ onEvent(handler: BroadcastSubscriber): () => void;
1244
1208
  private handleFetch;
1245
1209
  private handleWsOpen;
1246
1210
  private handleWsClose;
@@ -1360,7 +1324,7 @@ declare class FunnelListenersClient {
1360
1324
  }
1361
1325
  //#endregion
1362
1326
  //#region lib/funnel.d.ts
1363
- type Props$5 = {
1327
+ type Props$6 = {
1364
1328
  /** Settings persistence (channels with nested connectors / profiles). Defaults to a FunnelSettingsStore rooted at `dir`. */store?: FunnelSettingsReader; /** Filesystem boundary. Replace with MemoryFunnelFileSystem to sandbox all disk I/O. */
1365
1329
  fs?: FunnelFileSystem; /** Process runner used by gateway / claude / gh listener. Replace with MemoryFunnelProcessRunner for tests. */
1366
1330
  process?: FunnelProcessRunner; /** Logger flowed into every facet. Replace with MemoryFunnelLogger or NoopFunnelLogger to silence/inspect. */
@@ -1410,13 +1374,13 @@ type Props$5 = {
1410
1374
  declare class Funnel {
1411
1375
  private readonly props;
1412
1376
  private readonly memos;
1413
- constructor(props?: Props$5);
1377
+ constructor(props?: Props$6);
1414
1378
  /**
1415
1379
  * Sandboxed Funnel wired with in-memory implementations for every IO boundary.
1416
1380
  * Touches no real disk, processes, wall-clock time, or UUIDs — safe for tests
1417
1381
  * and ad-hoc experiments. Override individual fields by passing them in `props`.
1418
1382
  */
1419
- static inMemory(props?: Props$5): Funnel;
1383
+ static inMemory(props?: Props$6): Funnel;
1420
1384
  /** Resolved on-disk paths the facade will read/write when methods are called. Pure compute, not memoized. */
1421
1385
  get paths(): {
1422
1386
  dir: string;
@@ -1427,8 +1391,8 @@ declare class Funnel {
1427
1391
  get fs(): FunnelFileSystem;
1428
1392
  /** Process runner boundary. Defaults to NodeFunnelProcessRunner. */
1429
1393
  get process(): FunnelProcessRunner;
1430
- /** Logger boundary. Defaults to NodeFunnelLogger. */
1431
- get logger(): FunnelLogger;
1394
+ /** Logger boundary. Optional when no logger is injected, every facet's `this.logger?.x` call is a silent no-op. Production entry points (cli, daemon) inject a NodeFunnelLogger. */
1395
+ get logger(): FunnelLogger | undefined;
1432
1396
  /** Clock boundary. Defaults to NodeFunnelClock. */
1433
1397
  get clock(): FunnelClock;
1434
1398
  /**
@@ -1446,8 +1410,6 @@ declare class Funnel {
1446
1410
  get channels(): FunnelChannels;
1447
1411
  /** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
1448
1412
  get profiles(): FunnelProfiles;
1449
- /** Per-(channel, cwd) claude session-id store. Backs `--session-id` injection on launch. */
1450
- get sessions(): FunnelSessions;
1451
1413
  /** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
1452
1414
  get localConfig(): FunnelLocalConfig;
1453
1415
  /** Parses `.env.local` from a cwd (used by sync to back $VAR references). */
@@ -1485,7 +1447,8 @@ declare class Funnel {
1485
1447
  port?: number;
1486
1448
  dbPath?: string;
1487
1449
  killCompetingSlack?: boolean; /** Override the auth token. Defaults to the persisted gateway.token. Pass "" to disable auth (tests). */
1488
- token?: string;
1450
+ token?: string; /** Durable replay log. Defaults to a SqliteFunnelEventLog at dbPath; inject a MemoryFunnelEventLog (or any FunnelEventLog) to swap or disable persistence. */
1451
+ eventLog?: FunnelEventLog;
1489
1452
  /**
1490
1453
  * Additional hono app mounted before the built-in gateway routes.
1491
1454
  * Use to embed host-specific endpoints (e.g. an MCP route, custom `/api/*`).
@@ -1520,13 +1483,23 @@ declare const SETTINGS_PATH: string;
1520
1483
  type Deps = {
1521
1484
  path?: string;
1522
1485
  fs?: FunnelFileSystem;
1486
+ idGenerator?: FunnelIdGenerator;
1523
1487
  };
1524
1488
  declare class FunnelSettingsStore extends FunnelSettingsReader {
1525
1489
  private readonly path;
1526
1490
  private readonly fs;
1491
+ private readonly idGenerator;
1527
1492
  constructor(deps?: Deps);
1528
1493
  read(): Settings;
1529
1494
  private looksLikeLegacy;
1495
+ /**
1496
+ * Non-destructive migration for profiles written before `id` existed. The id
1497
+ * is a later addition to an otherwise-compatible schema, so rather than
1498
+ * rejecting the file we mint a uuid for each profile that lacks one; the next
1499
+ * `write` persists it. Mutates `parsed` in place (it is freshly JSON-parsed
1500
+ * and discarded after the schema parse, so no shared state is touched).
1501
+ */
1502
+ private backfillProfileIds;
1530
1503
  write(settings: Settings): void;
1531
1504
  }
1532
1505
  //#endregion
@@ -1556,7 +1529,7 @@ declare class NodeFunnelFileSystem extends FunnelFileSystem {
1556
1529
  }
1557
1530
  //#endregion
1558
1531
  //#region lib/engine/fs/memory-file-system.d.ts
1559
- type Props$4 = {
1532
+ type Props$5 = {
1560
1533
  dirs?: string[];
1561
1534
  files?: Record<string, string>;
1562
1535
  mtimes?: Record<string, number>;
@@ -1569,7 +1542,7 @@ declare class MemoryFunnelFileSystem extends FunnelFileSystem {
1569
1542
  private readonly mtimes;
1570
1543
  private readonly modes;
1571
1544
  private readonly now;
1572
- constructor(props?: Props$4);
1545
+ constructor(props?: Props$5);
1573
1546
  existsSync(path: string): boolean;
1574
1547
  readFileSync(path: string): string;
1575
1548
  writeFileSync(path: string, data: string): void;
@@ -1655,14 +1628,14 @@ declare class MemoryFunnelProcessRunner extends FunnelProcessRunner {
1655
1628
  }
1656
1629
  //#endregion
1657
1630
  //#region lib/engine/logger/node-logger.d.ts
1658
- type Props$3 = {
1631
+ type Props$4 = {
1659
1632
  file?: string;
1660
1633
  now?: () => Date;
1661
1634
  };
1662
1635
  declare class NodeFunnelLogger extends FunnelLogger {
1663
1636
  readonly file: string;
1664
1637
  private readonly now;
1665
- constructor(props?: Props$3);
1638
+ constructor(props?: Props$4);
1666
1639
  info(message: string, meta?: Record<string, unknown>): void;
1667
1640
  warn(message: string, meta?: Record<string, unknown>): void;
1668
1641
  error(message: string, meta?: Record<string, unknown>): void;
@@ -1698,12 +1671,12 @@ declare class NodeFunnelClock extends FunnelClock {
1698
1671
  }
1699
1672
  //#endregion
1700
1673
  //#region lib/engine/time/memory-clock.d.ts
1701
- type Props$2 = {
1674
+ type Props$3 = {
1702
1675
  start?: Date;
1703
1676
  };
1704
1677
  declare class MemoryFunnelClock extends FunnelClock {
1705
1678
  private current;
1706
- constructor(props?: Props$2);
1679
+ constructor(props?: Props$3);
1707
1680
  now(): Date;
1708
1681
  set(date: Date): void;
1709
1682
  advance(ms: number): void;
@@ -1715,13 +1688,13 @@ declare class NodeFunnelIdGenerator extends FunnelIdGenerator {
1715
1688
  }
1716
1689
  //#endregion
1717
1690
  //#region lib/engine/id/memory-id-generator.d.ts
1718
- type Props$1 = {
1691
+ type Props$2 = {
1719
1692
  prefix?: string;
1720
1693
  };
1721
1694
  declare class MemoryFunnelIdGenerator extends FunnelIdGenerator {
1722
1695
  private counter;
1723
1696
  private readonly prefix;
1724
- constructor(props?: Props$1);
1697
+ constructor(props?: Props$2);
1725
1698
  generate(): string;
1726
1699
  }
1727
1700
  //#endregion
@@ -1738,7 +1711,7 @@ declare class NodeFunnelTokenPrompter extends FunnelTokenPrompter {
1738
1711
  }
1739
1712
  //#endregion
1740
1713
  //#region lib/engine/token-prompter/memory-token-prompter.d.ts
1741
- type Props = {
1714
+ type Props$1 = {
1742
1715
  answers?: Record<string, string>;
1743
1716
  };
1744
1717
  /**
@@ -1748,10 +1721,82 @@ type Props = {
1748
1721
  declare class MemoryFunnelTokenPrompter extends FunnelTokenPrompter {
1749
1722
  private readonly answers;
1750
1723
  readonly asked: string[];
1751
- constructor(props?: Props);
1724
+ constructor(props?: Props$1);
1752
1725
  promptSecret(label: string): Promise<string>;
1753
1726
  }
1754
1727
  //#endregion
1728
+ //#region lib/gateway/sqlite-funnel-event-log.d.ts
1729
+ type Props = {
1730
+ /** SQLite database file path. Created on first write. ":memory:" for tests. */path: string; /** Override for tests. Defaults to `Date.now`. */
1731
+ now?: () => number; /** Optional row cap. Pruned on every insert. */
1732
+ maxRows?: number; /** Optional age cap in ms. Pruned on every insert. */
1733
+ maxAgeMs?: number;
1734
+ };
1735
+ /**
1736
+ * SQLite-backed `FunnelEventLog`. One indexed table holds every broadcaster
1737
+ * event with `channel_id` and `connector_id` as dedicated columns, so
1738
+ * per-channel and per-connector replay is an indexed range scan.
1739
+ *
1740
+ * Concurrency: `seq` is `INTEGER PRIMARY KEY`, so SQLite assigns it
1741
+ * atomically. The broadcaster owns its own offset counter at runtime
1742
+ * (seeded from `findMaxOffset()` at startup); each broadcaster event
1743
+ * flows in here via `record()` with that pre-assigned offset, which the
1744
+ * sink stores via `write()` — PK uniqueness catches double-emit bugs.
1745
+ *
1746
+ * System events (gateway lifecycle, channel connect/disconnect, etc.) do
1747
+ * NOT go through this store. They are diagnostic only and live in
1748
+ * `FunnelLogger`'s file so the seq space here stays exclusive to
1749
+ * broadcaster traffic. This is what makes the broadcaster's seq seeding
1750
+ * (`getMaxSeq()` at startup) correct without per-event coordination.
1751
+ */
1752
+ declare class SqliteFunnelEventLog extends FunnelEventLog {
1753
+ private readonly sink;
1754
+ private readonly now;
1755
+ constructor(props: Props);
1756
+ /**
1757
+ * Persist a broadcaster-driven event with its assigned offset. Caller
1758
+ * (the gateway-server) supplies the offset from `broadcaster.broadcast()`
1759
+ * so this store and the broadcaster's in-memory ring stay aligned.
1760
+ */
1761
+ record(record: FunnelEventRecord): void;
1762
+ /**
1763
+ * Returns events with offset > since. Filtering by channel/connector is
1764
+ * the broadcaster's responsibility (it knows the client's subscription),
1765
+ * so this returns the full slice and lets the caller filter.
1766
+ */
1767
+ loadSince(since: number): ReplayableEvent[];
1768
+ /**
1769
+ * Returns events for one channel (and optionally one connector). Used
1770
+ * by the gateway logs CLI for scoped queries. Channel/connector filters
1771
+ * are indexed columns, so this is an indexed range scan.
1772
+ */
1773
+ loadForChannel(props: {
1774
+ channelId: string;
1775
+ connectorId?: string;
1776
+ sinceSeq?: number;
1777
+ limit?: number;
1778
+ }): ReplayableEvent[];
1779
+ findMaxOffset(): number;
1780
+ close(): void;
1781
+ }
1782
+ //#endregion
1783
+ //#region lib/gateway/memory-funnel-event-log.d.ts
1784
+ /**
1785
+ * In-process `FunnelEventLog` backed by a plain array. Used by tests and by
1786
+ * embedders that do not need durability — replay works within the process
1787
+ * lifetime but is lost when the process exits. Unlike the SQLite log it does
1788
+ * not truncate content or prune, so it is not meant for unbounded production
1789
+ * traffic.
1790
+ */
1791
+ declare class MemoryFunnelEventLog extends FunnelEventLog {
1792
+ private readonly events;
1793
+ constructor();
1794
+ record(record: FunnelEventRecord): void;
1795
+ loadSince(since: number): ReplayableEvent[];
1796
+ findMaxOffset(): number;
1797
+ close(): void;
1798
+ }
1799
+ //#endregion
1755
1800
  //#region lib/cli/factory.d.ts
1756
1801
  type Env = {
1757
1802
  Variables: {
@@ -4386,4 +4431,4 @@ ${string}`;
4386
4431
  //#region lib/tui/tui.d.ts
4387
4432
  declare function launchTui(funnel: Funnel): Promise<void>;
4388
4433
  //#endregion
4389
- export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSessions, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, OnFunnelError, ProcessListStub, ProcessSnapshot, ProfileConfig, ProfileSpec, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
4434
+ export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventLog, FunnelEventRecord, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, OnFunnelError, ProcessListStub, ProcessSnapshot, ProfileConfig, ProfileSpec, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };