@remnic/core 9.3.645 → 9.3.647

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 (98) hide show
  1. package/dist/access-cli.js +18 -17
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.js +13 -13
  4. package/dist/access-mcp.js +10 -10
  5. package/dist/access-schema.js +3 -3
  6. package/dist/access-service.js +8 -8
  7. package/dist/adapters/index.js +4 -4
  8. package/dist/adapters/registry.js +2 -2
  9. package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
  10. package/dist/{chunk-6YLJZ2EU.js → chunk-3D6L7CEP.js} +92 -29
  11. package/dist/chunk-3D6L7CEP.js.map +1 -0
  12. package/dist/{chunk-3SF7QNKF.js → chunk-6BNFVP7Y.js} +2 -2
  13. package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
  14. package/dist/{chunk-HRXCPKN4.js → chunk-APWJRJFW.js} +2 -2
  15. package/dist/{chunk-VDX2J7OX.js → chunk-BUKK5SWA.js} +2 -2
  16. package/dist/{chunk-T22TZJWJ.js → chunk-DC66QVL2.js} +2 -2
  17. package/dist/chunk-EVWIEEKZ.js +315 -0
  18. package/dist/chunk-EVWIEEKZ.js.map +1 -0
  19. package/dist/{chunk-IZXOO6NP.js → chunk-FAYDM5WD.js} +2 -2
  20. package/dist/{chunk-Y2XKJJEI.js → chunk-IMWFHBG2.js} +10 -33
  21. package/dist/chunk-IMWFHBG2.js.map +1 -0
  22. package/dist/{chunk-W5ISYSIL.js → chunk-L7W5YW6Y.js} +4 -4
  23. package/dist/{chunk-DGNQRNLL.js → chunk-NT5TINK5.js} +2 -2
  24. package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
  25. package/dist/{chunk-CDHRUNBX.js → chunk-QKE4LHNR.js} +2 -2
  26. package/dist/{chunk-BTRL6GG7.js → chunk-RAELB5NX.js} +5 -5
  27. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  28. package/dist/chunk-S4DDLTPX.js +140 -0
  29. package/dist/chunk-S4DDLTPX.js.map +1 -0
  30. package/dist/{chunk-MBT3ASYV.js → chunk-TA4LQ5SR.js} +12 -12
  31. package/dist/{chunk-KVUG7FF7.js → chunk-U55D5UD5.js} +7 -7
  32. package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
  33. package/dist/{chunk-D2B22JDF.js → chunk-WEPMT6SC.js} +7 -7
  34. package/dist/{chunk-IHG27THY.js → chunk-XUGQQPGO.js} +77 -61
  35. package/dist/chunk-XUGQQPGO.js.map +1 -0
  36. package/dist/{chunk-YOXCJP3A.js → chunk-ZPPFKVSD.js} +3 -3
  37. package/dist/cli.js +30 -28
  38. package/dist/{first-start-migration-FF7YFGRP.js → first-start-migration-PG5HBC3K.js} +4 -4
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +77 -63
  41. package/dist/index.js.map +1 -1
  42. package/dist/lcm/engine.js +3 -3
  43. package/dist/lcm/index.js +11 -11
  44. package/dist/namespaces/migrate.js +5 -5
  45. package/dist/namespaces/search.js +4 -4
  46. package/dist/operator-toolkit.js +6 -6
  47. package/dist/orchestrator.js +14 -13
  48. package/dist/resume-bundles.js +3 -2
  49. package/dist/schemas.d.ts +22 -22
  50. package/dist/search/factory.js +3 -3
  51. package/dist/search/index.js +7 -7
  52. package/dist/session-identity.d.ts +118 -0
  53. package/dist/session-identity.js +21 -0
  54. package/dist/session-identity.js.map +1 -0
  55. package/dist/session-transcript-migration.d.ts +65 -0
  56. package/dist/session-transcript-migration.js +16 -0
  57. package/dist/session-transcript-migration.js.map +1 -0
  58. package/dist/summarizer.js +2 -1
  59. package/dist/transcript.d.ts +18 -6
  60. package/dist/transcript.js +2 -1
  61. package/dist/transfer/backup.js +2 -2
  62. package/dist/transfer/capsule-export.js +2 -2
  63. package/dist/transfer/capsule-import.js +2 -2
  64. package/dist/transfer/import-sqlite.js +2 -2
  65. package/dist/transfer/types.d.ts +12 -12
  66. package/package.json +1 -1
  67. package/src/cli.ts +79 -0
  68. package/src/index.ts +18 -0
  69. package/src/session-identity.test.ts +130 -0
  70. package/src/session-identity.ts +281 -0
  71. package/src/session-transcript-migration.test.ts +350 -0
  72. package/src/session-transcript-migration.ts +527 -0
  73. package/src/summarizer.ts +15 -35
  74. package/src/transcript-session-identity.test.ts +424 -0
  75. package/src/transcript.test.ts +110 -0
  76. package/src/transcript.ts +121 -73
  77. package/dist/chunk-6YLJZ2EU.js.map +0 -1
  78. package/dist/chunk-IHG27THY.js.map +0 -1
  79. package/dist/chunk-Y2XKJJEI.js.map +0 -1
  80. /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  81. /package/dist/{chunk-3SF7QNKF.js.map → chunk-6BNFVP7Y.js.map} +0 -0
  82. /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
  83. /package/dist/{chunk-HRXCPKN4.js.map → chunk-APWJRJFW.js.map} +0 -0
  84. /package/dist/{chunk-VDX2J7OX.js.map → chunk-BUKK5SWA.js.map} +0 -0
  85. /package/dist/{chunk-T22TZJWJ.js.map → chunk-DC66QVL2.js.map} +0 -0
  86. /package/dist/{chunk-IZXOO6NP.js.map → chunk-FAYDM5WD.js.map} +0 -0
  87. /package/dist/{chunk-W5ISYSIL.js.map → chunk-L7W5YW6Y.js.map} +0 -0
  88. /package/dist/{chunk-DGNQRNLL.js.map → chunk-NT5TINK5.js.map} +0 -0
  89. /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
  90. /package/dist/{chunk-CDHRUNBX.js.map → chunk-QKE4LHNR.js.map} +0 -0
  91. /package/dist/{chunk-BTRL6GG7.js.map → chunk-RAELB5NX.js.map} +0 -0
  92. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  93. /package/dist/{chunk-MBT3ASYV.js.map → chunk-TA4LQ5SR.js.map} +0 -0
  94. /package/dist/{chunk-KVUG7FF7.js.map → chunk-U55D5UD5.js.map} +0 -0
  95. /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
  96. /package/dist/{chunk-D2B22JDF.js.map → chunk-WEPMT6SC.js.map} +0 -0
  97. /package/dist/{chunk-YOXCJP3A.js.map → chunk-ZPPFKVSD.js.map} +0 -0
  98. /package/dist/{first-start-migration-FF7YFGRP.js.map → first-start-migration-PG5HBC3K.js.map} +0 -0
