@remnic/capture-audio 9.63.1 → 9.63.3

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.
@@ -4,7 +4,7 @@
4
4
  var CAPTURE_AUDIO_VERSION = "9.14.0";
5
5
  var DEFAULT_HOST = "127.0.0.1";
6
6
  var DEFAULT_PORT = 4340;
7
- var SPOOL_SCHEMA_VERSION = 2;
7
+ var SPOOL_SCHEMA_VERSION = 3;
8
8
  var MAX_CONVERSATIONS_LIMIT = 500;
9
9
  var DEFAULT_CONVERSATIONS_LIMIT = 50;
10
10
 
@@ -921,6 +921,30 @@ async function pruneExpiredRawAudio(rawDirectory, retentionMs, nowMs = Date.now(
921
921
  return removed.sort();
922
922
  }
923
923
 
924
+ // src/buffer-policy.ts
925
+ var MAX_BUFFERED_CHUNKS = 512;
926
+ var QUARANTINE_AFTER_FAILURES = 3;
927
+ var MAX_QUARANTINED_CHUNKS = 64;
928
+ var ChunkApplyError = class extends Error {
929
+ chunkId;
930
+ constructor(chunkId, cause) {
931
+ super(cause instanceof Error ? cause.message : String(cause));
932
+ this.name = "ChunkApplyError";
933
+ this.chunkId = chunkId;
934
+ if (cause instanceof Error) this.cause = cause;
935
+ }
936
+ };
937
+ function spansOverlap(left, right) {
938
+ return left.startMs < right.endMs && right.startMs < left.endMs;
939
+ }
940
+ function isReleaseEligible(candidate, threshold, held) {
941
+ if (candidate.endMs > threshold) return false;
942
+ for (const other of held) {
943
+ if (spansOverlap(candidate, other)) return false;
944
+ }
945
+ return true;
946
+ }
947
+
924
948
  // src/spool.ts
925
949
  import { chmodSync as chmodSync2 } from "fs";
926
950
  import { DatabaseSync } from "sqlite";
@@ -971,6 +995,16 @@ CREATE TABLE IF NOT EXISTS applied_chunks (
971
995
  conversation_id TEXT NOT NULL,
972
996
  applied_at_utc TEXT NOT NULL
973
997
  );
998
+ CREATE TABLE IF NOT EXISTS pending_chunks (
999
+ id TEXT PRIMARY KEY,
1000
+ wav_path TEXT NOT NULL,
1001
+ started_at_utc TEXT NOT NULL,
1002
+ ended_at_utc TEXT NOT NULL,
1003
+ channel TEXT NOT NULL,
1004
+ device TEXT,
1005
+ reason TEXT NOT NULL,
1006
+ created_at_utc TEXT NOT NULL
1007
+ );
974
1008
  CREATE INDEX IF NOT EXISTS idx_conv_keyset ON conversations(started_at_utc, id);
975
1009
  CREATE INDEX IF NOT EXISTS idx_seg_conv ON segments(conversation_id, ordinal);
976
1010
  `;
@@ -1504,6 +1538,42 @@ var Spool = class {
1504
1538
  const row = this.#db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE status = 'pending'").get();
1505
1539
  return row.n;
1506
1540
  }
1541
+ recordPendingChunk(input) {
1542
+ this.#db.prepare(
1543
+ "INSERT INTO pending_chunks(id, wav_path, started_at_utc, ended_at_utc, channel, device, reason, created_at_utc) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET wav_path = excluded.wav_path, started_at_utc = excluded.started_at_utc, ended_at_utc = excluded.ended_at_utc, channel = excluded.channel, device = excluded.device, reason = excluded.reason, created_at_utc = excluded.created_at_utc"
1544
+ ).run(
1545
+ input.id,
1546
+ input.wavPath,
1547
+ canonicalInstant(input.startedAtUtc, "pendingChunk.startedAtUtc"),
1548
+ canonicalInstant(input.endedAtUtc, "pendingChunk.endedAtUtc"),
1549
+ input.channel,
1550
+ input.device,
1551
+ input.reason,
1552
+ (/* @__PURE__ */ new Date()).toISOString()
1553
+ );
1554
+ if (input.reason !== "quarantined") return;
1555
+ const extra = this.#db.prepare(
1556
+ "SELECT id FROM pending_chunks WHERE reason = 'quarantined' ORDER BY created_at_utc ASC, id ASC"
1557
+ ).all();
1558
+ const overflow = extra.length - MAX_QUARANTINED_CHUNKS;
1559
+ if (overflow <= 0) return;
1560
+ const drop = this.#db.prepare("DELETE FROM pending_chunks WHERE id = ?");
1561
+ for (let i = 0; i < overflow; i++) {
1562
+ const id = extra[i]?.id;
1563
+ if (id !== void 0) drop.run(id);
1564
+ }
1565
+ }
1566
+ listPendingChunks(reason) {
1567
+ const rows = reason === void 0 ? this.#db.prepare(
1568
+ "SELECT id, wav_path AS wavPath, started_at_utc AS startedAtUtc, ended_at_utc AS endedAtUtc, channel, device, reason, created_at_utc AS createdAtUtc FROM pending_chunks ORDER BY created_at_utc ASC, id ASC"
1569
+ ).all() : this.#db.prepare(
1570
+ "SELECT id, wav_path AS wavPath, started_at_utc AS startedAtUtc, ended_at_utc AS endedAtUtc, channel, device, reason, created_at_utc AS createdAtUtc FROM pending_chunks WHERE reason = ? ORDER BY created_at_utc ASC, id ASC"
1571
+ ).all(reason);
1572
+ return rows;
1573
+ }
1574
+ deletePendingChunk(id) {
1575
+ this.#db.prepare("DELETE FROM pending_chunks WHERE id = ?").run(id);
1576
+ }
1507
1577
  stats() {
1508
1578
  const count = (table) => this.#db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
1509
1579
  return { conversations: count("conversations"), segments: count("segments"), chunks: count("chunks") };
@@ -2290,7 +2360,6 @@ function transcriptManifestKey(chunkId) {
2290
2360
  function chunkStableId(event) {
2291
2361
  return `chk_${createHash2("sha1").update(event.path).digest("hex")}`;
2292
2362
  }
2293
- var MAX_BUFFERED_CHUNKS = 512;
2294
2363
  function instantMs(value, field) {
2295
2364
  const ms = Date.parse(value);
2296
2365
  if (Number.isNaN(ms)) {
@@ -2314,7 +2383,7 @@ function lastEndMs(event, raw) {
2314
2383
  }
2315
2384
  function createChunkProcessor(deps) {
2316
2385
  let tail = Promise.resolve();
2317
- let recovered = false;
2386
+ const failCounts = /* @__PURE__ */ new Map();
2318
2387
  let openConversationId = null;
2319
2388
  const retainedChunks = /* @__PURE__ */ new Map();
2320
2389
  function isHeldForReplay(conversationId) {
@@ -2397,6 +2466,17 @@ function createChunkProcessor(deps) {
2397
2466
  } catch {
2398
2467
  }
2399
2468
  };
2469
+ function persistPending(entry, reason) {
2470
+ deps.spool.recordPendingChunk({
2471
+ id: entry.chunkId,
2472
+ wavPath: entry.event.path,
2473
+ startedAtUtc: entry.event.startedAtUtc,
2474
+ endedAtUtc: entry.event.endedAtUtc,
2475
+ channel: entry.event.channel,
2476
+ device: entry.event.device,
2477
+ reason
2478
+ });
2479
+ }
2400
2480
  async function process2(event) {
2401
2481
  const chunkId = chunkStableId(event);
2402
2482
  if (processedThisRun.has(chunkId) || bufferedIds.has(chunkId)) return;
@@ -2437,6 +2517,7 @@ function createChunkProcessor(deps) {
2437
2517
  const evicted = buffer.shift();
2438
2518
  if (evicted !== void 0) {
2439
2519
  bufferedIds.delete(evicted.chunkId);
2520
+ persistPending(evicted, "evicted");
2440
2521
  report(
2441
2522
  new Error(
2442
2523
  `reorder buffer is full (${MAX_BUFFERED_CHUNKS}); chunk ${evicted.chunkId} was dropped with its raw audio retained`
@@ -2466,19 +2547,18 @@ function createChunkProcessor(deps) {
2466
2547
  return built;
2467
2548
  }
2468
2549
  async function applyBatch(batch, progress) {
2469
- if (!recovered) {
2470
- recovered = true;
2471
- const ownPrefix = batch.flatMap((entry) => deps.spool.conversationIdsForChunk(entry.chunkId)).map((id) => deps.spool.capturingConversationById(id)).find((conversation) => conversation !== null);
2472
- const prior = ownPrefix ?? deps.spool.latestCapturingConversation();
2473
- if (prior) {
2474
- deps.assembler.resume(prior);
2475
- openConversationId = prior.id;
2476
- }
2477
- const capturingAtStartup = new Set(deps.spool.capturingConversationIds());
2478
- for (const chunkId of deps.spool.incompleteChunkIds()) {
2479
- const held = heldFor(chunkId, capturingAtStartup);
2480
- if (held.size > 0) retainedChunks.set(chunkId, held);
2481
- }
2550
+ deps.assembler.rewind([]);
2551
+ openConversationId = null;
2552
+ const ownPrefix = batch.flatMap((entry) => deps.spool.conversationIdsForChunk(entry.chunkId)).map((id) => deps.spool.capturingConversationById(id)).find((conversation) => conversation !== null);
2553
+ const prior = ownPrefix ?? deps.spool.latestCapturingConversation();
2554
+ if (prior) {
2555
+ deps.assembler.resume(prior);
2556
+ openConversationId = prior.id;
2557
+ }
2558
+ const capturingNow = new Set(deps.spool.capturingConversationIds());
2559
+ for (const chunkId of deps.spool.incompleteChunkIds()) {
2560
+ const held = heldFor(chunkId, capturingNow);
2561
+ if (held.size > 0) retainedChunks.set(chunkId, held);
2482
2562
  }
2483
2563
  const resumable = resumableConversations();
2484
2564
  for (const entry of batch) trackRetention(entry.chunkId, !isFullyProcessed(entry), resumable);
@@ -2512,7 +2592,11 @@ function createChunkProcessor(deps) {
2512
2592
  );
2513
2593
  if (deps.embed) {
2514
2594
  for (const { entry, item } of fresh) {
2515
- item.seg.embedding = await deps.embed(entry.event, item.raw);
2595
+ try {
2596
+ item.seg.embedding = await deps.embed(entry.event, item.raw);
2597
+ } catch (error) {
2598
+ throw new ChunkApplyError(entry.chunkId, error);
2599
+ }
2516
2600
  }
2517
2601
  }
2518
2602
  const runs = [];
@@ -2530,16 +2614,20 @@ function createChunkProcessor(deps) {
2530
2614
  if (openConversationId !== null && openConversationId !== run.id) {
2531
2615
  if (finalizeConv(openConversationId)) progress.persisted = true;
2532
2616
  }
2533
- deps.spool.appendAssembledSegments({
2534
- idempotencyKey: key,
2535
- chunkId: key,
2536
- conversationId: run.id,
2537
- startedAtUtc: run.startedAtUtc,
2538
- state: "capturing",
2539
- device: event.device,
2540
- wavPath: event.path,
2541
- segments: [item.seg]
2542
- });
2617
+ try {
2618
+ deps.spool.appendAssembledSegments({
2619
+ idempotencyKey: key,
2620
+ chunkId: key,
2621
+ conversationId: run.id,
2622
+ startedAtUtc: run.startedAtUtc,
2623
+ state: "capturing",
2624
+ device: event.device,
2625
+ wavPath: event.path,
2626
+ segments: [item.seg]
2627
+ });
2628
+ } catch (error) {
2629
+ throw new ChunkApplyError(chunkId, error);
2630
+ }
2543
2631
  progress.persisted = true;
2544
2632
  openConversationId = run.id;
2545
2633
  }
@@ -2555,6 +2643,8 @@ function createChunkProcessor(deps) {
2555
2643
  processedThisRun.add(entry.chunkId);
2556
2644
  if (!fullyProcessed) continue;
2557
2645
  deps.spool.markChunkComplete(entry.chunkId, openConversationId ?? "-");
2646
+ deps.spool.deletePendingChunk(entry.chunkId);
2647
+ failCounts.delete(entry.chunkId);
2558
2648
  try {
2559
2649
  await deps.cleanupRawAudio(entry.event);
2560
2650
  } catch (err) {
@@ -2591,11 +2681,12 @@ function createChunkProcessor(deps) {
2591
2681
  let finalFailure;
2592
2682
  for (; ; ) {
2593
2683
  const threshold = flushAll ? Number.POSITIVE_INFINITY : watermarkSourceMs - reorderWindowMs;
2684
+ const held = buffer.filter((entry) => entry.endMs > threshold);
2594
2685
  const batch = [];
2595
2686
  let manifestBlocked = false;
2596
2687
  for (let i = buffer.length - 1; i >= 0; i--) {
2597
2688
  const candidate = buffer[i];
2598
- if (candidate.endMs > threshold) continue;
2689
+ if (!isReleaseEligible(candidate, threshold, held)) continue;
2599
2690
  const manifest = candidate.manifestRecorded ? true : recordTranscriptManifest(candidate);
2600
2691
  if (manifest === "conflict") {
2601
2692
  buffer.splice(i, 1);
@@ -2629,6 +2720,7 @@ function createChunkProcessor(deps) {
2629
2720
  }
2630
2721
  if (batch.length === 0) {
2631
2722
  if (flushAll && buffer.length > 0) {
2723
+ for (const entry of buffer) persistPending(entry, "evicted");
2632
2724
  throw new Error("buffered chunks could not be released; they are retained for replay");
2633
2725
  }
2634
2726
  return;
@@ -2636,28 +2728,38 @@ function createChunkProcessor(deps) {
2636
2728
  batch.sort(
2637
2729
  (left, right) => left.startMs - right.startMs || left.endMs - right.endMs || (left.chunkId < right.chunkId ? -1 : left.chunkId > right.chunkId ? 1 : 0)
2638
2730
  );
2639
- const checkpoint = {
2640
- assembler: deps.assembler.checkpoint(),
2641
- recovered,
2642
- openConversationId
2643
- };
2644
2731
  const progress = { persisted: false };
2645
2732
  try {
2646
2733
  await applyBatch(batch, progress);
2647
2734
  } catch (error) {
2648
- if (!progress.persisted) {
2649
- deps.assembler.rewind(checkpoint.assembler);
2650
- recovered = checkpoint.recovered;
2651
- openConversationId = checkpoint.openConversationId;
2735
+ deps.assembler.rewind([]);
2736
+ openConversationId = null;
2737
+ let quarantinedId;
2738
+ if (error instanceof ChunkApplyError) {
2739
+ const failures = (failCounts.get(error.chunkId) ?? 0) + 1;
2740
+ failCounts.set(error.chunkId, failures);
2741
+ if (failures >= QUARANTINE_AFTER_FAILURES) {
2742
+ const poisoned = batch.find((entry) => entry.chunkId === error.chunkId);
2743
+ if (poisoned !== void 0) {
2744
+ persistPending(poisoned, "quarantined");
2745
+ retainedChunks.delete(poisoned.chunkId);
2746
+ quarantinedId = poisoned.chunkId;
2747
+ }
2748
+ }
2652
2749
  }
2653
2750
  for (const entry of batch) {
2654
2751
  if (processedThisRun.has(entry.chunkId) || bufferedIds.has(entry.chunkId)) continue;
2752
+ if (entry.chunkId === quarantinedId) continue;
2655
2753
  buffer.push(entry);
2656
2754
  bufferedIds.add(entry.chunkId);
2657
2755
  }
2658
- if (flushAll) finalFailure ??= error;
2756
+ if (flushAll) {
2757
+ for (const entry of buffer) persistPending(entry, "evicted");
2758
+ finalFailure ??= error;
2759
+ }
2659
2760
  report(error, batch[0].event);
2660
2761
  if (finalFailure !== void 0) throw finalFailure;
2762
+ if (quarantinedId !== void 0) continue;
2661
2763
  return;
2662
2764
  }
2663
2765
  deps.assembler.pruneFinalized();
@@ -2699,6 +2801,65 @@ function createChunkProcessor(deps) {
2699
2801
  return { enqueue, drain, finalize };
2700
2802
  }
2701
2803
 
2804
+ // src/orphan-scan.ts
2805
+ import { existsSync as existsSync2, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "fs";
2806
+ import path8 from "path";
2807
+ var DEFAULT_CHUNK_MS = 3e4;
2808
+ function scanOrphanedChunks(input) {
2809
+ const recovered = /* @__PURE__ */ new Map();
2810
+ const quarantined = new Set(input.spool.listPendingChunks("quarantined").map((row) => row.id));
2811
+ for (const row of input.spool.listPendingChunks("evicted")) {
2812
+ if (quarantined.has(row.id)) continue;
2813
+ if (input.spool.isChunkApplied(`${row.id}:done`)) continue;
2814
+ if (!existsSync2(row.wavPath)) continue;
2815
+ recovered.set(row.id, {
2816
+ path: row.wavPath,
2817
+ channel: row.channel,
2818
+ startedAtUtc: row.startedAtUtc,
2819
+ endedAtUtc: row.endedAtUtc,
2820
+ device: row.device
2821
+ });
2822
+ }
2823
+ let root;
2824
+ try {
2825
+ root = lstatSync3(input.rawDirectory);
2826
+ } catch (error) {
2827
+ if (error.code === "ENOENT") {
2828
+ return [...recovered.values()].sort(byStart);
2829
+ }
2830
+ throw error;
2831
+ }
2832
+ if (root.isSymbolicLink() || !root.isDirectory()) return [...recovered.values()].sort(byStart);
2833
+ for (const name of readdirSync2(input.rawDirectory)) {
2834
+ if (!name.endsWith(".wav")) continue;
2835
+ const location = path8.join(input.rawDirectory, name);
2836
+ let stat;
2837
+ try {
2838
+ stat = lstatSync3(location);
2839
+ } catch (error) {
2840
+ if (error.code === "ENOENT") continue;
2841
+ throw error;
2842
+ }
2843
+ if (!stat.isFile()) continue;
2844
+ const event = {
2845
+ path: location,
2846
+ channel: "mic",
2847
+ startedAtUtc: new Date(stat.mtimeMs - DEFAULT_CHUNK_MS).toISOString(),
2848
+ endedAtUtc: new Date(stat.mtimeMs).toISOString(),
2849
+ device: null
2850
+ };
2851
+ const id = chunkStableId(event);
2852
+ if (quarantined.has(id) || recovered.has(id)) continue;
2853
+ if (input.spool.isChunkApplied(`${id}:done`)) continue;
2854
+ recovered.set(id, event);
2855
+ }
2856
+ return [...recovered.values()].sort(byStart);
2857
+ }
2858
+ function byStart(left, right) {
2859
+ if (left.startedAtUtc !== right.startedAtUtc) return left.startedAtUtc < right.startedAtUtc ? -1 : 1;
2860
+ return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
2861
+ }
2862
+
2702
2863
  // src/stt.ts
2703
2864
  import { statSync as statSync2 } from "fs";
2704
2865
  import { spawn } from "child_process";
@@ -2814,7 +2975,7 @@ function runWhisperCli(command, args) {
2814
2975
  // src/capture.ts
2815
2976
  import { realpathSync } from "fs";
2816
2977
  import { rm as rm3 } from "fs/promises";
2817
- import path8 from "path";
2978
+ import path9 from "path";
2818
2979
  function createLiveCapture(options) {
2819
2980
  const { spool, config, outDir, defaultModelPath } = options;
2820
2981
  const resolveModel = options.resolveModel ?? (() => resolveModelPath(config.stt.modelPath ?? void 0, defaultModelPath));
@@ -2825,23 +2986,23 @@ function createLiveCapture(options) {
2825
2986
  threads: config.stt.threads,
2826
2987
  run: runWhisperCli
2827
2988
  }));
2828
- const rawBase = path8.resolve(outDir);
2989
+ const rawBase = path9.resolve(outDir);
2829
2990
  const realOrResolved = (p) => {
2830
2991
  try {
2831
- return realpathSync(path8.resolve(p));
2992
+ return realpathSync(path9.resolve(p));
2832
2993
  } catch {
2833
- return path8.resolve(p);
2994
+ return path9.resolve(p);
2834
2995
  }
2835
2996
  };
2836
2997
  const rawBaseReal = realOrResolved(rawBase);
2837
2998
  const withinRawDir = (p) => {
2838
2999
  const real = realOrResolved(p);
2839
- return real === rawBaseReal || real.startsWith(rawBaseReal + path8.sep);
3000
+ return real === rawBaseReal || real.startsWith(rawBaseReal + path9.sep);
2840
3001
  };
2841
3002
  const cleanupRawAudio = options.cleanupRawAudio ?? (async (event) => {
2842
3003
  if (config.rawRetentionHours > 0) return;
2843
3004
  if (!withinRawDir(event.path)) return;
2844
- await rm3(path8.resolve(event.path), { force: true });
3005
+ await rm3(path9.resolve(event.path), { force: true });
2845
3006
  });
2846
3007
  const assembler = new ConversationAssembler({
2847
3008
  gapMinutes: config.conversationGapMinutes,
@@ -2881,7 +3042,7 @@ function createLiveCapture(options) {
2881
3042
  options.onError?.(new CaptureInputError(`native helper chunk path escapes the capture directory: ${event.path}`));
2882
3043
  return;
2883
3044
  }
2884
- processor.enqueue({ ...event, path: path8.resolve(event.path) });
3045
+ processor.enqueue({ ...event, path: path9.resolve(event.path) });
2885
3046
  },
2886
3047
  ...options.onError ? { onError: options.onError } : {},
2887
3048
  ...options.onStderr ? { onStderr: options.onStderr } : {},
@@ -2898,6 +3059,9 @@ function createLiveCapture(options) {
2898
3059
  processor,
2899
3060
  start() {
2900
3061
  resolveModel();
3062
+ for (const event of scanOrphanedChunks({ rawDirectory: outDir, spool })) {
3063
+ processor.enqueue(event);
3064
+ }
2901
3065
  runner.start();
2902
3066
  },
2903
3067
  async stop() {
@@ -2943,7 +3107,7 @@ function enrollSelf(input) {
2943
3107
  }
2944
3108
 
2945
3109
  // src/service.ts
2946
- import path9 from "path";
3110
+ import path10 from "path";
2947
3111
  var DEFAULT_SERVICE_LABEL = "com.remnic.capture-audio";
2948
3112
  var SYSTEMD_UNIT_NAME = "remnic-capture-audio.service";
2949
3113
  function xmlEscape(value) {
@@ -3024,7 +3188,7 @@ WantedBy=default.target
3024
3188
  function planService(deps) {
3025
3189
  const label = validateLabel(deps.spec.label ?? DEFAULT_SERVICE_LABEL);
3026
3190
  if (deps.platform === "darwin") {
3027
- const target = path9.join(deps.home, "Library", "LaunchAgents", `${label}.plist`);
3191
+ const target = path10.join(deps.home, "Library", "LaunchAgents", `${label}.plist`);
3028
3192
  return {
3029
3193
  platform: deps.platform,
3030
3194
  path: target,
@@ -3034,7 +3198,7 @@ function planService(deps) {
3034
3198
  }
3035
3199
  if (deps.platform === "linux") {
3036
3200
  const unitName = deps.spec.label ? `${label}.service` : SYSTEMD_UNIT_NAME;
3037
- const target = path9.join(deps.home, ".config", "systemd", "user", unitName);
3201
+ const target = path10.join(deps.home, ".config", "systemd", "user", unitName);
3038
3202
  return {
3039
3203
  platform: deps.platform,
3040
3204
  path: target,
@@ -3049,7 +3213,7 @@ function installService(deps) {
3049
3213
  if (!deps.force && deps.exists?.(plan.path)) {
3050
3214
  throw new CaptureConfigError(`a capture-audio service is already installed at ${plan.path} (use --force to replace)`);
3051
3215
  }
3052
- deps.mkdir(path9.dirname(plan.path));
3216
+ deps.mkdir(path10.dirname(plan.path));
3053
3217
  deps.writeFile(plan.path, plan.contents);
3054
3218
  return plan;
3055
3219
  }
@@ -3062,8 +3226,8 @@ function uninstallService(deps) {
3062
3226
 
3063
3227
  // src/cli.ts
3064
3228
  import { spawn as spawn2 } from "child_process";
3065
- import { chmodSync as chmodSync3, existsSync as existsSync2, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
3066
- import path10 from "path";
3229
+ import { chmodSync as chmodSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
3230
+ import path11 from "path";
3067
3231
  import { setTimeout as delay } from "timers/promises";
3068
3232
  import { homedir } from "os";
3069
3233
  var VALUE_FLAGS = {
@@ -3130,7 +3294,7 @@ function resolvePaths(flags, env) {
3130
3294
  return capturePaths(baseDir);
3131
3295
  }
3132
3296
  function loadConfigOrDefault(paths, stderr) {
3133
- if (existsSync2(paths.configPath)) return loadDaemonConfig(paths.configPath);
3297
+ if (existsSync3(paths.configPath)) return loadDaemonConfig(paths.configPath);
3134
3298
  stderr(`no config at ${paths.configPath}; using defaults (run \`init\` to customize)`);
3135
3299
  return defaultDaemonConfig();
3136
3300
  }
