@remnic/core 9.3.646 → 9.3.648

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 (65) hide show
  1. package/dist/access-cli.js +10 -10
  2. package/dist/access-http.js +10 -10
  3. package/dist/access-mcp.js +9 -9
  4. package/dist/access-service.js +8 -8
  5. package/dist/{chunk-L7W5YW6Y.js → chunk-5ETA6OAS.js} +2 -2
  6. package/dist/{chunk-I3IWTRYB.js → chunk-6BNFVP7Y.js} +2 -2
  7. package/dist/{chunk-DCGT4FPP.js → chunk-76QTEJ2Q.js} +2 -2
  8. package/dist/{chunk-3JSWINVD.js → chunk-AEIZEAP7.js} +7 -7
  9. package/dist/{chunk-RAELB5NX.js → chunk-CNRZ6WJU.js} +3 -3
  10. package/dist/{chunk-3MAONBX3.js → chunk-FOVPSMGI.js} +2 -2
  11. package/dist/{chunk-ZPPFKVSD.js → chunk-FUXV6HSO.js} +2 -2
  12. package/dist/{chunk-MUKXANAM.js → chunk-I4COC5XW.js} +49 -6
  13. package/dist/{chunk-MUKXANAM.js.map → chunk-I4COC5XW.js.map} +1 -1
  14. package/dist/{chunk-APWJRJFW.js → chunk-NMIOW7XG.js} +86 -8
  15. package/dist/chunk-NMIOW7XG.js.map +1 -0
  16. package/dist/{chunk-FAV25DUZ.js → chunk-QT4THOLT.js} +1 -1
  17. package/dist/{chunk-FAV25DUZ.js.map → chunk-QT4THOLT.js.map} +1 -1
  18. package/dist/{chunk-FAYDM5WD.js → chunk-RRRCNIPK.js} +2 -2
  19. package/dist/{chunk-RG3LBSGH.js → chunk-TQUWNX7C.js} +2 -2
  20. package/dist/{chunk-H67ZTMTL.js → chunk-TWVRDGTX.js} +5 -5
  21. package/dist/{chunk-U55D5UD5.js → chunk-WPCCNSWO.js} +5 -5
  22. package/dist/{chunk-JLOJ5RJ7.js → chunk-XUGQQPGO.js} +10 -3
  23. package/dist/chunk-XUGQQPGO.js.map +1 -0
  24. package/dist/{chunk-DC66QVL2.js → chunk-ZT6R3WR3.js} +2 -2
  25. package/dist/cli.js +15 -15
  26. package/dist/index.js +16 -16
  27. package/dist/namespaces/migrate.js +8 -8
  28. package/dist/namespaces/search.d.ts +1 -0
  29. package/dist/namespaces/search.js +7 -7
  30. package/dist/operator-toolkit.js +9 -9
  31. package/dist/orchestrator.js +9 -9
  32. package/dist/qmd.d.ts +1 -0
  33. package/dist/qmd.js +2 -2
  34. package/dist/resume-bundles.js +2 -2
  35. package/dist/search/factory.js +6 -6
  36. package/dist/search/index.js +6 -6
  37. package/dist/search/lancedb-backend.js +2 -2
  38. package/dist/search/meilisearch-backend.js +2 -2
  39. package/dist/search/orama-backend.js +2 -2
  40. package/dist/search/port.d.ts +6 -0
  41. package/dist/search/port.js +1 -1
  42. package/dist/transcript.d.ts +6 -0
  43. package/dist/transcript.js +1 -1
  44. package/package.json +1 -1
  45. package/src/namespaces/search.test.ts +218 -18
  46. package/src/namespaces/search.ts +122 -7
  47. package/src/qmd-client.test.ts +74 -1
  48. package/src/qmd.ts +52 -6
  49. package/src/search/port.ts +9 -0
  50. package/src/transcript.test.ts +110 -0
  51. package/src/transcript.ts +21 -3
  52. package/dist/chunk-APWJRJFW.js.map +0 -1
  53. package/dist/chunk-JLOJ5RJ7.js.map +0 -1
  54. /package/dist/{chunk-L7W5YW6Y.js.map → chunk-5ETA6OAS.js.map} +0 -0
  55. /package/dist/{chunk-I3IWTRYB.js.map → chunk-6BNFVP7Y.js.map} +0 -0
  56. /package/dist/{chunk-DCGT4FPP.js.map → chunk-76QTEJ2Q.js.map} +0 -0
  57. /package/dist/{chunk-3JSWINVD.js.map → chunk-AEIZEAP7.js.map} +0 -0
  58. /package/dist/{chunk-RAELB5NX.js.map → chunk-CNRZ6WJU.js.map} +0 -0
  59. /package/dist/{chunk-3MAONBX3.js.map → chunk-FOVPSMGI.js.map} +0 -0
  60. /package/dist/{chunk-ZPPFKVSD.js.map → chunk-FUXV6HSO.js.map} +0 -0
  61. /package/dist/{chunk-FAYDM5WD.js.map → chunk-RRRCNIPK.js.map} +0 -0
  62. /package/dist/{chunk-RG3LBSGH.js.map → chunk-TQUWNX7C.js.map} +0 -0
  63. /package/dist/{chunk-H67ZTMTL.js.map → chunk-TWVRDGTX.js.map} +0 -0
  64. /package/dist/{chunk-U55D5UD5.js.map → chunk-WPCCNSWO.js.map} +0 -0
  65. /package/dist/{chunk-DC66QVL2.js.map → chunk-ZT6R3WR3.js.map} +0 -0