package/src/transcript.ts CHANGED
@@ -3,13 +3,8 @@ import path from "node:path";
3
3
  import { log } from "./logger.js";
4
4
  import type { TranscriptEntry, Checkpoint, PluginConfig } from "./types.js";
5
5
  import { analyzeSessionIntegrity, type SessionIntegrityReport } from "./session-integrity.js";
6
- import {
7
- encodeStoragePathSegment,
8
- encodeStoragePathSegmentWithHash,
9
- isSafeLegacyPathSegment,
10
- resolveSafeStoragePath,
11
- storagePathHash,
12
- } from "./storage-paths.js";
6
+ import { resolveSafeStoragePath } from "./storage-paths.js";
7
+ import { sessionStoragePaths } from "./session-identity.js";
13
8
 
14
9
  type DirectorySessionStatus = "missing" | "empty" | "matches" | "occupied";
15
10
  type DirectoryOwnershipCacheEntry = {
@@ -17,16 +12,25 @@ type DirectoryOwnershipCacheEntry = {
17
12
  fileSizes: Map<string, number>;
18
13
  };
19
14
 
20
- function legacyTranscriptDirFor(
21
- channelType: string,
22
- channelId: string,
23
- encodedDir: string,
24
- ): string | undefined {
25
- if (!isSafeLegacyPathSegment(channelType) || !isSafeLegacyPathSegment(channelId)) {
26
- return undefined;
27
- }
28
- const legacyDir = path.join(channelType, channelId);
29
- return legacyDir === encodedDir ? undefined : legacyDir;
15
+ /**
16
+ * De-duplicate JSONL lines collected across multiple read-back candidate
17
+ * directories. A partially-applied migration (issue #1496) can leave the SAME
18
+ * raw row in both the primary `session/<hash>` tree and a legacy read-back dir
19
+ * (`other/default` or an old `parts.length >= 3` directory) when the source was
20
+ * copied but not yet trimmed. Without this guard, `readRecent`, `readToolUse`,
21
+ * footprint estimation, and the summarizer fetch would return that row twice
22
+ * (cursor review on PR #1504). The exact raw JSONL line is the stable identity:
23
+ * the migration preserves byte content per session, so a copied-but-not-trimmed
24
+ * row is byte-identical in both locations. Returns `true` the first time a line
25
+ * is seen, `false` for every subsequent duplicate.
26
+ */
27
+ function makeRawLineDeduper(): (rawLine: string) => boolean {
28
+ const seen = new Set<string>();
29
+ return (rawLine: string): boolean => {
30
+ if (seen.has(rawLine)) return false;
31
+ seen.add(rawLine);
32
+ return true;
33
+ };
30
34
  }
31
35
 
32
36
  /**
@@ -64,13 +68,17 @@ export class TranscriptManager {
64
68
  }
65
69
 
66
70
  /**
67
- * Parse a sessionKey to extract channel type and ID.
71
+ * Resolve the storage path pieces for a sessionKey via the shared
72
+ * {@link sessionStoragePaths} helper (issue #1496, rule #22).
68
73
  *
69
- * SessionKey patterns:
70
- * - agent:<agent-id>:main type="main", id="default"
71
- * - agent:<agent-id>:discord:channel:<channel-id> type="discord", id="<channel-id>"
72
- * - agent:<agent-id>:cron:<job-id> type="cron", id="<job-id>"
73
- * - agent:<agent-id>:slack:channel:<channel-id> type="slack", id="<channel-id>"
74
+ * Legacy `agent:<id>:...` shapes keep their existing readable channel paths.
75
+ * Arbitrary / non-legacy keys (e.g. `pi-geek:abc123`) route to
76
+ * `transcripts/session/<hash>/YYYY-MM-DD.jsonl` from the FIRST write — they
77
+ * never start life in `other/default`. For those keys a list of
78
+ * read-back-only `readbackDirs` is returned so data written by older builds
79
+ * (under `other/default`, or under an old `parts.length >= 3` directory such
80
+ * as `baz/default`) stays discoverable until migrated. New writes never
81
+ * target read-back dirs. See `session-identity.ts`.
74
82
  *
75
83
  * @returns Object with raw channel identifiers and encoded storage path pieces.
76
84
  */
@@ -81,49 +89,21 @@ export class TranscriptManager {
81
89
  channelId: string;
82
90
  alternateDir: string;
83
91
  legacyDir?: string;
92
+ readbackDirs: string[];
84
93
  } {
85
- const parts = sessionKey.split(":");
86
-
87
- // Default fallback
88
- let channelType = "other";
89
- let channelId = "default";
90
-
91
- if (parts.length >= 3) {
92
- // parts[0] = "agent", parts[1] = agent name, parts[2] = channel type
93
- channelType = parts[2];
94
-
95
- // Extract channel ID based on pattern
96
- if (channelType === "main") {
97
- channelId = "default";
98
- } else if (channelType === "discord" && parts.length >= 5 && parts[3] === "channel") {
99
- channelId = parts[4];
100
- } else if (channelType === "slack" && parts.length >= 5 && parts[3] === "channel") {
101
- channelId = parts[4];
102
- } else if (channelType === "cron" && parts.length >= 4) {
103
- channelId = parts[3];
104
- } else if (parts.length >= 4) {
105
- // For other types, use the 4th part as ID if available
106
- channelId = parts[3];
107
- }
108
- }
94
+ const paths = sessionStoragePaths(sessionKey);
109
95
 
110
96
  // Daily rotation: transcripts/{channelType}/{channelId}/YYYY-MM-DD.jsonl
111
97
  const today = new Date().toISOString().slice(0, 10);
112
- const dir = path.join(
113
- encodeStoragePathSegment(channelType),
114
- encodeStoragePathSegment(channelId),
115
- );
116
- const alternateDir = path.join(
117
- encodeStoragePathSegmentWithHash(channelType),
118
- `${encodeStoragePathSegmentWithHash(channelId)}--session-${storagePathHash(sessionKey)}`,
119
- );
98
+
120
99
  return {
121
- dir,
100
+ dir: paths.dir,
122
101
  file: `${today}.jsonl`,
123
- channelType,
124
- channelId,
125
- alternateDir,
126
- legacyDir: legacyTranscriptDirFor(channelType, channelId, dir),
102
+ channelType: paths.channelType,
103
+ channelId: paths.channelId,
104
+ alternateDir: paths.alternateDir,
105
+ legacyDir: paths.legacyDir,
106
+ readbackDirs: paths.readbackDirs,
127
107
  };
128
108
  }
129
109
 
@@ -183,9 +163,16 @@ export class TranscriptManager {
183
163
  file: string;
184
164
  alternateDir: string;
185
165
  legacyDir?: string;
166
+ readbackDirs: string[];
186
167
  } {
187
168
  const p = this.getTranscriptPath(sessionKey);
188
- return { dir: p.dir, file: p.file, alternateDir: p.alternateDir, legacyDir: p.legacyDir };
169
+ return {
170
+ dir: p.dir,
171
+ file: p.file,
172
+ alternateDir: p.alternateDir,
173
+ legacyDir: p.legacyDir,
174
+ readbackDirs: p.readbackDirs,
175
+ };
189
176
  }
190
177
 
191
178
  private async selectStorageDirForWrite(
@@ -344,11 +331,12 @@ export class TranscriptManager {
344
331
  dir: string,
345
332
  legacyDir?: string,
346
333
  alternateDir?: string,
334
+ readbackDirs: string[] = [],
347
335
  ): Promise<Array<{ cacheKey: string; name: string; path: string }>> {
348
336
  const files: Array<{ cacheKey: string; name: string; path: string }> = [];
349
337
  const seenDirs = new Set<string>();
350
338
 
351
- for (const candidateDir of [dir, alternateDir, legacyDir]) {
339
+ for (const candidateDir of [dir, alternateDir, legacyDir, ...readbackDirs]) {
352
340
  if (!candidateDir || seenDirs.has(candidateDir)) continue;
353
341
  seenDirs.add(candidateDir);
354
342
 
@@ -400,10 +388,19 @@ export class TranscriptManager {
400
388
  startTime: Date,
401
389
  endTime: Date,
402
390
  ): Promise<Array<{ timestamp: string; sessionKey: string; tool: string }>> {
403
- const { dir, alternateDir, legacyDir } = this.getToolUsagePath(sessionKey);
391
+ const { dir, alternateDir, legacyDir, readbackDirs } = this.getToolUsagePath(sessionKey);
404
392
  try {
405
- const files = await this.getSessionStorageFiles(this.toolUsageDir, dir, legacyDir, alternateDir);
393
+ const files = await this.getSessionStorageFiles(
394
+ this.toolUsageDir,
395
+ dir,
396
+ legacyDir,
397
+ alternateDir,
398
+ readbackDirs,
399
+ );
406
400
  const out: Array<{ timestamp: string; sessionKey: string; tool: string }> = [];
401
+ // Dedup identical raw rows that a partially-applied migration may have left
402
+ // in both the primary dir and a read-back dir (issue #1496, cursor review).
403
+ const keepRawLine = makeRawLineDeduper();
407
404
  for (const file of files) {
408
405
  const raw = await readFile(file.path, "utf-8");
409
406
  for (const line of raw.split("\n")) {
@@ -414,7 +411,7 @@ export class TranscriptManager {
414
411
  if (!Number.isFinite(ts)) continue;
415
412
  if (ts >= startTime.getTime() && ts < endTime.getTime()) {
416
413
  if (typeof obj.tool === "string" && typeof obj.sessionKey === "string") {
417
- if (obj.sessionKey === sessionKey) {
414
+ if (obj.sessionKey === sessionKey && keepRawLine(line)) {
418
415
  out.push({ timestamp: obj.timestamp, sessionKey: obj.sessionKey, tool: obj.tool });
419
416
  }
420
417
  }
@@ -431,11 +428,24 @@ export class TranscriptManager {
431
428
  }
432
429
 
433
430
  async estimateSessionFootprint(sessionKey: string): Promise<{ bytes: number; tokens: number }> {
434
- const { dir, alternateDir, legacyDir } = this.getTranscriptPath(sessionKey);
431
+ const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);
435
432
  let bytes = 0;
436
433
 
434
+ // NOTE: this is a best-effort byte ESTIMATE for compaction sizing, not an
435
+ // exact row count, and is maintained via an incremental per-file cache. A
436
+ // copied-but-not-trimmed migration window (issue #1496) can transiently
437
+ // over-estimate by counting a duplicated row in both the primary and a
438
+ // read-back dir; that self-heals once the migration trims the source. The
439
+ // exact-once guarantee that matters for callers lives in readRecent /
440
+ // readToolUse / the summarizer fetch, which dedup by raw line.
437
441
  try {
438
- const files = await this.getSessionStorageFiles(this.transcriptsDir, dir, legacyDir, alternateDir);
442
+ const files = await this.getSessionStorageFiles(
443
+ this.transcriptsDir,
444
+ dir,
445
+ legacyDir,
446
+ alternateDir,
447
+ readbackDirs,
448
+ );
439
449
  const cached = this.sessionFootprintCache.get(sessionKey);
440
450
  if (!cached) {
441
451
  const fileBytes = new Map<string, number>();
@@ -636,12 +646,28 @@ export class TranscriptManager {
636
646
  * Read transcript entries for a date range.
637
647
  * Returns entries within the time range, optionally filtered by sessionKey.
638
648
  * Reads from all channel subdirectories in the hierarchical structure.
649
+ *
650
+ * The window is half-open `[start, end)`: the upper bound is exclusive so
651
+ * caller-specified ranges (explicit dates, analytics buckets) don't
652
+ * double-count at shared boundaries (CLAUDE.md rule #35 / AGENTS.md rule 23).
653
+ * readRecent's inclusive-of-now behavior is handled by readRecent itself
654
+ * extending its end, not by relaxing this bound.
639
655
  */
640
656
  async readRange(startTime: string, endTime: string, sessionKey?: string): Promise<TranscriptEntry[]> {
641
657
  const start = new Date(startTime);
642
658
  const end = new Date(endTime);
643
659
  const entries: TranscriptEntry[] = [];
644
660
 
661
+ // When a sessionKey is given, a partially-applied #1496 migration can leave
662
+ // the SAME raw row in both the primary `session/<hash>` dir and a read-back
663
+ // dir (`other/default` or an old `parts.length >= 3` dir). `readRange` scans
664
+ // EVERY transcript file, so it would append that row once per directory.
665
+ // Dedup by exact raw line to give the same exact-once guarantee `readRecent`
666
+ // / `readToolUse` / the summarizer fetch already provide (cursor review on
667
+ // PR #1504). Only applied for the session-scoped path so unfiltered range
668
+ // scans (each file already enumerated once) keep their existing behavior.
669
+ const keepRawLine = sessionKey ? makeRawLineDeduper() : undefined;
670
+
645
671
  try {
646
672
  // Get all transcript files from the hierarchical structure
647
673
  const transcriptFiles = await this.getAllTranscriptFiles();
@@ -658,10 +684,11 @@ export class TranscriptManager {
658
684
  const entry = JSON.parse(line) as TranscriptEntry;
659
685
  const entryTime = new Date(entry.timestamp);
660
686
 
661
- // Check if entry is within time range
687
+ // Check if entry is within time range (half-open: exclusive end)
662
688
  if (entryTime >= start && entryTime < end) {
663
689
  // Filter by sessionKey if provided
664
690
  if (!sessionKey || entry.sessionKey === sessionKey) {
691
+ if (keepRawLine && !keepRawLine(line)) continue;
665
692
  entries.push(entry);
666
693
  }
667
694
  }
@@ -693,8 +720,17 @@ export class TranscriptManager {
693
720
  * specific channel instead of scanning all 95+ transcript files across all channels.
694
721
  */
695
722
  async readRecent(hours: number, sessionKey?: string): Promise<TranscriptEntry[]> {
696
- const end = new Date();
697
- const start = new Date(end.getTime() - hours * 60 * 60 * 1000);
723
+ const now = new Date();
724
+ // The recent-read window is inclusive of "now": a turn that is buffered and
725
+ // then immediately read back can carry the same millisecond timestamp as
726
+ // this read, so a half-open [start, now) filter would drop it. Extend the
727
+ // upper bound by 1ms so the exclusive [start, end) filters in
728
+ // readRecentForSession / readRange (which stay half-open for
729
+ // caller-specified ranges, CLAUDE.md rule #35) still capture an entry
730
+ // stamped at exactly "now". This is the only place the "up to now" boundary
731
+ // is widened; explicit ranges are unaffected.
732
+ const end = new Date(now.getTime() + 1);
733
+ const start = new Date(now.getTime() - hours * 60 * 60 * 1000);
698
734
 
699
735
  if (sessionKey) {
700
736
  return this.readRecentForSession(start, end, sessionKey);
@@ -711,7 +747,7 @@ export class TranscriptManager {
711
747
  end: Date,
712
748
  sessionKey: string,
713
749
  ): Promise<TranscriptEntry[]> {
714
- const { dir, alternateDir, legacyDir } = this.getTranscriptPath(sessionKey);
750
+ const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);
715
751
 
716
752
  // Build set of date strings that overlap with [start, end].
717
753
  // Always include end's date to handle midnight-crossing lookbacks
@@ -725,8 +761,17 @@ export class TranscriptManager {
725
761
  dateStrings.add(end.toISOString().slice(0, 10));
726
762
 
727
763
  const entries: TranscriptEntry[] = [];
728
- const files = await this.getSessionStorageFiles(this.transcriptsDir, dir, legacyDir, alternateDir);
764
+ const files = await this.getSessionStorageFiles(
765
+ this.transcriptsDir,
766
+ dir,
767
+ legacyDir,
768
+ alternateDir,
769
+ readbackDirs,
770
+ );
729
771
 
772
+ // Dedup identical raw rows that a partially-applied migration may have left
773
+ // in both the primary dir and a read-back dir (issue #1496, cursor review).
774
+ const keepRawLine = makeRawLineDeduper();
730
775
  for (const file of files) {
731
776
  // Only read files whose date is within the window
732
777
  const dateStr = file.name.slice(0, 10);
@@ -739,7 +784,10 @@ export class TranscriptManager {
739
784
  try {
740
785
  const entry = JSON.parse(line) as TranscriptEntry;
741
786
  const ts = new Date(entry.timestamp);
742
- if (ts >= start && ts < end && entry.sessionKey === sessionKey) {
787
+ // Half-open window: exclusive end. readRecent widens its own end by
788
+ // 1ms so a just-written "now" entry is still captured here without
789
+ // relaxing this bound for direct callers (tests/transcript-boundary).
790
+ if (ts >= start && ts < end && entry.sessionKey === sessionKey && keepRawLine(line)) {
743
791
  entries.push(entry);
744
792
  }
745
793
  } catch {