@@ -3158,7 +3322,7 @@ function recordHealthUrl(record, paths, stderr) {
3158
3322
  return healthUrlFor(config.host, config.port);
3159
3323
  }
3160
3324
  function tokenHeader(paths) {
3161
- if (!existsSync2(paths.tokenPath)) return {};
3325
+ if (!existsSync3(paths.tokenPath)) return {};
3162
3326
  return { authorization: `Bearer ${readFileSync6(paths.tokenPath, "utf8").trim()}` };
3163
3327
  }
3164
3328
  function ensurePrivateDir(dir) {
@@ -3237,7 +3401,7 @@ async function superviseReplay(spool, replayDir, io, signal) {
3237
3401
  }
3238
3402
  function cmdInit(paths, flags, stdout) {
3239
3403
  ensurePrivateDir(paths.baseDir);
3240
- if (existsSync2(paths.configPath) && flags.force !== true) {
3404
+ if (existsSync3(paths.configPath) && flags.force !== true) {
3241
3405
  stdout(`config already exists at ${paths.configPath} (use --force to overwrite)`);
3242
3406
  } else {
3243
3407
  writeFileSync3(paths.configPath, serializeDaemonConfig(defaultDaemonConfig()), "utf8");
@@ -3250,13 +3414,13 @@ function cmdInit(paths, flags, stdout) {
3250
3414
  }
3251
3415
  async function cmdDownloadModel(paths, flags, stdout, downloadModel) {
3252
3416
  if (typeof flags.model !== "string") throw new CaptureInputError("flag --model requires a value");
3253
- const result = await downloadModel({ model: flags.model, directory: path10.join(paths.baseDir, "models") });
3417
+ const result = await downloadModel({ model: flags.model, directory: path11.join(paths.baseDir, "models") });
3254
3418
  stdout(`${result.downloaded ? "downloaded" : "model already present"} ${flags.model} to ${result.path}`);
3255
3419
  return 0;
3256
3420
  }
3257
3421
  async function cmdJanitor(paths, stdout, stderr) {
3258
3422
  const config = loadConfigOrDefault(paths, stderr);
3259
- const removed = await pruneExpiredRawAudio(path10.join(paths.baseDir, "raw"), config.rawRetentionHours * 60 * 60 * 1e3);
3423
+ const removed = await pruneExpiredRawAudio(path11.join(paths.baseDir, "raw"), config.rawRetentionHours * 60 * 60 * 1e3);
3260
3424
  stdout(`janitor: removed ${removed.length} expired raw audio file(s)`);
3261
3425
  return 0;
3262
3426
  }
@@ -3333,13 +3497,13 @@ async function cmdStart(paths, flags, env, stdout, stderr, spawnArgvPrefix) {
3333
3497
  let live = null;
3334
3498
  if (flags.capture === true) {
3335
3499
  try {
3336
- const rawDir = path10.join(paths.baseDir, "raw");
3500
+ const rawDir = path11.join(paths.baseDir, "raw");
3337
3501
  mkdirSync3(rawDir, { recursive: true });
3338
3502
  live = createLiveCapture({
3339
3503
  spool,
3340
3504
  config,
3341
3505
  outDir: rawDir,
3342
- defaultModelPath: path10.join(paths.baseDir, "models", "ggml-base.bin"),
3506
+ defaultModelPath: path11.join(paths.baseDir, "models", "ggml-base.bin"),
3343
3507
  onError: (e) => stderr(`capture: ${describeError(e)}`),
3344
3508
  onStderr: (l) => stderr(`helper: ${l}`)
3345
3509
  });
@@ -3504,7 +3668,7 @@ function cmdInstallService(paths, flags, env, stdout, spawnArgvPrefix) {
3504
3668
  platform,
3505
3669
  home,
3506
3670
  spec,
3507
- exists: existsSync2,
3671
+ exists: existsSync3,
3508
3672
  remove: (f) => rmSync2(f, { force: true })
3509
3673
  });
3510
3674
  stdout(removed ? `removed ${plan2.path}` : `no capture-audio service installed at ${plan2.path}`);
@@ -3516,7 +3680,7 @@ function cmdInstallService(paths, flags, env, stdout, spawnArgvPrefix) {
3516
3680
  home,
3517
3681
  spec,
3518
3682
  force: flags.force === true,
3519
- exists: existsSync2,
3683
+ exists: existsSync3,
3520
3684
  mkdir: (dir) => mkdirSync3(dir, { recursive: true }),
3521
3685
  writeFile: (file, contents) => writeFileSync3(file, contents, { mode: 420 })
3522
3686
  });
@@ -3540,7 +3704,7 @@ function cmdEnrollSelf(paths, flags, stdout) {
3540
3704
  }
3541
3705
  }
3542
3706
  function cmdLogs(paths, flags, stdout) {
3543
- if (!existsSync2(paths.logPath)) {
3707
+ if (!existsSync3(paths.logPath)) {
3544
3708
  stdout(`no log file at ${paths.logPath}`);
3545
3709
  return 0;
3546
3710
  }
@@ -3666,6 +3830,8 @@ export {
3666
3830
  whisperModelUrl,
3667
3831
  downloadWhisperModel,
3668
3832
  pruneExpiredRawAudio,
3833
+ MAX_BUFFERED_CHUNKS,
3834
+ QUARANTINE_AFTER_FAILURES,
3669
3835
  Spool,
3670
3836
  assembleConversations,
3671
3837
  DEFAULT_CONVERSATION_GAP_MINUTES,
@@ -3683,6 +3849,7 @@ export {
3683
3849
  dedupeCrossChannel,
3684
3850
  chunkStableId,
3685
3851
  createChunkProcessor,
3852
+ scanOrphanedChunks,
3686
3853
  parseWhisperJson,
3687
3854
  resolveModelPath,
3688
3855
  buildWhisperArgs,
@@ -3700,4 +3867,4 @@ export {
3700
3867
  superviseReplay,
3701
3868
  runCapture
3702
3869
  };
3703
- //# sourceMappingURL=chunk-MFWH245M.js.map
3870
+ //# sourceMappingURL=chunk-4Z3DZYEB.js.map