@@ -0,0 +1,110 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdtemp } from "node:fs/promises";
6
+ import { TranscriptManager } from "./transcript.js";
7
+ import type { PluginConfig, TranscriptEntry } from "./types.js";
8
+
9
+ function makeConfig(memoryDir: string): PluginConfig {
10
+ return {
11
+ memoryDir,
12
+ transcriptSkipChannelTypes: [],
13
+ } as unknown as PluginConfig;
14
+ }
15
+
16
+ async function makeManager(): Promise<{ manager: TranscriptManager; memoryDir: string }> {
17
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-transcript-recent-"));
18
+ const manager = new TranscriptManager(makeConfig(memoryDir));
19
+ await manager.initialize();
20
+ return { manager, memoryDir };
21
+ }
22
+
23
+ function entryAt(timestamp: string, sessionKey: string, turnId: string): TranscriptEntry {
24
+ return { timestamp, role: "user", content: `turn ${turnId}`, sessionKey, turnId };
25
+ }
26
+
27
+ /**
28
+ * Freeze `new Date()` / `Date.now()` to a fixed instant for the duration of `fn`.
29
+ *
30
+ * This lets a test seed a transcript entry whose timestamp is *exactly* the
31
+ * instant the read observes as "now", deterministically reproducing the
32
+ * millisecond-collision that the recent-window boundary fix targets.
33
+ * Single-argument constructions (`new Date(isoString)`, `new Date(ms)`,
34
+ * `new Date(otherDate)`) are forwarded unchanged so timestamp parsing still works.
35
+ */
36
+ async function withFrozenNow<T>(now: Date, fn: () => Promise<T>): Promise<T> {
37
+ const RealDate = Date;
38
+ const fixedMs = now.getTime();
39
+ class FrozenDate extends RealDate {
40
+ constructor(...args: any[]) {
41
+ if (args.length === 0) {
42
+ super(fixedMs);
43
+ } else if (args.length === 1) {
44
+ super(args[0] as number);
45
+ } else {
46
+ super(
47
+ args[0] as number,
48
+ args[1] as number,
49
+ args[2] as number,
50
+ args[3] as number,
51
+ args[4] as number,
52
+ args[5] as number,
53
+ args[6] as number,
54
+ );
55
+ }
56
+ }
57
+ static now(): number {
58
+ return fixedMs;
59
+ }
60
+ }
61
+ (globalThis as { Date: DateConstructor }).Date = FrozenDate as unknown as DateConstructor;
62
+ try {
63
+ return await fn();
64
+ } finally {
65
+ (globalThis as { Date: DateConstructor }).Date = RealDate;
66
+ }
67
+ }
68
+
69
+ test("readRecent (session fast path) returns a turn written at the same instant as the read", async () => {
70
+ const { manager } = await makeManager();
71
+ const sessionKey = "agent:test-agent:main";
72
+ const now = new Date("2026-06-29T12:00:00.000Z");
73
+
74
+ const entries = await withFrozenNow(now, async () => {
75
+ // Seed an entry stamped at the exact instant the read observes as "now".
76
+ await manager.append(entryAt(now.toISOString(), sessionKey, "boundary"));
77
+ return manager.readRecent(48, sessionKey);
78
+ });
79
+
80
+ assert.equal(entries.length, 1, "just-written boundary entry should be included in last-N-hours read");
81
+ assert.equal(entries[0].turnId, "boundary");
82
+ });
83
+
84
+ test("readRecent (full scan, no sessionKey) returns a turn written at the same instant as the read", async () => {
85
+ const { manager } = await makeManager();
86
+ const sessionKey = "agent:test-agent:main";
87
+ const now = new Date("2026-06-29T12:00:00.000Z");
88
+
89
+ const entries = await withFrozenNow(now, async () => {
90
+ await manager.append(entryAt(now.toISOString(), sessionKey, "boundary"));
91
+ return manager.readRecent(48);
92
+ });
93
+
94
+ assert.equal(entries.length, 1, "just-written boundary entry should be included in full-scan recent read");
95
+ assert.equal(entries[0].turnId, "boundary");
96
+ });
97
+
98
+ test("readRange keeps an exclusive upper bound for caller-specified ranges", async () => {
99
+ const { manager } = await makeManager();
100
+ const sessionKey = "agent:test-agent:main";
101
+ const start = "2026-06-29T00:00:00.000Z";
102
+ const end = "2026-06-29T12:00:00.000Z";
103
+
104
+ await manager.append(entryAt("2026-06-29T11:59:59.999Z", sessionKey, "inside"));
105
+ await manager.append(entryAt(end, sessionKey, "at-end"));
106
+
107
+ const exclusive = await manager.readRange(start, end, sessionKey);
108
+ const ids = exclusive.map((e) => e.turnId);
109
+ assert.deepEqual(ids, ["inside"], "explicit [start, end) range must exclude the entry at exactly end (rule #35)");
110
+ });
package/src/transcript.ts CHANGED
@@ -646,6 +646,12 @@ export class TranscriptManager {
646
646
  * Read transcript entries for a date range.
647
647
  * Returns entries within the time range, optionally filtered by sessionKey.
648
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.
649
655
  */
650
656
  async readRange(startTime: string, endTime: string, sessionKey?: string): Promise<TranscriptEntry[]> {
651
657
  const start = new Date(startTime);
@@ -678,7 +684,7 @@ export class TranscriptManager {
678
684
  const entry = JSON.parse(line) as TranscriptEntry;
679
685
  const entryTime = new Date(entry.timestamp);
680
686
 
681
- // Check if entry is within time range
687
+ // Check if entry is within time range (half-open: exclusive end)
682
688
  if (entryTime >= start && entryTime < end) {
683
689
  // Filter by sessionKey if provided
684
690
  if (!sessionKey || entry.sessionKey === sessionKey) {
@@ -714,8 +720,17 @@ export class TranscriptManager {
714
720
  * specific channel instead of scanning all 95+ transcript files across all channels.
715
721
  */
716
722
  async readRecent(hours: number, sessionKey?: string): Promise<TranscriptEntry[]> {
717
- const end = new Date();
718
- 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);
719
734
 
720
735
  if (sessionKey) {
721
736
  return this.readRecentForSession(start, end, sessionKey);
@@ -769,6 +784,9 @@ export class TranscriptManager {
769
784
  try {
770
785
  const entry = JSON.parse(line) as TranscriptEntry;
771
786
  const ts = new Date(entry.timestamp);
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).
772
790
  if (ts >= start && ts < end && entry.sessionKey === sessionKey && keepRawLine(line)) {
773
791
  entries.push(entry);
774
792
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/namespaces/search.ts"],"sourcesContent":["import type { PluginConfig, QmdSearchResult } from \"../types.js\";\nimport type { SearchBackend, SearchExecutionOptions, SearchQueryOptions } from \"../search/port.js\";\nimport { createSearchBackend } from \"../search/factory.js\";\nimport { namespaceIdentityToken, normalizeNamespaceIdentity } from \"./identity.js\";\n\nexport function namespaceCollectionName(\n baseCollection: string,\n namespace: string,\n options?: {\n defaultNamespace?: string;\n useLegacyDefaultCollection?: boolean;\n },\n): string {\n const trimmed = normalizeNamespaceIdentity(namespace);\n const defaultNamespace = normalizeNamespaceIdentity(options?.defaultNamespace ?? \"\") || \"default\";\n if (\n options?.useLegacyDefaultCollection === true &&\n trimmed === defaultNamespace\n ) {\n return baseCollection;\n }\n\n return `${baseCollection}--${namespaceIdentityToken(trimmed || defaultNamespace)}`;\n}\n\ntype StorageRouterLike = {\n storageFor(namespace: string): Promise<{ dir: string }>;\n};\n\ntype NamespaceBackendRecord = {\n backend: SearchBackend;\n collection: string;\n memoryDir: string;\n available: boolean;\n collectionState: \"present\" | \"missing\" | \"unknown\" | \"skipped\";\n};\n\ntype NamespaceScopedSearchConfig = PluginConfig & {\n hostEmbeddingProviderScope?: string;\n};\n\nexport class NamespaceSearchRouter {\n private readonly cache = new Map<string, Promise<NamespaceBackendRecord>>();\n\n constructor(\n private readonly config: PluginConfig,\n private readonly storageRouter: StorageRouterLike,\n private readonly createBackend: (config: PluginConfig) => SearchBackend = createSearchBackend,\n ) {}\n\n async collectionForNamespace(namespace: string): Promise<string> {\n return (await this.backendRecordFor(namespace)).collection;\n }\n\n async searchAcrossNamespaces(options: {\n query: string;\n namespaces: string[];\n maxResults?: number;\n mode?: \"search\" | \"hybrid\" | \"bm25\" | \"vector\";\n searchOptions?: SearchQueryOptions;\n execution?: SearchExecutionOptions;\n }): Promise<QmdSearchResult[]> {\n const query = options.query.trim();\n if (!query) return [];\n const maxResults = Math.max(0, Math.floor(options.maxResults ?? this.config.qmdMaxResults));\n if (maxResults === 0) return [];\n\n const method = options.mode ?? \"search\";\n const namespaces = Array.from(new Set(options.namespaces.map((value) => value.trim()).filter(Boolean)));\n if (namespaces.length === 0) return [];\n\n const resultsByNamespace = await Promise.all(\n namespaces.map(async (namespace) => {\n const record = await this.backendRecordFor(namespace);\n if (!record.available || record.collectionState === \"missing\") {\n return { namespace, results: [] as QmdSearchResult[] };\n }\n let results: QmdSearchResult[];\n switch (method) {\n case \"hybrid\":\n results = await record.backend.hybridSearch(query, record.collection, maxResults, options.execution);\n break;\n case \"bm25\":\n results = await record.backend.bm25Search(query, record.collection, maxResults, options.execution);\n break;\n case \"vector\":\n results = await record.backend.vectorSearch(query, record.collection, maxResults, options.execution);\n break;\n default:\n results = await record.backend.search(\n query,\n record.collection,\n maxResults,\n options.searchOptions,\n options.execution,\n );\n break;\n }\n return { namespace, results };\n }),\n );\n\n return mergeNamespaceSearchResults(resultsByNamespace, maxResults);\n }\n\n /**\n * Update all namespace backends.\n * Returns the number of backends for which an update was attempted\n * (i.e., available and collection present). Callers can treat 0 as a\n * signal that no backend was eligible — useful for success-verification in\n * startup-sync when namespacesEnabled is true.\n */\n async updateNamespaces(\n namespaces: string[],\n execution?: SearchExecutionOptions,\n ): Promise<number> {\n const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));\n const eligible = (await Promise.all(\n unique.map(async (namespace) => {\n const record = await this.backendRecordFor(namespace);\n return record.available && record.collectionState !== \"missing\"\n ? record\n : null;\n }),\n )).filter((record): record is NamespaceBackendRecord => record !== null);\n\n const globalRecord = eligible.find((record) => record.backend.updatesAllCollections?.() === true);\n const scopedRecords = globalRecord\n ? eligible.filter((record) => record.backend.updatesAllCollections?.() !== true)\n : eligible;\n\n await Promise.all([\n globalRecord ? globalRecord.backend.update(execution) : Promise.resolve(),\n ...scopedRecords.map((record) => record.backend.update(execution)),\n ]);\n\n return (globalRecord ? 1 : 0) + scopedRecords.length;\n }\n\n async embedNamespaces(namespaces: string[]): Promise<void> {\n const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));\n await Promise.all(\n unique.map(async (namespace) => {\n const record = await this.backendRecordFor(namespace);\n if (!record.available || record.collectionState === \"missing\") return;\n await record.backend.embed();\n }),\n );\n }\n\n async ensureNamespaceCollection(\n namespace: string,\n execution?: SearchExecutionOptions,\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\"> {\n const record = await this.backendRecordFor(namespace, execution);\n return record.collectionState;\n }\n\n /** Clear cached backend records so the next access re-probes availability. */\n clearCache(): void {\n this.cache.clear();\n }\n\n /** Release any per-namespace backend handles held by cached records. */\n async dispose(): Promise<void> {\n const pendingRecords = Array.from(this.cache.values());\n this.cache.clear();\n const settled = await Promise.allSettled(pendingRecords);\n await Promise.allSettled(\n settled.flatMap((entry) => {\n if (entry.status !== \"fulfilled\") return [];\n const dispose = (entry.value.backend as { dispose?: () => void | Promise<void> }).dispose;\n return dispose ? [dispose.call(entry.value.backend)] : [];\n }),\n );\n }\n\n private async backendRecordFor(\n namespace: string,\n execution?: SearchExecutionOptions,\n ): Promise<NamespaceBackendRecord> {\n const key = namespace.trim() || this.config.defaultNamespace;\n const existing = this.cache.get(key);\n if (existing) return await existing;\n\n const pending = (async (): Promise<NamespaceBackendRecord> => {\n const storage = await this.storageRouter.storageFor(key);\n const useLegacyDefaultCollection =\n key === this.config.defaultNamespace && storage.dir === this.config.memoryDir;\n const rootHostEmbeddingScope =\n (this.config as NamespaceScopedSearchConfig).hostEmbeddingProviderScope ??\n this.config.memoryDir;\n const scopedConfig: NamespaceScopedSearchConfig = {\n ...this.config,\n memoryDir: storage.dir,\n hostEmbeddingProviderScope: rootHostEmbeddingScope,\n qmdCollection: namespaceCollectionName(this.config.qmdCollection, key, {\n defaultNamespace: this.config.defaultNamespace,\n useLegacyDefaultCollection,\n }),\n };\n\n const backend = this.createBackend(scopedConfig);\n const available = await backend.probe().catch(() => false);\n const collectionState = available\n ? await backend.ensureCollection(storage.dir, scopedConfig.qmdCollection, execution).catch(() => \"unknown\" as const)\n : \"unknown\";\n return {\n backend,\n collection: scopedConfig.qmdCollection,\n memoryDir: storage.dir,\n available,\n collectionState,\n };\n })();\n\n this.cache.set(key, pending);\n return await pending;\n }\n}\n\nfunction mergeNamespaceSearchResults(\n lists: Array<{ namespace: string; results: QmdSearchResult[] }>,\n maxResults: number,\n): QmdSearchResult[] {\n const merged = new Map<string, QmdSearchResult>();\n\n for (const { namespace, results } of lists) {\n for (const result of results) {\n const key = `${namespace}\\0${result.path || result.docid}`;\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, result);\n continue;\n }\n if (result.score > existing.score) {\n merged.set(key, {\n ...result,\n snippet: existing.snippet || result.snippet || \"\",\n });\n }\n }\n }\n\n return [...merged.values()]\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/transcript.ts"],"sourcesContent":["import { appendFile, mkdir, readdir, readFile, stat, unlink, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { log } from \"./logger.js\";\nimport type { TranscriptEntry, Checkpoint, PluginConfig } from \"./types.js\";\nimport { analyzeSessionIntegrity, type SessionIntegrityReport } from \"./session-integrity.js\";\nimport { resolveSafeStoragePath } from \"./storage-paths.js\";\nimport { sessionStoragePaths } from \"./session-identity.js\";\n\ntype DirectorySessionStatus = \"missing\" | \"empty\" | \"matches\" | \"occupied\";\ntype DirectoryOwnershipCacheEntry = {\n status: \"empty\" | \"matches\";\n fileSizes: Map<string, number>;\n};\n\n/**\n * De-duplicate JSONL lines collected across multiple read-back candidate\n * directories. A partially-applied migration (issue #1496) can leave the SAME\n * raw row in both the primary `session/<hash>` tree and a legacy read-back dir\n * (`other/default` or an old `parts.length >= 3` directory) when the source was\n * copied but not yet trimmed. Without this guard, `readRecent`, `readToolUse`,\n * footprint estimation, and the summarizer fetch would return that row twice\n * (cursor review on PR #1504). The exact raw JSONL line is the stable identity:\n * the migration preserves byte content per session, so a copied-but-not-trimmed\n * row is byte-identical in both locations. Returns `true` the first time a line\n * is seen, `false` for every subsequent duplicate.\n */\nfunction makeRawLineDeduper(): (rawLine: string) => boolean {\n const seen = new Set<string>();\n return (rawLine: string): boolean => {\n if (seen.has(rawLine)) return false;\n seen.add(rawLine);\n return true;\n };\n}\n\n/**\n * Manages conversation transcript storage, checkpointing, and recall formatting.\n *\n * Transcripts are stored as JSONL files in a hierarchical structure:\n * transcripts/{channelType}/{channelId}.jsonl\n *\n * Channel types are extracted from sessionKey (discord, slack, cron, main, etc.)\n * Checkpoints are used to preserve conversation context across compaction events.\n */\nexport class TranscriptManager {\n private transcriptsDir: string;\n private checkpointPath: string;\n private stateDir: string;\n private toolUsageDir: string;\n private config: PluginConfig;\n private sessionFootprintCache = new Map<\n string,\n { totalBytes: number; fileBytes: Map<string, number>; fileSizes: Map<string, number> }\n >();\n private directoryOwnershipCache = new Map<string, DirectoryOwnershipCacheEntry>();\n\n /** Default checkpoint TTL in hours */\n private static readonly DEFAULT_CHECKPOINT_TTL_HOURS = 24;\n /** Approximate characters per token for rough estimation */\n private static readonly CHARS_PER_TOKEN = 4;\n\n constructor(config: PluginConfig) {\n this.config = config;\n this.transcriptsDir = path.join(config.memoryDir, \"transcripts\");\n this.stateDir = path.join(config.memoryDir, \"state\");\n this.checkpointPath = path.join(this.stateDir, \"checkpoint.json\");\n this.toolUsageDir = path.join(this.stateDir, \"tool-usage\");\n }\n\n /**\n * Resolve the storage path pieces for a sessionKey via the shared\n * {@link sessionStoragePaths} helper (issue #1496, rule #22).\n *\n * Legacy `agent:<id>:...` shapes keep their existing readable channel paths.\n * Arbitrary / non-legacy keys (e.g. `pi-geek:abc123`) route to\n * `transcripts/session/<hash>/YYYY-MM-DD.jsonl` from the FIRST write — they\n * never start life in `other/default`. For those keys a list of\n * read-back-only `readbackDirs` is returned so data written by older builds\n * (under `other/default`, or under an old `parts.length >= 3` directory such\n * as `baz/default`) stays discoverable until migrated. New writes never\n * target read-back dirs. See `session-identity.ts`.\n *\n * @returns Object with raw channel identifiers and encoded storage path pieces.\n */\n getTranscriptPath(sessionKey: string): {\n dir: string;\n file: string;\n channelType: string;\n channelId: string;\n alternateDir: string;\n legacyDir?: string;\n readbackDirs: string[];\n } {\n const paths = sessionStoragePaths(sessionKey);\n\n // Daily rotation: transcripts/{channelType}/{channelId}/YYYY-MM-DD.jsonl\n const today = new Date().toISOString().slice(0, 10);\n\n return {\n dir: paths.dir,\n file: `${today}.jsonl`,\n channelType: paths.channelType,\n channelId: paths.channelId,\n alternateDir: paths.alternateDir,\n legacyDir: paths.legacyDir,\n readbackDirs: paths.readbackDirs,\n };\n }\n\n /**\n * Initialize the transcript manager by ensuring directories exist.\n */\n async initialize(): Promise<void> {\n await mkdir(this.transcriptsDir, { recursive: true });\n await mkdir(this.stateDir, { recursive: true });\n await mkdir(this.toolUsageDir, { recursive: true });\n log.info(\"transcript manager initialized\");\n }\n\n /**\n * Best-effort list of sessionKeys that have transcript files on disk.\n * This is used by cron-style tooling (hourly summaries, conversation indexing)\n * to iterate across \"active\" sessions.\n */\n async listSessionKeys(): Promise<string[]> {\n const transcriptDir = this.transcriptsDir;\n const sessionKeys = new Set<string>();\n\n try {\n const typeEntries = await readdir(transcriptDir, { withFileTypes: true });\n for (const typeEnt of typeEntries) {\n if (!typeEnt.isDirectory()) continue;\n const typeDir = path.join(transcriptDir, typeEnt.name);\n const idEntries = await readdir(typeDir, { withFileTypes: true });\n for (const idEnt of idEntries) {\n if (!idEnt.isDirectory()) continue;\n const chanDir = path.join(typeDir, idEnt.name);\n const files = (await readdir(chanDir)).filter((f) => f.endsWith(\".jsonl\")).sort();\n const last = files[files.length - 1];\n if (!last) continue;\n try {\n const raw = await readFile(path.join(chanDir, last), \"utf-8\");\n const firstLine = raw.split(\"\\n\").find((l) => l.trim().length > 0);\n if (!firstLine) continue;\n const entry = JSON.parse(firstLine) as TranscriptEntry;\n if (typeof entry.sessionKey === \"string\" && entry.sessionKey.length > 0) {\n sessionKeys.add(entry.sessionKey);\n }\n } catch {\n // ignore\n }\n }\n }\n } catch {\n return [];\n }\n\n return Array.from(sessionKeys);\n }\n\n getToolUsagePath(sessionKey: string): {\n dir: string;\n file: string;\n alternateDir: string;\n legacyDir?: string;\n readbackDirs: string[];\n } {\n const p = this.getTranscriptPath(sessionKey);\n return {\n dir: p.dir,\n file: p.file,\n alternateDir: p.alternateDir,\n legacyDir: p.legacyDir,\n readbackDirs: p.readbackDirs,\n };\n }\n\n private async selectStorageDirForWrite(\n root: string,\n dir: string,\n legacyDir?: string,\n sessionKey?: string,\n alternateDir?: string,\n ): Promise<{ dir: string; channelDir: string }> {\n const channelDir = await resolveSafeStoragePath(root, dir);\n const encodedStatus = await this.directorySessionStatus(root, dir, sessionKey);\n if (encodedStatus === \"matches\" || encodedStatus === \"empty\") return { dir, channelDir };\n\n if (legacyDir) {\n const legacyChannelDir = await resolveSafeStoragePath(root, legacyDir);\n if ((await this.directorySessionStatus(root, legacyDir, sessionKey)) === \"matches\") {\n return { dir: legacyDir, channelDir: legacyChannelDir };\n }\n }\n\n if (encodedStatus === \"missing\") return { dir, channelDir };\n\n if (alternateDir) {\n const alternateChannelDir = await resolveSafeStoragePath(root, alternateDir);\n const alternateStatus = await this.directorySessionStatus(root, alternateDir, sessionKey);\n if (\n alternateStatus === \"missing\" ||\n alternateStatus === \"empty\" ||\n alternateStatus === \"matches\"\n ) {\n return { dir: alternateDir, channelDir: alternateChannelDir };\n }\n }\n\n throw new Error(`transcript storage path collision for session: ${sessionKey ?? \"(unknown)\"}`);\n }\n\n private async directorySessionStatus(\n root: string,\n dir: string,\n sessionKey?: string,\n ): Promise<DirectorySessionStatus> {\n let channelDir: string;\n try {\n channelDir = await resolveSafeStoragePath(root, dir);\n if (!(await stat(channelDir)).isDirectory()) return \"occupied\";\n } catch (err) {\n const code =\n err && typeof err === \"object\" && \"code\" in err\n ? (err as { code?: string }).code\n : undefined;\n if (code === \"ENOENT\") return \"missing\";\n throw err;\n }\n\n let names: string[];\n try {\n names = (await readdir(channelDir)).filter((file) => file.endsWith(\".jsonl\"));\n } catch {\n return \"occupied\";\n }\n\n const fileSizes = await this.directoryJsonlFileSizes(root, dir, names);\n if (!fileSizes) return \"occupied\";\n\n if (!sessionKey) return \"matches\";\n const cacheKey = this.directoryOwnershipCacheKey(root, dir, sessionKey);\n const cached = this.directoryOwnershipCache.get(cacheKey);\n if (cached && this.sameFileSizes(cached.fileSizes, fileSizes)) {\n return cached.status;\n }\n\n let hasEntries = false;\n let hasMatchingEntry = false;\n\n for (const name of names) {\n const filePath = await resolveSafeStoragePath(root, dir, name).catch(() => null);\n if (filePath === null) return \"occupied\";\n try {\n const raw = await readFile(filePath, \"utf-8\");\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n hasEntries = true;\n try {\n const obj = JSON.parse(line) as { sessionKey?: string };\n if (obj.sessionKey === sessionKey) {\n hasMatchingEntry = true;\n } else {\n return \"occupied\";\n }\n } catch {\n return \"occupied\";\n }\n }\n } catch {\n return \"occupied\";\n }\n }\n\n const status = hasMatchingEntry ? \"matches\" : hasEntries ? \"occupied\" : \"empty\";\n if (status === \"matches\" || status === \"empty\") {\n this.directoryOwnershipCache.set(cacheKey, { status, fileSizes });\n }\n return status;\n }\n\n private directoryOwnershipCacheKey(root: string, dir: string, sessionKey: string): string {\n return `${path.resolve(root)}\\0${dir}\\0${sessionKey}`;\n }\n\n private sameFileSizes(left: Map<string, number>, right: Map<string, number>): boolean {\n if (left.size !== right.size) return false;\n for (const [name, size] of left) {\n if (right.get(name) !== size) return false;\n }\n return true;\n }\n\n private async directoryJsonlFileSizes(\n root: string,\n dir: string,\n names: string[],\n ): Promise<Map<string, number> | null> {\n const fileSizes = new Map<string, number>();\n for (const name of names) {\n const filePath = await resolveSafeStoragePath(root, dir, name).catch(() => null);\n if (filePath === null) return null;\n const fileInfo = await stat(filePath).catch(() => null);\n if (!fileInfo?.isFile()) return null;\n fileSizes.set(name, Math.max(0, fileInfo.size));\n }\n return fileSizes;\n }\n\n private async rememberDirectoryOwnership(\n root: string,\n dir: string,\n sessionKey: string,\n ): Promise<void> {\n try {\n const channelDir = await resolveSafeStoragePath(root, dir);\n const names = (await readdir(channelDir)).filter((file) => file.endsWith(\".jsonl\"));\n const fileSizes = await this.directoryJsonlFileSizes(root, dir, names);\n if (!fileSizes) return;\n this.directoryOwnershipCache.set(\n this.directoryOwnershipCacheKey(root, dir, sessionKey),\n { status: \"matches\", fileSizes },\n );\n } catch {\n // Cache refresh is best-effort; write path correctness does not depend on it.\n }\n }\n\n private async getSessionStorageFiles(\n root: string,\n dir: string,\n legacyDir?: string,\n alternateDir?: string,\n readbackDirs: string[] = [],\n ): Promise<Array<{ cacheKey: string; name: string; path: string }>> {\n const files: Array<{ cacheKey: string; name: string; path: string }> = [];\n const seenDirs = new Set<string>();\n\n for (const candidateDir of [dir, alternateDir, legacyDir, ...readbackDirs]) {\n if (!candidateDir || seenDirs.has(candidateDir)) continue;\n seenDirs.add(candidateDir);\n\n let channelDir: string;\n try {\n channelDir = await resolveSafeStoragePath(root, candidateDir);\n } catch {\n continue;\n }\n\n let names: string[];\n try {\n names = (await readdir(channelDir)).filter((file) => file.endsWith(\".jsonl\")).sort();\n } catch {\n continue;\n }\n\n for (const name of names) {\n const filePath = await resolveSafeStoragePath(root, candidateDir, name).catch(() => null);\n if (filePath === null) continue;\n files.push({\n cacheKey: path.join(candidateDir, name),\n name,\n path: filePath,\n });\n }\n }\n\n return files.sort((a, b) => a.cacheKey.localeCompare(b.cacheKey));\n }\n\n async appendToolUse(entry: { timestamp: string; sessionKey: string; tool: string }): Promise<void> {\n const { dir, file, alternateDir, legacyDir } = this.getToolUsagePath(entry.sessionKey);\n const { dir: writeDir, channelDir } = await this.selectStorageDirForWrite(\n this.toolUsageDir,\n dir,\n legacyDir,\n entry.sessionKey,\n alternateDir,\n );\n await mkdir(channelDir, { recursive: true });\n const filePath = await resolveSafeStoragePath(this.toolUsageDir, writeDir, file);\n await appendFile(filePath, JSON.stringify(entry) + \"\\n\", \"utf-8\");\n await this.rememberDirectoryOwnership(this.toolUsageDir, writeDir, entry.sessionKey);\n }\n\n async readToolUse(\n sessionKey: string,\n startTime: Date,\n endTime: Date,\n ): Promise<Array<{ timestamp: string; sessionKey: string; tool: string }>> {\n const { dir, alternateDir, legacyDir, readbackDirs } = this.getToolUsagePath(sessionKey);\n try {\n const files = await this.getSessionStorageFiles(\n this.toolUsageDir,\n dir,\n legacyDir,\n alternateDir,\n readbackDirs,\n );\n const out: Array<{ timestamp: string; sessionKey: string; tool: string }> = [];\n // Dedup identical raw rows that a partially-applied migration may have left\n // in both the primary dir and a read-back dir (issue #1496, cursor review).\n const keepRawLine = makeRawLineDeduper();\n for (const file of files) {\n const raw = await readFile(file.path, \"utf-8\");\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const obj = JSON.parse(line) as any;\n const ts = new Date(String(obj.timestamp ?? \"\")).getTime();\n if (!Number.isFinite(ts)) continue;\n if (ts >= startTime.getTime() && ts < endTime.getTime()) {\n if (typeof obj.tool === \"string\" && typeof obj.sessionKey === \"string\") {\n if (obj.sessionKey === sessionKey && keepRawLine(line)) {\n out.push({ timestamp: obj.timestamp, sessionKey: obj.sessionKey, tool: obj.tool });\n }\n }\n }\n } catch {\n // ignore\n }\n }\n }\n return out;\n } catch {\n return [];\n }\n }\n\n async estimateSessionFootprint(sessionKey: string): Promise<{ bytes: number; tokens: number }> {\n const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);\n let bytes = 0;\n\n // NOTE: this is a best-effort byte ESTIMATE for compaction sizing, not an\n // exact row count, and is maintained via an incremental per-file cache. A\n // copied-but-not-trimmed migration window (issue #1496) can transiently\n // over-estimate by counting a duplicated row in both the primary and a\n // read-back dir; that self-heals once the migration trims the source. The\n // exact-once guarantee that matters for callers lives in readRecent /\n // readToolUse / the summarizer fetch, which dedup by raw line.\n try {\n const files = await this.getSessionStorageFiles(\n this.transcriptsDir,\n dir,\n legacyDir,\n alternateDir,\n readbackDirs,\n );\n const cached = this.sessionFootprintCache.get(sessionKey);\n if (!cached) {\n const fileBytes = new Map<string, number>();\n const fileSizes = new Map<string, number>();\n for (const file of files) {\n try {\n const fileInfo = await stat(file.path);\n const sessionBytes = await this.estimateSessionBytesInFile(\n file.path,\n sessionKey,\n );\n fileBytes.set(file.cacheKey, sessionBytes);\n fileSizes.set(file.cacheKey, Math.max(0, fileInfo.size));\n bytes += sessionBytes;\n } catch {\n // fail-open\n }\n }\n this.sessionFootprintCache.set(sessionKey, { totalBytes: bytes, fileBytes, fileSizes });\n } else {\n bytes = cached.totalBytes;\n const seen = new Set(files.map((file) => file.cacheKey));\n\n // Drop removed files from the cached total.\n for (const [cachedFile, cachedSessionBytes] of cached.fileBytes.entries()) {\n if (!seen.has(cachedFile)) {\n bytes -= cachedSessionBytes;\n cached.fileBytes.delete(cachedFile);\n cached.fileSizes.delete(cachedFile);\n }\n }\n\n // Read only newly discovered files.\n for (const file of files) {\n if (cached.fileBytes.has(file.cacheKey)) continue;\n try {\n const fileInfo = await stat(file.path);\n const sessionBytes = await this.estimateSessionBytesInFile(file.path, sessionKey);\n cached.fileBytes.set(file.cacheKey, sessionBytes);\n cached.fileSizes.set(file.cacheKey, Math.max(0, fileInfo.size));\n bytes += sessionBytes;\n } catch {\n // fail-open\n }\n }\n\n // Recompute any shard whose file size changed. A session can have both\n // encoded and legacy directories during migration, so path ordering does\n // not reliably identify the file that can grow.\n for (const file of files) {\n try {\n const fileInfo = await stat(file.path);\n const size = Math.max(0, fileInfo.size);\n const previousSessionBytes = cached.fileBytes.get(file.cacheKey) ?? 0;\n const previousSize = cached.fileSizes.get(file.cacheKey) ?? -1;\n if (size !== previousSize) {\n const sessionBytes = await this.estimateSessionBytesInFile(file.path, sessionKey);\n cached.fileBytes.set(file.cacheKey, sessionBytes);\n cached.fileSizes.set(file.cacheKey, size);\n bytes += sessionBytes - previousSessionBytes;\n }\n } catch {\n // fail-open\n }\n }\n\n if (bytes < 0) bytes = 0;\n cached.totalBytes = bytes;\n }\n } catch {\n // fail-open\n this.sessionFootprintCache.delete(sessionKey);\n }\n\n return {\n bytes,\n tokens: Math.floor(bytes / TranscriptManager.CHARS_PER_TOKEN),\n };\n }\n\n private async estimateSessionBytesInFile(filePath: string, sessionKey: string): Promise<number> {\n try {\n const raw = await readFile(filePath, \"utf-8\");\n let total = 0;\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line) as { sessionKey?: string };\n if (parsed.sessionKey === sessionKey) {\n total += Buffer.byteLength(`${line}\\n`, \"utf-8\");\n }\n } catch {\n // fail-open for malformed lines\n }\n }\n return total;\n } catch {\n return 0;\n }\n }\n\n /**\n * Check if a file is a legacy flat transcript file (YYYY-MM-DD.jsonl format).\n */\n private isLegacyTranscriptFile(filename: string): boolean {\n return /^\\d{4}-\\d{2}-\\d{2}\\.jsonl$/.test(filename);\n }\n\n /**\n * Append a turn to the appropriate transcript file.\n * Files are stored hierarchically: transcripts/{channelType}/{channelId}.jsonl\n *\n * Skips channel types in config.transcriptSkipChannelTypes (e.g., \"cron\").\n */\n async append(entry: TranscriptEntry): Promise<void> {\n try {\n const { dir, file, channelType, alternateDir, legacyDir } = this.getTranscriptPath(entry.sessionKey);\n\n // Skip if this channel type is in the skip list\n if (this.config.transcriptSkipChannelTypes.includes(channelType)) {\n return;\n }\n\n const { dir: writeDir, channelDir } = await this.selectStorageDirForWrite(\n this.transcriptsDir,\n dir,\n legacyDir,\n entry.sessionKey,\n alternateDir,\n );\n const filePath = await resolveSafeStoragePath(this.transcriptsDir, writeDir, file);\n\n // Ensure channel directory exists\n await mkdir(channelDir, { recursive: true });\n\n const line = JSON.stringify(entry) + \"\\n\";\n await appendFile(filePath, line, \"utf-8\");\n await this.rememberDirectoryOwnership(this.transcriptsDir, writeDir, entry.sessionKey);\n log.debug(`appended transcript entry for ${entry.sessionKey}: ${entry.turnId}`);\n } catch (err) {\n log.error(\"failed to append transcript entry:\", err);\n throw err;\n }\n }\n\n /**\n * Get all transcript files from the hierarchical directory structure.\n * Recursively finds all .jsonl files in transcripts/{channelType}/{channelId}/ subdirectories.\n */\n private async getAllTranscriptFiles(): Promise<string[]> {\n const files: string[] = [];\n\n try {\n const entries = await readdir(this.transcriptsDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n // This is a channel type directory (discord, slack, cron, main, etc.)\n const channelTypeDir = path.join(this.transcriptsDir, entry.name);\n try {\n const channelTypeEntries = await readdir(channelTypeDir, { withFileTypes: true });\n\n for (const channelTypeEntry of channelTypeEntries) {\n if (channelTypeEntry.isDirectory()) {\n // This is a channel ID directory - contains daily transcript files\n const channelDir = path.join(channelTypeDir, channelTypeEntry.name);\n try {\n const channelFiles = await readdir(channelDir);\n for (const file of channelFiles) {\n if (file.endsWith(\".jsonl\")) {\n files.push(path.join(entry.name, channelTypeEntry.name, file));\n }\n }\n } catch {\n // Skip unreadable directories\n }\n } else if (channelTypeEntry.isFile() && channelTypeEntry.name.endsWith(\".jsonl\")) {\n // Legacy: channel type dir contains .jsonl files directly\n files.push(path.join(entry.name, channelTypeEntry.name));\n }\n }\n } catch {\n // Skip unreadable directories\n }\n } else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n // Legacy flat file - still include for backward compatibility\n files.push(entry.name);\n }\n }\n } catch {\n // Directory doesn't exist or is unreadable\n }\n\n return files;\n }\n\n /**\n * Read transcript entries for a date range.\n * Returns entries within the time range, optionally filtered by sessionKey.\n * Reads from all channel subdirectories in the hierarchical structure.\n */\n async readRange(startTime: string, endTime: string, sessionKey?: string): Promise<TranscriptEntry[]> {\n const start = new Date(startTime);\n const end = new Date(endTime);\n const entries: TranscriptEntry[] = [];\n\n // When a sessionKey is given, a partially-applied #1496 migration can leave\n // the SAME raw row in both the primary `session/<hash>` dir and a read-back\n // dir (`other/default` or an old `parts.length >= 3` dir). `readRange` scans\n // EVERY transcript file, so it would append that row once per directory.\n // Dedup by exact raw line to give the same exact-once guarantee `readRecent`\n // / `readToolUse` / the summarizer fetch already provide (cursor review on\n // PR #1504). Only applied for the session-scoped path so unfiltered range\n // scans (each file already enumerated once) keep their existing behavior.\n const keepRawLine = sessionKey ? makeRawLineDeduper() : undefined;\n\n try {\n // Get all transcript files from the hierarchical structure\n const transcriptFiles = await this.getAllTranscriptFiles();\n\n // Read each relevant file\n for (const relativePath of transcriptFiles) {\n const filePath = path.join(this.transcriptsDir, relativePath);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line) as TranscriptEntry;\n const entryTime = new Date(entry.timestamp);\n\n // Check if entry is within time range\n if (entryTime >= start && entryTime < end) {\n // Filter by sessionKey if provided\n if (!sessionKey || entry.sessionKey === sessionKey) {\n if (keepRawLine && !keepRawLine(line)) continue;\n entries.push(entry);\n }\n }\n } catch {\n // Skip malformed lines\n log.debug(`skipped malformed transcript line in ${relativePath}`);\n }\n }\n } catch {\n // File doesn't exist or is unreadable - skip\n }\n }\n\n // Sort by timestamp\n entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n\n log.debug(`read ${entries.length} transcript entries from ${transcriptFiles.length} file(s)`);\n return entries;\n } catch (err) {\n log.error(\"failed to read transcript range:\", err);\n return [];\n }\n }\n\n /**\n * Read the last N hours of transcript.\n *\n * Fast path: when sessionKey is given, reads only the 1-2 daily files for that\n * specific channel instead of scanning all 95+ transcript files across all channels.\n */\n async readRecent(hours: number, sessionKey?: string): Promise<TranscriptEntry[]> {\n const end = new Date();\n const start = new Date(end.getTime() - hours * 60 * 60 * 1000);\n\n if (sessionKey) {\n return this.readRecentForSession(start, end, sessionKey);\n }\n return this.readRange(start.toISOString(), end.toISOString(), undefined);\n }\n\n /**\n * Optimized read for a specific session: only looks in that session's channel\n * directory and only reads files whose date falls within the lookback window.\n */\n private async readRecentForSession(\n start: Date,\n end: Date,\n sessionKey: string,\n ): Promise<TranscriptEntry[]> {\n const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);\n\n // Build set of date strings that overlap with [start, end].\n // Always include end's date to handle midnight-crossing lookbacks\n // (e.g. start=23:30 yesterday, end=00:30 today).\n const dateStrings = new Set<string>();\n const cursor = new Date(start);\n while (cursor <= end) {\n dateStrings.add(cursor.toISOString().slice(0, 10));\n cursor.setDate(cursor.getDate() + 1);\n }\n dateStrings.add(end.toISOString().slice(0, 10));\n\n const entries: TranscriptEntry[] = [];\n const files = await this.getSessionStorageFiles(\n this.transcriptsDir,\n dir,\n legacyDir,\n alternateDir,\n readbackDirs,\n );\n\n // Dedup identical raw rows that a partially-applied migration may have left\n // in both the primary dir and a read-back dir (issue #1496, cursor review).\n const keepRawLine = makeRawLineDeduper();\n for (const file of files) {\n // Only read files whose date is within the window\n const dateStr = file.name.slice(0, 10);\n if (!dateStrings.has(dateStr)) continue;\n\n try {\n const content = await readFile(file.path, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const entry = JSON.parse(line) as TranscriptEntry;\n const ts = new Date(entry.timestamp);\n if (ts >= start && ts < end && entry.sessionKey === sessionKey && keepRawLine(line)) {\n entries.push(entry);\n }\n } catch {\n // skip malformed line\n }\n }\n } catch {\n // skip unreadable file\n }\n }\n\n entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n log.debug(`readRecentForSession: ${entries.length} entries from ${files.length} file(s) in ${dir}`);\n return entries;\n }\n\n /**\n * Cleanup old transcript entries that are older than retentionDays.\n * For hierarchical structure, reads each file and rewrites without old entries.\n * Legacy flat files are deleted if their date is older than retentionDays.\n * Returns the number of files processed (cleaned or deleted).\n */\n async cleanup(retentionDays: number): Promise<number> {\n if (retentionDays <= 0) {\n log.warn(\"cleanup called with invalid retentionDays:\", retentionDays);\n return 0;\n }\n\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - retentionDays);\n cutoff.setHours(0, 0, 0, 0);\n\n let processed = 0;\n\n try {\n const entries = await readdir(this.transcriptsDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n // This is a channel type directory (discord, slack, cron, main, etc.)\n const channelTypeDir = path.join(this.transcriptsDir, entry.name);\n try {\n const channelTypeEntries = await readdir(channelTypeDir, { withFileTypes: true });\n\n for (const channelTypeEntry of channelTypeEntries) {\n if (channelTypeEntry.isDirectory()) {\n // This is a channel ID directory - contains daily transcript files\n const channelDir = path.join(channelTypeDir, channelTypeEntry.name);\n try {\n const channelFiles = await readdir(channelDir);\n for (const file of channelFiles) {\n if (!file.endsWith(\".jsonl\")) continue;\n\n const filePath = path.join(channelDir, file);\n\n // Check if file is a daily transcript file (YYYY-MM-DD.jsonl)\n if (this.isLegacyTranscriptFile(file)) {\n const dateStr = file.slice(0, 10);\n const fileDate = new Date(dateStr);\n\n if (!isNaN(fileDate.getTime()) && fileDate < cutoff) {\n try {\n await unlink(filePath);\n processed++;\n log.debug(`deleted old daily transcript file: ${entry.name}/${channelTypeEntry.name}/${file}`);\n } catch (err) {\n log.error(`failed to delete transcript file ${filePath}:`, err);\n }\n }\n } else {\n // Legacy file in new structure - clean up old entries\n const cleaned = await this.cleanupTranscriptFile(filePath, cutoff);\n if (cleaned) {\n processed++;\n }\n }\n }\n } catch (err) {\n log.debug(`failed to process channel directory ${entry.name}/${channelTypeEntry.name}:`, err);\n }\n } else if (channelTypeEntry.isFile() && channelTypeEntry.name.endsWith(\".jsonl\")) {\n // Legacy: channel type dir contains .jsonl files directly\n const filePath = path.join(channelTypeDir, channelTypeEntry.name);\n const cleaned = await this.cleanupTranscriptFile(filePath, cutoff);\n if (cleaned) {\n processed++;\n }\n }\n }\n } catch (err) {\n log.debug(`failed to process channel type directory ${entry.name}:`, err);\n }\n } else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n // Handle legacy flat files - delete if older than retentionDays\n if (this.isLegacyTranscriptFile(entry.name)) {\n const dateStr = entry.name.slice(0, 10);\n const fileDate = new Date(dateStr);\n\n if (!isNaN(fileDate.getTime()) && fileDate < cutoff) {\n const filePath = path.join(this.transcriptsDir, entry.name);\n try {\n await unlink(filePath);\n processed++;\n log.debug(`deleted old legacy transcript file: ${entry.name}`);\n } catch (err) {\n log.error(`failed to delete legacy transcript file ${entry.name}:`, err);\n }\n }\n }\n }\n }\n\n if (processed > 0) {\n log.info(`cleaned up ${processed} transcript file(s) older than ${retentionDays} days`);\n }\n\n return processed;\n } catch (err) {\n log.error(\"failed to cleanup old transcripts:\", err);\n return 0;\n }\n }\n\n /**\n * Clean up old entries from a single transcript file.\n * Reads the file, filters out entries older than cutoff, and rewrites if needed.\n * Returns true if the file was processed (cleaned or deleted).\n */\n private async cleanupTranscriptFile(filePath: string, cutoff: Date): Promise<boolean> {\n try {\n const content = await readFile(filePath, \"utf-8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n\n const validLines: string[] = [];\n let hasOldEntries = false;\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line) as TranscriptEntry;\n const entryTime = new Date(entry.timestamp);\n\n if (entryTime >= cutoff) {\n validLines.push(line);\n } else {\n hasOldEntries = true;\n }\n } catch {\n // Keep malformed lines to avoid data loss\n validLines.push(line);\n }\n }\n\n if (validLines.length === 0) {\n // No valid entries left, delete the file\n try {\n await unlink(filePath);\n log.debug(`deleted empty transcript file: ${filePath}`);\n return true;\n } catch (err) {\n log.error(`failed to delete empty transcript file ${filePath}:`, err);\n return false;\n }\n }\n\n if (hasOldEntries) {\n // Rewrite file without old entries\n await writeFile(filePath, validLines.join(\"\\n\") + \"\\n\", \"utf-8\");\n log.debug(`cleaned old entries from transcript file: ${filePath}`);\n return true;\n }\n\n // No old entries found, no action needed\n return false;\n } catch (err) {\n // File doesn't exist or is unreadable\n return false;\n }\n }\n\n /**\n * Save a checkpoint to preserve conversation context.\n * Called when compaction is detected.\n */\n async saveCheckpoint(checkpoint: Checkpoint): Promise<void> {\n try {\n await writeFile(this.checkpointPath, JSON.stringify(checkpoint, null, 2), \"utf-8\");\n log.info(`saved checkpoint for session ${checkpoint.sessionKey} with ${checkpoint.turns.length} turn(s)`);\n } catch (err) {\n log.error(\"failed to save checkpoint:\", err);\n throw err;\n }\n }\n\n /**\n * Load a checkpoint if one exists and is not expired.\n * Returns null if no checkpoint exists or if it has expired.\n */\n async loadCheckpoint(sessionKey?: string): Promise<Checkpoint | null> {\n try {\n const raw = await readFile(this.checkpointPath, \"utf-8\");\n const checkpoint = JSON.parse(raw) as Checkpoint;\n\n // Validate checkpoint structure\n if (!checkpoint.sessionKey || !checkpoint.capturedAt || !checkpoint.ttl || !Array.isArray(checkpoint.turns)) {\n log.warn(\"checkpoint file has invalid structure\");\n return null;\n }\n\n // Check if checkpoint is for the requested session (if specified)\n if (sessionKey && checkpoint.sessionKey !== sessionKey) {\n log.debug(`checkpoint session mismatch: ${checkpoint.sessionKey} vs ${sessionKey}`);\n return null;\n }\n\n // Check if checkpoint has expired\n const ttl = new Date(checkpoint.ttl);\n if (isNaN(ttl.getTime())) {\n log.warn(\"checkpoint has invalid TTL format\");\n return null;\n }\n\n if (ttl < new Date()) {\n log.info(`checkpoint expired at ${checkpoint.ttl}`);\n return null;\n }\n\n log.info(`loaded checkpoint with ${checkpoint.turns.length} turn(s), expires at ${checkpoint.ttl}`);\n return checkpoint;\n } catch (err) {\n // File doesn't exist or is unreadable - that's fine\n log.debug(\"no valid checkpoint found\");\n return null;\n }\n }\n\n /**\n * Clear (delete) the checkpoint file.\n * Called after successful injection of checkpoint context.\n */\n async clearCheckpoint(): Promise<void> {\n try {\n await unlink(this.checkpointPath);\n log.info(\"cleared checkpoint\");\n } catch (err) {\n // File doesn't exist - that's fine\n log.debug(\"no checkpoint to clear\");\n }\n }\n\n /**\n * Format entries for recall injection.\n * Returns a formatted string suitable for injecting into agent context.\n *\n * Format:\n * ## Recent Conversation (last X hours)\n * [10:32] User: message content\n * [10:33] Assistant: response content\n *\n * Content is trimmed to approximately maxTokens.\n */\n formatForRecall(entries: TranscriptEntry[], maxTokens: number): string {\n if (entries.length === 0) {\n return \"\";\n }\n\n const maxChars = maxTokens * TranscriptManager.CHARS_PER_TOKEN;\n const lines: string[] = [];\n\n // Calculate time range for header\n const firstEntry = new Date(entries[0].timestamp);\n const lastEntry = new Date(entries[entries.length - 1].timestamp);\n const hoursDiff = Math.round((lastEntry.getTime() - firstEntry.getTime()) / (60 * 60 * 1000));\n\n // Add header\n if (hoursDiff < 1) {\n lines.push(\"## Recent Conversation (last few minutes)\");\n } else {\n lines.push(`## Recent Conversation (last ${hoursDiff} hour${hoursDiff === 1 ? \"\" : \"s\"})`);\n }\n lines.push(\"\");\n\n // Format each entry\n const formattedEntries: string[] = [];\n for (const entry of entries) {\n const time = new Date(entry.timestamp);\n const timeStr = time.toLocaleTimeString(\"en-US\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n });\n const roleLabel = entry.role === \"user\" ? \"User\" : \"Assistant\";\n formattedEntries.push(`[${timeStr}] ${roleLabel}: ${entry.content}`);\n }\n\n // Build output, trimming from the beginning if too long\n // (we want to keep the most recent context)\n let totalChars = lines.join(\"\\n\").length;\n const selectedEntries: string[] = [];\n\n for (let i = formattedEntries.length - 1; i >= 0; i--) {\n const entry = formattedEntries[i];\n const entryChars = entry.length + 1; // +1 for newline\n\n if (totalChars + entryChars > maxChars && selectedEntries.length > 0) {\n // Adding this entry would exceed limit, and we have some entries already\n break;\n }\n\n selectedEntries.unshift(entry);\n totalChars += entryChars;\n }\n\n lines.push(...selectedEntries);\n lines.push(\"\"); // Trailing newline\n\n const result = lines.join(\"\\n\");\n log.debug(`formatted ${selectedEntries.length}/${entries.length} transcript entries for recall (~${result.length} chars)`);\n\n return result;\n }\n\n /**\n * Create a checkpoint from the current buffer state.\n * Helper method for creating checkpoints before compaction.\n */\n createCheckpoint(sessionKey: string, turns: TranscriptEntry[], ttlHours?: number): Checkpoint {\n const ttl = ttlHours ?? TranscriptManager.DEFAULT_CHECKPOINT_TTL_HOURS;\n const expiresAt = new Date();\n expiresAt.setHours(expiresAt.getHours() + ttl);\n\n return {\n sessionKey,\n capturedAt: new Date().toISOString(),\n turns: [...turns], // Copy turns to avoid mutation\n ttl: expiresAt.toISOString(),\n };\n }\n\n /**\n * Get statistics about stored transcripts.\n * Returns counts from the hierarchical directory structure.\n */\n async getStats(): Promise<{\n totalFiles: number;\n totalEntries: number;\n oldestFile: string | null;\n newestFile: string | null;\n channelTypes: Record<string, number>;\n }> {\n try {\n const allFiles = await this.getAllTranscriptFiles();\n\n if (allFiles.length === 0) {\n return {\n totalFiles: 0,\n totalEntries: 0,\n oldestFile: null,\n newestFile: null,\n channelTypes: {},\n };\n }\n\n // Sort files by path\n const sortedFiles = allFiles.sort();\n\n let totalEntries = 0;\n const channelTypes: Record<string, number> = {};\n\n for (const relativePath of allFiles) {\n const filePath = path.join(this.transcriptsDir, relativePath);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n totalEntries += lines.length;\n\n // Count by channel type (first directory in path)\n const channelType = relativePath.includes(path.sep)\n ? relativePath.split(path.sep)[0]\n : \"legacy\";\n channelTypes[channelType] = (channelTypes[channelType] || 0) + 1;\n } catch {\n // Skip unreadable files\n }\n }\n\n return {\n totalFiles: allFiles.length,\n totalEntries,\n oldestFile: sortedFiles[0],\n newestFile: sortedFiles[sortedFiles.length - 1],\n channelTypes,\n };\n } catch (err) {\n log.error(\"failed to get transcript stats:\", err);\n return {\n totalFiles: 0,\n totalEntries: 0,\n oldestFile: null,\n newestFile: null,\n channelTypes: {},\n };\n }\n }\n\n async analyzeIntegrity(): Promise<SessionIntegrityReport> {\n return analyzeSessionIntegrity({ memoryDir: this.config.memoryDir });\n }\n\n async getRecoverySummary(sessionKey?: string): Promise<{\n generatedAt: string;\n sessionKey?: string;\n healthy: boolean;\n issueCount: number;\n incompleteTurns: number;\n brokenChains: number;\n checkpointHealthy: boolean;\n }> {\n const report = await this.analyzeIntegrity();\n const selectedSessions = sessionKey\n ? report.sessions.filter((session) => session.sessionKey === sessionKey)\n : report.sessions;\n const incompleteTurns = selectedSessions.reduce((sum, session) => sum + session.incompleteTurns, 0);\n const brokenChains = selectedSessions.reduce((sum, session) => sum + session.brokenChains, 0);\n const filteredIssues = report.issues.filter((issue) => !sessionKey || issue.sessionKey === sessionKey);\n const issueCount = filteredIssues.length;\n const severeIssueCount = filteredIssues.filter((issue) => issue.severity !== \"info\").length;\n return {\n generatedAt: report.generatedAt,\n sessionKey,\n healthy: sessionKey ? severeIssueCount === 0 && report.checkpoint.healthy : report.healthy,\n issueCount,\n incompleteTurns,\n brokenChains,\n checkpointHealthy: report.checkpoint.healthy,\n };\n }\n}\n"],"mappings":"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","names":[]}