@remnic/core 9.3.648 → 9.3.650
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-cli.js +4 -4
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +4 -4
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +3 -3
- package/dist/{access-service-DFXIlGvZ.d.ts → access-service-DIZRHQ7Q.d.ts} +255 -2
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-TWVRDGTX.js → chunk-23RYLGYA.js} +185 -55
- package/dist/chunk-23RYLGYA.js.map +1 -0
- package/dist/{chunk-CNRZ6WJU.js → chunk-3IJEQWQX.js} +4 -4
- package/dist/{chunk-XUGQQPGO.js → chunk-AGRPGAKR.js} +12 -1
- package/dist/chunk-AGRPGAKR.js.map +1 -0
- package/dist/{chunk-6GIKAUTN.js → chunk-MMJANTJX.js} +33 -2
- package/dist/{chunk-6GIKAUTN.js.map → chunk-MMJANTJX.js.map} +1 -1
- package/dist/{chunk-6BNFVP7Y.js → chunk-RZOBQ23O.js} +2 -2
- package/dist/{chunk-AEIZEAP7.js → chunk-TUMH6EDV.js} +12 -15
- package/dist/chunk-TUMH6EDV.js.map +1 -0
- package/dist/{chunk-FUXV6HSO.js → chunk-TVOPSKOK.js} +3 -3
- package/dist/{chunk-5ETA6OAS.js → chunk-YAFSTKTH.js} +608 -80
- package/dist/chunk-YAFSTKTH.js.map +1 -0
- package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +7 -7
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +3 -3
- package/dist/resume-bundles.js +2 -2
- package/dist/transcript.d.ts +18 -1
- package/dist/transcript.js +5 -3
- package/package.json +1 -1
- package/src/access-service-lcm-forgery.test.ts +410 -0
- package/src/access-service-observe-lcm-parity.test.ts +1397 -0
- package/src/access-service-observe-scope.test.ts +599 -0
- package/src/access-service-raw-excerpt-read-gate.test.ts +443 -0
- package/src/access-service.ts +1270 -113
- package/src/cli.ts +10 -12
- package/src/coding/coding-namespace.test.ts +44 -0
- package/src/coding/coding-namespace.ts +163 -0
- package/src/orchestrator.ts +335 -77
- package/src/transcript-day-range.test.ts +101 -0
- package/src/transcript.ts +26 -0
- package/dist/chunk-5ETA6OAS.js.map +0 -1
- package/dist/chunk-AEIZEAP7.js.map +0 -1
- package/dist/chunk-TWVRDGTX.js.map +0 -1
- package/dist/chunk-XUGQQPGO.js.map +0 -1
- /package/dist/{chunk-CNRZ6WJU.js.map → chunk-3IJEQWQX.js.map} +0 -0
- /package/dist/{chunk-6BNFVP7Y.js.map → chunk-RZOBQ23O.js.map} +0 -0
- /package/dist/{chunk-FUXV6HSO.js.map → chunk-TVOPSKOK.js.map} +0 -0
|
@@ -6,13 +6,13 @@ import {
|
|
|
6
6
|
} from "./chunk-7WV3F5DQ.js";
|
|
7
7
|
import {
|
|
8
8
|
EngramMcpServer
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-TVOPSKOK.js";
|
|
10
10
|
import {
|
|
11
11
|
EngramAccessInputError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YAFSTKTH.js";
|
|
13
13
|
import {
|
|
14
14
|
projectTagProjectId
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-MMJANTJX.js";
|
|
16
16
|
import {
|
|
17
17
|
isTrustZoneName
|
|
18
18
|
} from "./chunk-JGSKJHF7.js";
|
|
@@ -2023,4 +2023,4 @@ function positiveIntQueryParam(value, label) {
|
|
|
2023
2023
|
export {
|
|
2024
2024
|
EngramAccessHttpServer
|
|
2025
2025
|
};
|
|
2026
|
-
//# sourceMappingURL=chunk-
|
|
2026
|
+
//# sourceMappingURL=chunk-3IJEQWQX.js.map
|
|
@@ -22,6 +22,16 @@ function makeRawLineDeduper() {
|
|
|
22
22
|
return true;
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
function utcDayRange(date) {
|
|
26
|
+
const start = `${date}T00:00:00Z`;
|
|
27
|
+
const startMs = Date.parse(start);
|
|
28
|
+
if (Number.isNaN(startMs)) {
|
|
29
|
+
return { start, end: start };
|
|
30
|
+
}
|
|
31
|
+
const next = new Date(startMs);
|
|
32
|
+
next.setUTCDate(next.getUTCDate() + 1);
|
|
33
|
+
return { start, end: `${next.toISOString().slice(0, 10)}T00:00:00Z` };
|
|
34
|
+
}
|
|
25
35
|
var TranscriptManager = class _TranscriptManager {
|
|
26
36
|
transcriptsDir;
|
|
27
37
|
checkpointPath;
|
|
@@ -925,6 +935,7 @@ var TranscriptManager = class _TranscriptManager {
|
|
|
925
935
|
};
|
|
926
936
|
|
|
927
937
|
export {
|
|
938
|
+
utcDayRange,
|
|
928
939
|
TranscriptManager
|
|
929
940
|
};
|
|
930
|
-
//# sourceMappingURL=chunk-
|
|
941
|
+
//# sourceMappingURL=chunk-AGRPGAKR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 * Compute the half-open UTC instant range `[start, end)` that covers an entire\n * calendar day, suitable for {@link TranscriptManager.readRange}.\n *\n * `readRange` filters with an EXCLUSIVE upper bound (`entryTime < end`, CLAUDE.md\n * rule #35 / AGENTS.md rule 23). The end is therefore the NEXT day's\n * `00:00:00Z`, not `${date}T23:59:59Z`: a literal `23:59:59Z` end (== `.000Z`)\n * would drop any entry stamped in the final second of the day\n * (`23:59:59.000Z`–`23:59:59.999Z`). Using the next day's midnight keeps the\n * `[start, end)` semantics intact while including the whole day.\n *\n * @param date - A `YYYY-MM-DD` calendar day (UTC).\n */\nexport function utcDayRange(date: string): { start: string; end: string } {\n const start = `${date}T00:00:00Z`;\n const startMs = Date.parse(start);\n if (Number.isNaN(startMs)) {\n // Malformed date: preserve the pre-existing \"empty range\" behavior (an\n // unparseable bound makes `readRange` match nothing) rather than throwing.\n return { start, end: start };\n }\n const next = new Date(startMs);\n next.setUTCDate(next.getUTCDate() + 1);\n return { start, end: `${next.toISOString().slice(0, 10)}T00:00:00Z` };\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 * The window is half-open `[start, end)`: the upper bound is exclusive so\n * caller-specified ranges (explicit dates, analytics buckets) don't\n * double-count at shared boundaries (CLAUDE.md rule #35 / AGENTS.md rule 23).\n * readRecent's inclusive-of-now behavior is handled by readRecent itself\n * extending its end, not by relaxing this bound.\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 (half-open: exclusive end)\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 now = new Date();\n // The recent-read window is inclusive of \"now\": a turn that is buffered and\n // then immediately read back can carry the same millisecond timestamp as\n // this read, so a half-open [start, now) filter would drop it. Extend the\n // upper bound by 1ms so the exclusive [start, end) filters in\n // readRecentForSession / readRange (which stay half-open for\n // caller-specified ranges, CLAUDE.md rule #35) still capture an entry\n // stamped at exactly \"now\". This is the only place the \"up to now\" boundary\n // is widened; explicit ranges are unaffected.\n const end = new Date(now.getTime() + 1);\n const start = new Date(now.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 // Half-open window: exclusive end. readRecent widens its own end by\n // 1ms so a just-written \"now\" entry is still captured here without\n // relaxing this bound for direct callers (tests/transcript-boundary).\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":[]}
|
|
@@ -272,6 +272,35 @@ function describeCodingScope(codingContext, config, defaultNamespace) {
|
|
|
272
272
|
disabledReason: null
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
+
var LCM_NS_SENTINEL = "";
|
|
276
|
+
function escapeDefaultLcmKey(sessionKey) {
|
|
277
|
+
return sessionKey.startsWith(LCM_NS_SENTINEL) ? `${LCM_NS_SENTINEL}${sessionKey}` : sessionKey;
|
|
278
|
+
}
|
|
279
|
+
function lcmSessionKeyForNamespace(namespace, sessionKey, defaultNamespace) {
|
|
280
|
+
if (typeof sessionKey !== "string" || sessionKey.length === 0) return sessionKey;
|
|
281
|
+
if (typeof namespace === "string" && namespace.length > 0 && namespace !== defaultNamespace) {
|
|
282
|
+
return `${LCM_NS_SENTINEL}${namespace}${LCM_NS_SENTINEL}${sessionKey}`;
|
|
283
|
+
}
|
|
284
|
+
return escapeDefaultLcmKey(sessionKey);
|
|
285
|
+
}
|
|
286
|
+
function lcmReadSessionIdsForNamespaces(namespaces, sessionKey, defaultNamespace) {
|
|
287
|
+
if (typeof sessionKey !== "string" || sessionKey.length === 0) {
|
|
288
|
+
return [void 0];
|
|
289
|
+
}
|
|
290
|
+
const out = [];
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
for (const namespace of namespaces) {
|
|
293
|
+
const key = lcmSessionKeyForNamespace(namespace, sessionKey, defaultNamespace) ?? sessionKey;
|
|
294
|
+
if (!seen.has(key)) {
|
|
295
|
+
seen.add(key);
|
|
296
|
+
out.push(key);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (out.length === 0) {
|
|
300
|
+
out.push(sessionKey);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
275
304
|
|
|
276
305
|
// src/procedural/reinforcement-core.ts
|
|
277
306
|
function clusterByKey(items, keyFn) {
|
|
@@ -303,6 +332,8 @@ export {
|
|
|
303
332
|
combineNamespaces,
|
|
304
333
|
branchNamespaceName,
|
|
305
334
|
resolveCodingNamespaceOverlay,
|
|
306
|
-
describeCodingScope
|
|
335
|
+
describeCodingScope,
|
|
336
|
+
lcmSessionKeyForNamespace,
|
|
337
|
+
lcmReadSessionIdsForNamespaces
|
|
307
338
|
};
|
|
308
|
-
//# sourceMappingURL=chunk-
|
|
339
|
+
//# sourceMappingURL=chunk-MMJANTJX.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/coding/git-context.ts","../src/coding/coding-namespace.ts","../src/procedural/reinforcement-core.ts"],"sourcesContent":["/**\n * GitContextResolver — pure module for detecting the git project + branch\n * a session is operating in.\n *\n * Introduced by issue #569 (coding-agent project/branch-scoped namespaces).\n *\n * This module is deliberately pure:\n * - no orchestrator references\n * - no config side-effects\n * - no namespace wiring\n *\n * Downstream slices (PR 2+ of #569) wire `resolveGitContext` into the\n * `NamespaceResolver` / `Orchestrator` so that memories are scoped to a\n * detected project / branch without leaking across repos.\n *\n * CLAUDE.md rule 17 (expand `~`): the `rootPath` returned here is always an\n * absolute, tilde-expanded path. Callers must not re-expand.\n *\n * CLAUDE.md rule 51 (reject invalid input): `cwd` must be an absolute path\n * and must exist. `resolveGitContext` returns `null` — rather than throwing —\n * when the directory is not inside a git worktree, because being outside a\n * repo is a normal runtime state (e.g. agent opened in a scratch dir).\n */\nimport path from \"node:path\";\n\nimport { expandTildePath } from \"../utils/path.js\";\nimport { launchProcessSync } from \"../runtime/child-process.js\";\n\n// Re-export so existing callers / tests that imported `expandTildePath` from\n// this module keep working. CLAUDE.md #17 requires consistent `~` expansion\n// across every user-facing path input; the canonical implementation now\n// lives in `utils/path.ts`.\nexport { expandTildePath };\n\n// ──────────────────────────────────────────────────────────────────────────\n// Public types\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface GitContext {\n /**\n * Stable identifier for the project. Derived from `git remote get-url origin`\n * when an origin remote is configured, otherwise from the repo root path.\n *\n * Formatted as `origin:<hex>` or `root:<hex>` so that the source is visible\n * to operators (see `remnic doctor`, issue #569 acceptance criteria).\n */\n projectId: string;\n /**\n * Current branch, e.g. `main`, `feat/foo`. `null` only in detached-HEAD\n * state (e.g. rebase in progress). Callers should treat `null` as \"no\n * branch-scope overlay applies\" without erroring.\n */\n branch: string | null;\n /**\n * Absolute path to the repository root (the directory containing `.git`).\n * Tilde-expanded per CLAUDE.md #17.\n */\n rootPath: string;\n /**\n * Best-effort default branch (usually `main` or `master`). Derived from the\n * `refs/remotes/origin/HEAD` symbolic ref. `null` when not available (e.g.\n * fresh clone without a default branch symref, or no origin remote).\n */\n defaultBranch: string | null;\n}\n\n/**\n * Injectable git-invocation surface. Only the commands `resolveGitContext`\n * actually needs are exposed. Tests inject a mock implementation to avoid\n * spawning a real git process.\n */\nexport interface GitInvoker {\n /**\n * Run `git <args>` with `cwd` as the working directory. Must return\n * `{ stdout, exitCode }` with `stdout` trimmed by the caller as needed.\n * Implementations should NOT throw for non-zero exit codes — they should\n * return the exit code so the resolver can decide how to recover.\n */\n (cwd: string, args: string[]): { stdout: string; exitCode: number };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Default git invoker — spawns real `git` via the shared child-process helper\n// ──────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_GIT_TIMEOUT_MS = 2_000;\n\nexport function defaultGitInvoker(): GitInvoker {\n return (cwd: string, args: string[]) => {\n const result = launchProcessSync(\"git\", args, {\n cwd,\n encoding: \"utf-8\",\n timeout: DEFAULT_GIT_TIMEOUT_MS,\n shell: false,\n });\n if (result.error) {\n // Spawn failure (git not on PATH, timeout, etc.). Surface as non-zero.\n return { stdout: \"\", exitCode: 127 };\n }\n return {\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n exitCode: typeof result.status === \"number\" ? result.status : 1,\n };\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Stable hashing\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Non-cryptographic stable hash. Used only to derive a deterministic\n * `projectId` from either the origin URL or the root path. The hash does not\n * need to be collision-resistant against adversarial input — it is purely a\n * namespace discriminator.\n *\n * Uses FNV-1a 32-bit so we don't pull in `node:crypto` for a simple bucket\n * key. Output is lowercase hex, zero-padded to 8 characters.\n */\nexport function stableHash(input: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193) >>> 0;\n }\n return hash.toString(16).padStart(8, \"0\");\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Origin URL normalization\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Normalize a git remote URL so that equivalent SSH / HTTPS forms of the\n * same repo produce the same `projectId`. Handles:\n * - `git@github.com:foo/bar.git` → `github.com/foo/bar`\n * - `https://github.com/foo/bar` → `github.com/foo/bar`\n * - `https://github.com/foo/bar.git` → `github.com/foo/bar`\n * - `ssh://git@github.com/foo/bar` → `github.com/foo/bar`\n * - `ssh://git@github.com:2222/foo/bar` → `github.com/foo/bar` (port stripped)\n *\n * Case-insensitive (remote hostnames and most repo paths on major forges are\n * case-insensitive in practice).\n */\nexport function normalizeOriginUrl(rawUrl: string): string {\n let url = rawUrl.trim();\n if (!url) return \"\";\n\n // Strip trailing `.git` case-insensitively — the whole result is\n // lowercased at the end, so `.GIT` / `.Git` must be treated the same as\n // `.git`. Previously the `.endsWith(\".git\")` check let `.GIT` leak\n // through and appear in the output.\n if (/\\.git$/i.test(url)) url = url.slice(0, -4);\n\n // Windows drive-letter local path (e.g. `C:/repos/app`): detect here\n // so the scp matcher below can accept single-character SSH host aliases\n // (`h:foo/bar` from `.ssh/config`). A drive letter is exactly one ASCII\n // letter followed by `:/` or `:\\`; SSH aliases never have a slash\n // immediately after the colon.\n if (/^[A-Za-z]:[\\\\/]/.test(url)) {\n return url.toLowerCase();\n }\n\n // Protocol-prefixed: ssh://, https://, http://, git://, file://\n // Must be tried FIRST so that scp-style detection below doesn't\n // incorrectly swallow an ssh:// URL that happens to contain `:port/`.\n //\n // Matches:\n // 1: host — bracketed IPv6 `[2001:db8::1]`, plain host with no `:` / `/`,\n // OR empty (for `file:///path` which has no host component).\n // 2: port (optional) — preserved in the output so two repos on the same\n // host under different ports get distinct project namespaces.\n // Losing the port risked false-coalescing separate repos on custom\n // SSH mesh setups.\n // 3: path (optional)\n const protoMatch =\n /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]+@)?(\\[[^\\]]+\\]|[^/:]*)(?::(\\d+))?(\\/.*)?$/i.exec(url);\n if (protoMatch) {\n let host = protoMatch[1] ?? \"\";\n // Detect IPv6 via the bracketed input form BEFORE stripping brackets,\n // so that when we later re-attach a port we can preserve the\n // `[host]:port` boundary. Without the brackets, `host:2222` is\n // ambiguous with a longer bare IPv6 address like `2001:db8::1:2222`.\n const wasBracketed =\n host.startsWith(\"[\") && host.endsWith(\"]\");\n if (wasBracketed) host = host.slice(1, -1);\n const port = protoMatch[2];\n const repoPath = (protoMatch[3] ?? \"\").replace(/^\\/+/, \"\");\n const hostPort = port\n ? wasBracketed\n ? `[${host}]:${port}`\n : `${host}:${port}`\n : host;\n // For protocols without a host component (file:///path), fall back to\n // a stable prefix so distinct local paths don't collapse to \"/path\".\n const prefix = hostPort.length > 0 ? hostPort : \"localhost\";\n return `${prefix}/${repoPath}`.toLowerCase();\n }\n\n // scp-like syntax: [user@]host:path. Protocol-prefixed URLs (`scheme://`)\n // are handled above, so the scp branch below guards against them: a\n // matched `host` of `scheme` followed by a path starting with `//` is\n // a protocol URL that fell through and must NOT be parsed here.\n // `user@` is optional — git also accepts userless scp forms like\n // `host:org/repo`. Valid scp paths may start with digits (e.g.\n // `git@host:123/repo.git`), so no numeric guard is needed: port-bearing\n // URLs have the `://` prefix and match the protocol branch above before\n // reaching here.\n //\n // Windows drive letters were filtered above, so single-character SSH\n // host aliases (`h:foo/bar`) are accepted here.\n //\n // Bracketed IPv6 (`[2001:db8::1]`) is supported: the host alternative\n // matches the bracketed literal up to `]` without splitting on internal\n // `:`. Brackets are stripped in the normalised form so the scp and\n // `ssh://` forms of the same IPv6 remote produce identical projectIds.\n const scpMatch =\n /^(?:([^@\\s/]+)@)?(\\[[^\\]]+\\]|[^:@\\s/]+):(.+)$/.exec(url);\n if (scpMatch) {\n let host = scpMatch[2] ?? \"\";\n if (host.startsWith(\"[\") && host.endsWith(\"]\")) host = host.slice(1, -1);\n const repoPath = scpMatch[3] ?? \"\";\n // Reject protocol-like leftovers (e.g. `file:///path` where the scp\n // regex greedily matched `file` as host and `///path` as path).\n if (repoPath.startsWith(\"//\")) {\n return url.toLowerCase();\n }\n return `${host}/${repoPath.replace(/^\\/+/, \"\")}`.toLowerCase();\n }\n\n // Fallback: use raw lowercased\n return url.toLowerCase();\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Resolver\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface ResolveGitContextOptions {\n /** Inject a git invoker (tests). Defaults to spawning real `git`. */\n invoker?: GitInvoker;\n}\n\n/**\n * Detect the git project + branch for `cwd`.\n *\n * Returns `null` when:\n * - `cwd` is not an absolute path (invalid input, CLAUDE.md #51)\n * - `cwd` is not inside a git worktree\n * - `git` is not available on PATH\n *\n * Never throws.\n */\nexport async function resolveGitContext(\n cwd: string,\n options: ResolveGitContextOptions = {},\n): Promise<GitContext | null> {\n // Wrap the whole body so the documented \"Never throws\" contract is\n // enforced. Possible throw sites include:\n // - `expandTildePath` → `resolveHomeDir()` → `os.homedir()` when HOME\n // is unset (e.g. minimal containers)\n // - a custom `options.invoker` that raises instead of returning a\n // non-zero exitCode\n // - any future helper added to this chain\n // All of those map to \"not in a repo\" / `null`.\n try {\n // Validate input: must be a non-empty string.\n if (typeof cwd !== \"string\" || cwd.length === 0) return null;\n\n // Expand `~` per CLAUDE.md #17, then require absolute path.\n const expanded = expandTildePath(cwd);\n if (!path.isAbsolute(expanded)) return null;\n\n const invoker = options.invoker ?? defaultGitInvoker();\n\n // 1. Locate the repo root.\n const topLevel = invoker(expanded, [\"rev-parse\", \"--show-toplevel\"]);\n if (topLevel.exitCode !== 0) return null;\n const rootPath = topLevel.stdout.trim();\n if (!rootPath) return null;\n\n // 2. Current branch. `--abbrev-ref HEAD` returns `HEAD` in detached\n // state, which we normalize to `null`. On a fresh `git init` the\n // HEAD ref is unborn and `--abbrev-ref HEAD` fails, but\n // `symbolic-ref HEAD` still returns the target branch. Fall back\n // so newly-initialized repos get a sensible branch name.\n const branchResult = invoker(rootPath, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n let branch: string | null = null;\n if (branchResult.exitCode === 0) {\n const raw = branchResult.stdout.trim();\n branch = raw && raw !== \"HEAD\" ? raw : null;\n } else {\n const unbornRef = invoker(rootPath, [\"symbolic-ref\", \"--quiet\", \"HEAD\"]);\n if (unbornRef.exitCode === 0) {\n const raw = unbornRef.stdout.trim();\n const prefix = \"refs/heads/\";\n if (raw.startsWith(prefix)) {\n const candidate = raw.slice(prefix.length);\n if (candidate) branch = candidate;\n }\n }\n }\n\n // 3. Origin URL — optional. Used to derive a stable `projectId`.\n const originResult = invoker(rootPath, [\"remote\", \"get-url\", \"origin\"]);\n let projectId: string;\n if (originResult.exitCode === 0) {\n const normalized = normalizeOriginUrl(originResult.stdout);\n projectId = normalized ? `origin:${stableHash(normalized)}` : `root:${stableHash(rootPath)}`;\n } else {\n projectId = `root:${stableHash(rootPath)}`;\n }\n\n // 4. Default branch — best effort.\n const headRef = invoker(rootPath, [\"symbolic-ref\", \"--quiet\", \"refs/remotes/origin/HEAD\"]);\n let defaultBranch: string | null = null;\n if (headRef.exitCode === 0) {\n const raw = headRef.stdout.trim();\n const prefix = \"refs/remotes/origin/\";\n if (raw.startsWith(prefix)) {\n const candidate = raw.slice(prefix.length);\n if (candidate) defaultBranch = candidate;\n }\n }\n\n return {\n projectId,\n branch,\n rootPath,\n defaultBranch,\n };\n } catch {\n // Never throws — any unexpected error falls back to \"not in a repo\".\n return null;\n }\n}\n","/**\n * Coding-agent namespace overlay (issue #569 PR 2 + PR 3).\n *\n * Given a `CodingContext` (from `resolveGitContext`) and a `CodingModeConfig`,\n * returns the namespace that recall + write paths should use — or `null` when\n * no overlay should apply (coding mode disabled, no context supplied, or\n * feature flags off).\n *\n * PR 2 ships the project overlay. PR 3 will add the branch overlay; the\n * function here already handles both flags so the schema / types / plumbing\n * don't have to change a second time when branch-scope lands.\n *\n * Pure function — no orchestrator, no config side-effects. Callers keep rule\n * 42 (read + write through same namespace layer) by consulting the same\n * function on both paths.\n */\n\nimport type { CodingContext, CodingModeConfig } from \"../types.js\";\nimport { stableHash } from \"./git-context.js\";\n\nexport interface CodingNamespaceOverlay {\n /**\n * Effective namespace to use for this session's memory operations. When\n * `branchScope` is on, takes the form `project:<id>/branch:<b>`; otherwise\n * `project:<id>`.\n */\n namespace: string;\n /**\n * Read fallbacks — additional namespaces a caller should include in recall\n * so that, for example, a branch-scoped session still sees project-level\n * memories that were written before the branch scope was enabled.\n *\n * Writes MUST go to `namespace` only; these are read-side only.\n *\n * Introduced to carry PR 3's branch→project fallback; PR 2 returns an empty\n * array here.\n */\n readFallbacks: string[];\n /**\n * `\"project\"` when only project scope applies, `\"branch\"` when branch scope\n * is also layered on. Used for diagnostics (`remnic doctor`) and logging.\n */\n scope: \"project\" | \"branch\";\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Sanitization\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Normalize a projectId / branch fragment so the resulting namespace passes\n * the router's `isSafeRouteNamespace` check (`[A-Za-z0-9._-]{1,64}`).\n *\n * Namespaces are used as filesystem directory names and must not contain\n * path separators (`/`, `\\`) or colons — so both `:` and `/` collapse to `-`.\n * The project-id format `origin:<8hex>` and branch names like `feat/x` both\n * flow through this helper before hitting the storage layer.\n *\n * NOT a security boundary — projectIds come from `resolveGitContext` (known\n * hex), and branch names come from local git. This defends against corrupt\n * input only.\n */\n/**\n * Single-pass sanitization — each input character is visited exactly once.\n * Rewriting as an explicit loop (instead of chained `replace()` calls with\n * greedy quantifiers) closes the polynomial-backtracking surface that\n * CodeQL flagged on patterns like `-+` and `^-+|-+$`.\n */\nfunction sanitizeFragment(input: string): string {\n if (typeof input !== \"string\") return \"\";\n const trimmed = input.trim().toLowerCase();\n let out = \"\";\n let prevIsDash = true; // suppress leading dashes\n for (let i = 0; i < trimmed.length; i += 1) {\n const c = trimmed[i]!;\n const cc = trimmed.charCodeAt(i);\n const isSafe =\n (cc >= 48 && cc <= 57) /* 0-9 */ ||\n (cc >= 97 && cc <= 122) /* a-z */ ||\n cc === 46 /* . */ ||\n cc === 95 /* _ */;\n if (isSafe) {\n out += c;\n prevIsDash = false;\n } else if (!prevIsDash) {\n out += \"-\";\n prevIsDash = true;\n }\n }\n // Strip a single trailing dash introduced by the final run of unsafe chars.\n if (out.endsWith(\"-\")) out = out.slice(0, -1);\n return out;\n}\n\n/**\n * Cap to the router's per-namespace upper bound.\n *\n * Raw truncation alone would collapse distinct long inputs that differ near\n * the end (e.g. two `feat/...` branches with different suffixes) into the\n * same namespace — silently mixing recall/write state across branches or\n * projects. When truncation is needed, we append a short deterministic\n * hash suffix (`-<8hex>`) derived from the FULL pre-truncated value so\n * collisions only happen under true hash collisions, not simple prefix\n * overlap.\n *\n * The tail is trimmed to leave room for the separator and 8-char hash and\n * any trailing `-` introduced by the slice is stripped so the final\n * character before `-<hash>` is always alphanumeric or `.`/`_`.\n */\nconst MAX_NAMESPACE_LEN = 64;\nconst HASH_SUFFIX_LEN = 9; // \"-\" + 8 hex chars\n\nfunction capLength(value: string): string {\n if (value.length <= MAX_NAMESPACE_LEN) return value;\n // Reuse the FNV-1a 32-bit hash from git-context — one canonical\n // implementation, one set of edge-case fixes. Uses Math.imul for\n // correct 32-bit wrap-around, which plain `*` would not guarantee\n // for the largest intermediate products.\n const hash = stableHash(value);\n // Trim trailing '-' with a linear, non-backtracking loop. A regex\n // like `-+$` is linear too, but an explicit loop keeps CodeQL happy\n // about polynomial backtracking warnings when several `\\-+` patterns\n // appear in the same module.\n let end = MAX_NAMESPACE_LEN - HASH_SUFFIX_LEN;\n while (end > 0 && value.charCodeAt(end - 1) === 45 /* '-' */) end -= 1;\n return `${value.slice(0, end)}-${hash}`;\n}\n\n/**\n * Produce the project-scope namespace name. Exported for tests and for\n * `remnic doctor` to render. Guaranteed to satisfy `isSafeRouteNamespace`:\n * no `/`, no `:`, lowercase only, length-capped to 64 chars.\n */\nexport function projectNamespaceName(projectId: string): string {\n const frag = sanitizeFragment(projectId);\n return capLength(`project-${frag || \"unknown\"}`);\n}\n\nexport function projectTagProjectId(projectTag: string): string {\n const trimmed = projectTag.trim();\n const frag = sanitizeFragment(trimmed);\n const disambig = trimmed.length > 0 && frag !== trimmed;\n const suffix = disambig ? `-${stableHash(trimmed)}` : \"\";\n return `tag:${frag || \"unknown\"}${suffix}`;\n}\n\n/**\n * Preserve case when sanitizing a principal-derived base namespace. The\n * router's `isSafeRouteNamespace` check accepts `[A-Za-z0-9._-]{1,64}`, so\n * upper-case characters in the principal name are safe and MUST be kept to\n * avoid colliding two otherwise-distinct principals (e.g. `Alice` vs\n * `alice`) into the same combined namespace.\n *\n * Otherwise identical to `sanitizeFragment`: single-pass, linear, no\n * polynomial-backtracking quantifiers, unsafe chars collapse to `-` with\n * leading/trailing dashes suppressed.\n */\nfunction sanitizeBaseFragment(input: string): string {\n if (typeof input !== \"string\") return \"\";\n const trimmed = input.trim();\n let out = \"\";\n let prevIsDash = true;\n for (let i = 0; i < trimmed.length; i += 1) {\n const c = trimmed[i]!;\n const cc = trimmed.charCodeAt(i);\n const isSafe =\n (cc >= 48 && cc <= 57) /* 0-9 */ ||\n (cc >= 65 && cc <= 90) /* A-Z */ ||\n (cc >= 97 && cc <= 122) /* a-z */ ||\n cc === 46 /* . */ ||\n cc === 95 /* _ */;\n if (isSafe) {\n out += c;\n prevIsDash = false;\n } else if (!prevIsDash) {\n out += \"-\";\n prevIsDash = true;\n }\n }\n if (out.endsWith(\"-\")) out = out.slice(0, -1);\n return out;\n}\n\n/**\n * Combine a principal-derived base namespace (e.g. `default`, `alice`) with a\n * coding-agent overlay namespace (e.g. `project-origin-abcd1234`). The result\n * is a single safe-route token that preserves principal isolation (CLAUDE.md\n * rule 42: read + write must resolve through the same namespace layer — and\n * here, through the same principal-scoped prefix) while layering project or\n * project/branch scope on top.\n *\n * Multiple principals working in the same repo thus get distinct namespaces:\n *\n * alice + project-origin-ab12 → alice-project-origin-ab12\n * bob + project-origin-ab12 → bob-project-origin-ab12\n * Alice + project-origin-ab12 → Alice-project-origin-ab12 (distinct)\n *\n * The base fragment preserves case so `Alice` and `alice` remain distinct;\n * the overlay fragment is still lowercase-sanitized because it derives from\n * deterministic, pre-lowercased git hashes.\n *\n * Output is re-capped through `capLength` so a very long base + overlay\n * combination still fits inside `isSafeRouteNamespace` (≤ 64 chars). The\n * deterministic hash suffix on truncation keeps distinct inputs distinct.\n */\nexport function combineNamespaces(base: string, overlay: string): string {\n const baseFrag = sanitizeBaseFragment(base);\n const overlayFrag = sanitizeFragment(overlay);\n if (!baseFrag) return capLength(overlayFrag || \"unknown\");\n if (!overlayFrag) return capLength(baseFrag);\n return capLength(`${baseFrag}-${overlayFrag}`);\n}\n\n/**\n * Produce the branch-scope namespace name. Format:\n * `project-<id>-branch-<name>[-<hash>]`. Uses `-` as the structural separator\n * rather than `/` or `:` so the result is a single safe route-namespace\n * token that can be used directly as a filesystem directory.\n *\n * Two failure modes must not collapse distinct branches to one namespace:\n *\n * 1. Sanitization is lossy (`feat/x` and `feat-x` both sanitize to\n * `feat-x`; `Feature` and `feature` both sanitize to `feature`). When\n * sanitization rewrote any character, we append a short hash of the\n * RAW branch so distinct inputs stay distinct.\n * 2. Truncation is applied when the total exceeds 64 chars. In that\n * mode `capLength` appends its own hash of the full pre-truncated\n * value.\n *\n * Long branches that also sanitize may receive both kinds of hashes — that\n * is acceptable: the router only requires the result be unique and\n * deterministic, and the two hashes derive from different domains so they\n * don't conflict.\n */\nexport function branchNamespaceName(projectId: string, branch: string): string {\n const projectFrag = sanitizeFragment(projectId);\n const trimmedBranch = branch.trim();\n const branchFrag = sanitizeFragment(trimmedBranch);\n // Lossy-sanitization disambiguator: append hash of the raw (trimmed)\n // branch when sanitization actually changed the string. Preserves\n // distinctness across `feat/x` vs `feat-x` and `Feature` vs `feature`.\n // The comparison uses the raw trimmed value (NOT `.toLowerCase()`) so\n // case-only variants are treated as lossy and receive their own hash.\n // Empty / already-safe-lowercase inputs get no hash so the common case\n // stays readable.\n const disambig = trimmedBranch.length > 0 && branchFrag !== trimmedBranch;\n const base = `project-${projectFrag || \"unknown\"}-branch-${branchFrag || \"unknown\"}`;\n const suffixed = disambig ? `${base}-${stableHash(trimmedBranch)}` : base;\n return capLength(suffixed);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Overlay resolver\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Compute the namespace overlay for a session.\n *\n * Returns `null` when no overlay applies — callers should then use their\n * existing `defaultNamespaceForPrincipal(...)` result unchanged. This keeps\n * CLAUDE.md #30 (escape hatch): setting `codingMode.projectScope: false`\n * exactly restores pre-#569 behaviour at every call site.\n *\n * @param codingContext — git context from the connector\n * @param config — coding mode flags (projectScope, branchScope, globalFallback)\n * @param defaultNamespace — retained for call-site compatibility; no longer\n * used. The global fallback is expressed as an empty-string sentinel in\n * `readFallbacks`, which `combineNamespaces(principal, \"\")` resolves to the\n * principal's own namespace at the call site.\n */\nexport function resolveCodingNamespaceOverlay(\n codingContext: CodingContext | null | undefined,\n config: Pick<CodingModeConfig, \"projectScope\" | \"branchScope\" | \"globalFallback\">,\n defaultNamespace?: string,\n): CodingNamespaceOverlay | null {\n // No context supplied (session isn't in a git repo, or connector didn't\n // attach one) → no overlay.\n if (!codingContext) return null;\n\n // Project scope disabled → no overlay at all. Branch scope depends on\n // project scope being on; there is no branch-only mode.\n if (!config.projectScope) return null;\n\n // Require a non-empty projectId — defensive.\n const projectId = typeof codingContext.projectId === \"string\" ? codingContext.projectId.trim() : \"\";\n if (!projectId) return null;\n\n const projectNs = projectNamespaceName(projectId);\n\n // Root/global namespace fallback: when `globalFallback` is true, include\n // the principal's self namespace in readFallbacks so cross-project knowledge\n // remains visible. CLAUDE.md #30: the gate is `globalFallback` — set to\n // false for strict project isolation.\n //\n // The fallback value is \"\" (empty string), NOT the defaultNamespace name.\n // The orchestrator passes each fallback through combineNamespaces(principal, fallback),\n // and combineNamespaces(base, \"\") returns base unchanged — yielding the\n // principal's own namespace. Using the actual namespace name (e.g., \"default\")\n // would produce \"default-default\" after combination, missing the target.\n const includeRoot = config.globalFallback === true;\n\n // Branch-scope layering (PR 3):\n // - only when config.branchScope is explicitly true\n // - only when we actually have a branch (null in detached HEAD)\n // - project namespace becomes a read fallback so project-level memories\n // remain visible from any branch (deliberate asymmetry — branch writes\n // don't leak up, but project reads leak down).\n // - when globalFallback is on, the root namespace is also appended so\n // globally useful memories surface in every branch.\n if (config.branchScope && typeof codingContext.branch === \"string\" && codingContext.branch.length > 0) {\n const branchNs = branchNamespaceName(projectId, codingContext.branch);\n const fallbacks = [projectNs];\n if (includeRoot) fallbacks.push(\"\");\n return {\n namespace: branchNs,\n readFallbacks: fallbacks,\n scope: \"branch\",\n };\n }\n\n return {\n namespace: projectNs,\n readFallbacks: includeRoot ? [\"\"] : [],\n scope: \"project\",\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Diagnostics (issue #569 PR 3 + PR 8)\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface CodingScopeDescription {\n /** \"none\" when no overlay is active; otherwise the resolved scope level. */\n scope: \"none\" | \"project\" | \"branch\";\n /** Project id (raw, not sanitized) when a context is attached. */\n projectId: string | null;\n /** Branch name (raw, not sanitized) when available. */\n branch: string | null;\n /** Effective namespace writes route to. `null` when no overlay applies. */\n effectiveNamespace: string | null;\n /** Read fallbacks included in recall (non-empty only when branch-scope is on). */\n readFallbacks: string[];\n /**\n * Why no overlay applies, when `scope === \"none\"`. One of:\n * - `\"no-context\"` — connector didn't attach a CodingContext\n * - `\"disabled\"` — codingMode.projectScope is false\n * - `\"empty-project\"` — codingContext.projectId was empty/whitespace\n */\n disabledReason: \"no-context\" | \"disabled\" | \"empty-project\" | null;\n}\n\n/**\n * Human-readable description of the coding-agent scope that currently applies\n * for a session. Consumed by `remnic doctor` (PR 8) and by logs to surface\n * why recall routes where it does.\n *\n * Pure — callers pass the coding context + config they already have.\n */\nexport function describeCodingScope(\n codingContext: CodingContext | null | undefined,\n config: Pick<CodingModeConfig, \"projectScope\" | \"branchScope\" | \"globalFallback\">,\n defaultNamespace?: string,\n): CodingScopeDescription {\n const projectId = codingContext?.projectId ?? null;\n const branch = codingContext?.branch ?? null;\n\n if (!codingContext) {\n return {\n scope: \"none\",\n projectId: null,\n branch: null,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"no-context\",\n };\n }\n if (!config.projectScope) {\n return {\n scope: \"none\",\n projectId,\n branch,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"disabled\",\n };\n }\n const trimmedId = typeof projectId === \"string\" ? projectId.trim() : \"\";\n if (!trimmedId) {\n return {\n scope: \"none\",\n projectId,\n branch,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"empty-project\",\n };\n }\n\n const overlay = resolveCodingNamespaceOverlay(codingContext, config, defaultNamespace);\n // Unreachable in practice given the guards above, but keep the return\n // shape consistent if the resolver grows new null branches later.\n if (!overlay) {\n return {\n scope: \"none\",\n projectId,\n branch,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"disabled\",\n };\n }\n return {\n scope: overlay.scope,\n projectId,\n branch,\n effectiveNamespace: overlay.namespace,\n readFallbacks: overlay.readFallbacks,\n disabledReason: null,\n };\n}\n","/**\n * Generic reinforcement-core primitives extracted from `procedure-miner.ts`\n * (issue #687 PR 1/4). Procedure-specific scoring (success rate, step\n * normalization) intentionally stays in the miner — this module only\n * exposes category-agnostic clustering and cluster summarization helpers\n * so future PRs can run reinforcement across non-procedural categories.\n *\n * Pure refactor — no behavior change.\n */\n\n/**\n * Group `items` into clusters keyed by `keyFn(item)`.\n *\n * - Preserves the original input order within each cluster's array.\n * - The returned `Map` insertion order matches first-seen key order, so\n * downstream iteration is deterministic for a given input.\n * - Throws `TypeError` if `keyFn` returns a non-string (e.g. `undefined`,\n * `null`, or a number). Callers must produce a stable string key.\n */\nexport function clusterByKey<T>(items: readonly T[], keyFn: (item: T) => string): Map<string, T[]> {\n const clusters = new Map<string, T[]>();\n for (const item of items) {\n const key = keyFn(item);\n if (typeof key !== \"string\") {\n throw new TypeError(\n `clusterByKey: keyFn must return a string, got ${key === null ? \"null\" : typeof key}`,\n );\n }\n const existing = clusters.get(key);\n if (existing) {\n existing.push(item);\n } else {\n clusters.set(key, [item]);\n }\n }\n return clusters;\n}\n\nexport interface ClusterSummary {\n /** Number of items in the cluster. */\n count: number;\n /** Earliest timestamp seen in the cluster (string min via `localeCompare`). */\n firstSeen: string;\n /** Latest timestamp seen in the cluster (string max via `localeCompare`). */\n lastSeen: string;\n}\n\n/**\n * Summarize a cluster by counting items and tracking earliest/latest\n * timestamps. Timestamp comparison uses `String#localeCompare`, which is\n * correct for ISO-8601 strings (lexicographic order matches chronological\n * order).\n *\n * - Throws `RangeError` on empty clusters — `firstSeen`/`lastSeen` are not\n * meaningful without at least one item.\n * - When all timestamps are equal, `firstSeen === lastSeen`.\n */\nexport function summarizeCluster<T>(\n cluster: readonly T[],\n extractTimestamp: (item: T) => string,\n): ClusterSummary {\n if (cluster.length === 0) {\n throw new RangeError(\"summarizeCluster: cluster must contain at least one item\");\n }\n let firstSeen = extractTimestamp(cluster[0]);\n let lastSeen = firstSeen;\n for (let i = 1; i < cluster.length; i += 1) {\n const ts = extractTimestamp(cluster[i]);\n if (ts.localeCompare(firstSeen) < 0) firstSeen = ts;\n if (ts.localeCompare(lastSeen) > 0) lastSeen = ts;\n }\n return { count: cluster.length, firstSeen, lastSeen };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/coding/git-context.ts","../src/coding/coding-namespace.ts","../src/procedural/reinforcement-core.ts"],"sourcesContent":["/**\n * GitContextResolver — pure module for detecting the git project + branch\n * a session is operating in.\n *\n * Introduced by issue #569 (coding-agent project/branch-scoped namespaces).\n *\n * This module is deliberately pure:\n * - no orchestrator references\n * - no config side-effects\n * - no namespace wiring\n *\n * Downstream slices (PR 2+ of #569) wire `resolveGitContext` into the\n * `NamespaceResolver` / `Orchestrator` so that memories are scoped to a\n * detected project / branch without leaking across repos.\n *\n * CLAUDE.md rule 17 (expand `~`): the `rootPath` returned here is always an\n * absolute, tilde-expanded path. Callers must not re-expand.\n *\n * CLAUDE.md rule 51 (reject invalid input): `cwd` must be an absolute path\n * and must exist. `resolveGitContext` returns `null` — rather than throwing —\n * when the directory is not inside a git worktree, because being outside a\n * repo is a normal runtime state (e.g. agent opened in a scratch dir).\n */\nimport path from \"node:path\";\n\nimport { expandTildePath } from \"../utils/path.js\";\nimport { launchProcessSync } from \"../runtime/child-process.js\";\n\n// Re-export so existing callers / tests that imported `expandTildePath` from\n// this module keep working. CLAUDE.md #17 requires consistent `~` expansion\n// across every user-facing path input; the canonical implementation now\n// lives in `utils/path.ts`.\nexport { expandTildePath };\n\n// ──────────────────────────────────────────────────────────────────────────\n// Public types\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface GitContext {\n /**\n * Stable identifier for the project. Derived from `git remote get-url origin`\n * when an origin remote is configured, otherwise from the repo root path.\n *\n * Formatted as `origin:<hex>` or `root:<hex>` so that the source is visible\n * to operators (see `remnic doctor`, issue #569 acceptance criteria).\n */\n projectId: string;\n /**\n * Current branch, e.g. `main`, `feat/foo`. `null` only in detached-HEAD\n * state (e.g. rebase in progress). Callers should treat `null` as \"no\n * branch-scope overlay applies\" without erroring.\n */\n branch: string | null;\n /**\n * Absolute path to the repository root (the directory containing `.git`).\n * Tilde-expanded per CLAUDE.md #17.\n */\n rootPath: string;\n /**\n * Best-effort default branch (usually `main` or `master`). Derived from the\n * `refs/remotes/origin/HEAD` symbolic ref. `null` when not available (e.g.\n * fresh clone without a default branch symref, or no origin remote).\n */\n defaultBranch: string | null;\n}\n\n/**\n * Injectable git-invocation surface. Only the commands `resolveGitContext`\n * actually needs are exposed. Tests inject a mock implementation to avoid\n * spawning a real git process.\n */\nexport interface GitInvoker {\n /**\n * Run `git <args>` with `cwd` as the working directory. Must return\n * `{ stdout, exitCode }` with `stdout` trimmed by the caller as needed.\n * Implementations should NOT throw for non-zero exit codes — they should\n * return the exit code so the resolver can decide how to recover.\n */\n (cwd: string, args: string[]): { stdout: string; exitCode: number };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Default git invoker — spawns real `git` via the shared child-process helper\n// ──────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_GIT_TIMEOUT_MS = 2_000;\n\nexport function defaultGitInvoker(): GitInvoker {\n return (cwd: string, args: string[]) => {\n const result = launchProcessSync(\"git\", args, {\n cwd,\n encoding: \"utf-8\",\n timeout: DEFAULT_GIT_TIMEOUT_MS,\n shell: false,\n });\n if (result.error) {\n // Spawn failure (git not on PATH, timeout, etc.). Surface as non-zero.\n return { stdout: \"\", exitCode: 127 };\n }\n return {\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n exitCode: typeof result.status === \"number\" ? result.status : 1,\n };\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Stable hashing\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Non-cryptographic stable hash. Used only to derive a deterministic\n * `projectId` from either the origin URL or the root path. The hash does not\n * need to be collision-resistant against adversarial input — it is purely a\n * namespace discriminator.\n *\n * Uses FNV-1a 32-bit so we don't pull in `node:crypto` for a simple bucket\n * key. Output is lowercase hex, zero-padded to 8 characters.\n */\nexport function stableHash(input: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193) >>> 0;\n }\n return hash.toString(16).padStart(8, \"0\");\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Origin URL normalization\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Normalize a git remote URL so that equivalent SSH / HTTPS forms of the\n * same repo produce the same `projectId`. Handles:\n * - `git@github.com:foo/bar.git` → `github.com/foo/bar`\n * - `https://github.com/foo/bar` → `github.com/foo/bar`\n * - `https://github.com/foo/bar.git` → `github.com/foo/bar`\n * - `ssh://git@github.com/foo/bar` → `github.com/foo/bar`\n * - `ssh://git@github.com:2222/foo/bar` → `github.com/foo/bar` (port stripped)\n *\n * Case-insensitive (remote hostnames and most repo paths on major forges are\n * case-insensitive in practice).\n */\nexport function normalizeOriginUrl(rawUrl: string): string {\n let url = rawUrl.trim();\n if (!url) return \"\";\n\n // Strip trailing `.git` case-insensitively — the whole result is\n // lowercased at the end, so `.GIT` / `.Git` must be treated the same as\n // `.git`. Previously the `.endsWith(\".git\")` check let `.GIT` leak\n // through and appear in the output.\n if (/\\.git$/i.test(url)) url = url.slice(0, -4);\n\n // Windows drive-letter local path (e.g. `C:/repos/app`): detect here\n // so the scp matcher below can accept single-character SSH host aliases\n // (`h:foo/bar` from `.ssh/config`). A drive letter is exactly one ASCII\n // letter followed by `:/` or `:\\`; SSH aliases never have a slash\n // immediately after the colon.\n if (/^[A-Za-z]:[\\\\/]/.test(url)) {\n return url.toLowerCase();\n }\n\n // Protocol-prefixed: ssh://, https://, http://, git://, file://\n // Must be tried FIRST so that scp-style detection below doesn't\n // incorrectly swallow an ssh:// URL that happens to contain `:port/`.\n //\n // Matches:\n // 1: host — bracketed IPv6 `[2001:db8::1]`, plain host with no `:` / `/`,\n // OR empty (for `file:///path` which has no host component).\n // 2: port (optional) — preserved in the output so two repos on the same\n // host under different ports get distinct project namespaces.\n // Losing the port risked false-coalescing separate repos on custom\n // SSH mesh setups.\n // 3: path (optional)\n const protoMatch =\n /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]+@)?(\\[[^\\]]+\\]|[^/:]*)(?::(\\d+))?(\\/.*)?$/i.exec(url);\n if (protoMatch) {\n let host = protoMatch[1] ?? \"\";\n // Detect IPv6 via the bracketed input form BEFORE stripping brackets,\n // so that when we later re-attach a port we can preserve the\n // `[host]:port` boundary. Without the brackets, `host:2222` is\n // ambiguous with a longer bare IPv6 address like `2001:db8::1:2222`.\n const wasBracketed =\n host.startsWith(\"[\") && host.endsWith(\"]\");\n if (wasBracketed) host = host.slice(1, -1);\n const port = protoMatch[2];\n const repoPath = (protoMatch[3] ?? \"\").replace(/^\\/+/, \"\");\n const hostPort = port\n ? wasBracketed\n ? `[${host}]:${port}`\n : `${host}:${port}`\n : host;\n // For protocols without a host component (file:///path), fall back to\n // a stable prefix so distinct local paths don't collapse to \"/path\".\n const prefix = hostPort.length > 0 ? hostPort : \"localhost\";\n return `${prefix}/${repoPath}`.toLowerCase();\n }\n\n // scp-like syntax: [user@]host:path. Protocol-prefixed URLs (`scheme://`)\n // are handled above, so the scp branch below guards against them: a\n // matched `host` of `scheme` followed by a path starting with `//` is\n // a protocol URL that fell through and must NOT be parsed here.\n // `user@` is optional — git also accepts userless scp forms like\n // `host:org/repo`. Valid scp paths may start with digits (e.g.\n // `git@host:123/repo.git`), so no numeric guard is needed: port-bearing\n // URLs have the `://` prefix and match the protocol branch above before\n // reaching here.\n //\n // Windows drive letters were filtered above, so single-character SSH\n // host aliases (`h:foo/bar`) are accepted here.\n //\n // Bracketed IPv6 (`[2001:db8::1]`) is supported: the host alternative\n // matches the bracketed literal up to `]` without splitting on internal\n // `:`. Brackets are stripped in the normalised form so the scp and\n // `ssh://` forms of the same IPv6 remote produce identical projectIds.\n const scpMatch =\n /^(?:([^@\\s/]+)@)?(\\[[^\\]]+\\]|[^:@\\s/]+):(.+)$/.exec(url);\n if (scpMatch) {\n let host = scpMatch[2] ?? \"\";\n if (host.startsWith(\"[\") && host.endsWith(\"]\")) host = host.slice(1, -1);\n const repoPath = scpMatch[3] ?? \"\";\n // Reject protocol-like leftovers (e.g. `file:///path` where the scp\n // regex greedily matched `file` as host and `///path` as path).\n if (repoPath.startsWith(\"//\")) {\n return url.toLowerCase();\n }\n return `${host}/${repoPath.replace(/^\\/+/, \"\")}`.toLowerCase();\n }\n\n // Fallback: use raw lowercased\n return url.toLowerCase();\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Resolver\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface ResolveGitContextOptions {\n /** Inject a git invoker (tests). Defaults to spawning real `git`. */\n invoker?: GitInvoker;\n}\n\n/**\n * Detect the git project + branch for `cwd`.\n *\n * Returns `null` when:\n * - `cwd` is not an absolute path (invalid input, CLAUDE.md #51)\n * - `cwd` is not inside a git worktree\n * - `git` is not available on PATH\n *\n * Never throws.\n */\nexport async function resolveGitContext(\n cwd: string,\n options: ResolveGitContextOptions = {},\n): Promise<GitContext | null> {\n // Wrap the whole body so the documented \"Never throws\" contract is\n // enforced. Possible throw sites include:\n // - `expandTildePath` → `resolveHomeDir()` → `os.homedir()` when HOME\n // is unset (e.g. minimal containers)\n // - a custom `options.invoker` that raises instead of returning a\n // non-zero exitCode\n // - any future helper added to this chain\n // All of those map to \"not in a repo\" / `null`.\n try {\n // Validate input: must be a non-empty string.\n if (typeof cwd !== \"string\" || cwd.length === 0) return null;\n\n // Expand `~` per CLAUDE.md #17, then require absolute path.\n const expanded = expandTildePath(cwd);\n if (!path.isAbsolute(expanded)) return null;\n\n const invoker = options.invoker ?? defaultGitInvoker();\n\n // 1. Locate the repo root.\n const topLevel = invoker(expanded, [\"rev-parse\", \"--show-toplevel\"]);\n if (topLevel.exitCode !== 0) return null;\n const rootPath = topLevel.stdout.trim();\n if (!rootPath) return null;\n\n // 2. Current branch. `--abbrev-ref HEAD` returns `HEAD` in detached\n // state, which we normalize to `null`. On a fresh `git init` the\n // HEAD ref is unborn and `--abbrev-ref HEAD` fails, but\n // `symbolic-ref HEAD` still returns the target branch. Fall back\n // so newly-initialized repos get a sensible branch name.\n const branchResult = invoker(rootPath, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n let branch: string | null = null;\n if (branchResult.exitCode === 0) {\n const raw = branchResult.stdout.trim();\n branch = raw && raw !== \"HEAD\" ? raw : null;\n } else {\n const unbornRef = invoker(rootPath, [\"symbolic-ref\", \"--quiet\", \"HEAD\"]);\n if (unbornRef.exitCode === 0) {\n const raw = unbornRef.stdout.trim();\n const prefix = \"refs/heads/\";\n if (raw.startsWith(prefix)) {\n const candidate = raw.slice(prefix.length);\n if (candidate) branch = candidate;\n }\n }\n }\n\n // 3. Origin URL — optional. Used to derive a stable `projectId`.\n const originResult = invoker(rootPath, [\"remote\", \"get-url\", \"origin\"]);\n let projectId: string;\n if (originResult.exitCode === 0) {\n const normalized = normalizeOriginUrl(originResult.stdout);\n projectId = normalized ? `origin:${stableHash(normalized)}` : `root:${stableHash(rootPath)}`;\n } else {\n projectId = `root:${stableHash(rootPath)}`;\n }\n\n // 4. Default branch — best effort.\n const headRef = invoker(rootPath, [\"symbolic-ref\", \"--quiet\", \"refs/remotes/origin/HEAD\"]);\n let defaultBranch: string | null = null;\n if (headRef.exitCode === 0) {\n const raw = headRef.stdout.trim();\n const prefix = \"refs/remotes/origin/\";\n if (raw.startsWith(prefix)) {\n const candidate = raw.slice(prefix.length);\n if (candidate) defaultBranch = candidate;\n }\n }\n\n return {\n projectId,\n branch,\n rootPath,\n defaultBranch,\n };\n } catch {\n // Never throws — any unexpected error falls back to \"not in a repo\".\n return null;\n }\n}\n","/**\n * Coding-agent namespace overlay (issue #569 PR 2 + PR 3).\n *\n * Given a `CodingContext` (from `resolveGitContext`) and a `CodingModeConfig`,\n * returns the namespace that recall + write paths should use — or `null` when\n * no overlay should apply (coding mode disabled, no context supplied, or\n * feature flags off).\n *\n * PR 2 ships the project overlay. PR 3 will add the branch overlay; the\n * function here already handles both flags so the schema / types / plumbing\n * don't have to change a second time when branch-scope lands.\n *\n * Pure function — no orchestrator, no config side-effects. Callers keep rule\n * 42 (read + write through same namespace layer) by consulting the same\n * function on both paths.\n */\n\nimport type { CodingContext, CodingModeConfig } from \"../types.js\";\nimport { stableHash } from \"./git-context.js\";\n\nexport interface CodingNamespaceOverlay {\n /**\n * Effective namespace to use for this session's memory operations. When\n * `branchScope` is on, takes the form `project:<id>/branch:<b>`; otherwise\n * `project:<id>`.\n */\n namespace: string;\n /**\n * Read fallbacks — additional namespaces a caller should include in recall\n * so that, for example, a branch-scoped session still sees project-level\n * memories that were written before the branch scope was enabled.\n *\n * Writes MUST go to `namespace` only; these are read-side only.\n *\n * Introduced to carry PR 3's branch→project fallback; PR 2 returns an empty\n * array here.\n */\n readFallbacks: string[];\n /**\n * `\"project\"` when only project scope applies, `\"branch\"` when branch scope\n * is also layered on. Used for diagnostics (`remnic doctor`) and logging.\n */\n scope: \"project\" | \"branch\";\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Sanitization\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Normalize a projectId / branch fragment so the resulting namespace passes\n * the router's `isSafeRouteNamespace` check (`[A-Za-z0-9._-]{1,64}`).\n *\n * Namespaces are used as filesystem directory names and must not contain\n * path separators (`/`, `\\`) or colons — so both `:` and `/` collapse to `-`.\n * The project-id format `origin:<8hex>` and branch names like `feat/x` both\n * flow through this helper before hitting the storage layer.\n *\n * NOT a security boundary — projectIds come from `resolveGitContext` (known\n * hex), and branch names come from local git. This defends against corrupt\n * input only.\n */\n/**\n * Single-pass sanitization — each input character is visited exactly once.\n * Rewriting as an explicit loop (instead of chained `replace()` calls with\n * greedy quantifiers) closes the polynomial-backtracking surface that\n * CodeQL flagged on patterns like `-+` and `^-+|-+$`.\n */\nfunction sanitizeFragment(input: string): string {\n if (typeof input !== \"string\") return \"\";\n const trimmed = input.trim().toLowerCase();\n let out = \"\";\n let prevIsDash = true; // suppress leading dashes\n for (let i = 0; i < trimmed.length; i += 1) {\n const c = trimmed[i]!;\n const cc = trimmed.charCodeAt(i);\n const isSafe =\n (cc >= 48 && cc <= 57) /* 0-9 */ ||\n (cc >= 97 && cc <= 122) /* a-z */ ||\n cc === 46 /* . */ ||\n cc === 95 /* _ */;\n if (isSafe) {\n out += c;\n prevIsDash = false;\n } else if (!prevIsDash) {\n out += \"-\";\n prevIsDash = true;\n }\n }\n // Strip a single trailing dash introduced by the final run of unsafe chars.\n if (out.endsWith(\"-\")) out = out.slice(0, -1);\n return out;\n}\n\n/**\n * Cap to the router's per-namespace upper bound.\n *\n * Raw truncation alone would collapse distinct long inputs that differ near\n * the end (e.g. two `feat/...` branches with different suffixes) into the\n * same namespace — silently mixing recall/write state across branches or\n * projects. When truncation is needed, we append a short deterministic\n * hash suffix (`-<8hex>`) derived from the FULL pre-truncated value so\n * collisions only happen under true hash collisions, not simple prefix\n * overlap.\n *\n * The tail is trimmed to leave room for the separator and 8-char hash and\n * any trailing `-` introduced by the slice is stripped so the final\n * character before `-<hash>` is always alphanumeric or `.`/`_`.\n */\nconst MAX_NAMESPACE_LEN = 64;\nconst HASH_SUFFIX_LEN = 9; // \"-\" + 8 hex chars\n\nfunction capLength(value: string): string {\n if (value.length <= MAX_NAMESPACE_LEN) return value;\n // Reuse the FNV-1a 32-bit hash from git-context — one canonical\n // implementation, one set of edge-case fixes. Uses Math.imul for\n // correct 32-bit wrap-around, which plain `*` would not guarantee\n // for the largest intermediate products.\n const hash = stableHash(value);\n // Trim trailing '-' with a linear, non-backtracking loop. A regex\n // like `-+$` is linear too, but an explicit loop keeps CodeQL happy\n // about polynomial backtracking warnings when several `\\-+` patterns\n // appear in the same module.\n let end = MAX_NAMESPACE_LEN - HASH_SUFFIX_LEN;\n while (end > 0 && value.charCodeAt(end - 1) === 45 /* '-' */) end -= 1;\n return `${value.slice(0, end)}-${hash}`;\n}\n\n/**\n * Produce the project-scope namespace name. Exported for tests and for\n * `remnic doctor` to render. Guaranteed to satisfy `isSafeRouteNamespace`:\n * no `/`, no `:`, lowercase only, length-capped to 64 chars.\n */\nexport function projectNamespaceName(projectId: string): string {\n const frag = sanitizeFragment(projectId);\n return capLength(`project-${frag || \"unknown\"}`);\n}\n\nexport function projectTagProjectId(projectTag: string): string {\n const trimmed = projectTag.trim();\n const frag = sanitizeFragment(trimmed);\n const disambig = trimmed.length > 0 && frag !== trimmed;\n const suffix = disambig ? `-${stableHash(trimmed)}` : \"\";\n return `tag:${frag || \"unknown\"}${suffix}`;\n}\n\n/**\n * Preserve case when sanitizing a principal-derived base namespace. The\n * router's `isSafeRouteNamespace` check accepts `[A-Za-z0-9._-]{1,64}`, so\n * upper-case characters in the principal name are safe and MUST be kept to\n * avoid colliding two otherwise-distinct principals (e.g. `Alice` vs\n * `alice`) into the same combined namespace.\n *\n * Otherwise identical to `sanitizeFragment`: single-pass, linear, no\n * polynomial-backtracking quantifiers, unsafe chars collapse to `-` with\n * leading/trailing dashes suppressed.\n */\nfunction sanitizeBaseFragment(input: string): string {\n if (typeof input !== \"string\") return \"\";\n const trimmed = input.trim();\n let out = \"\";\n let prevIsDash = true;\n for (let i = 0; i < trimmed.length; i += 1) {\n const c = trimmed[i]!;\n const cc = trimmed.charCodeAt(i);\n const isSafe =\n (cc >= 48 && cc <= 57) /* 0-9 */ ||\n (cc >= 65 && cc <= 90) /* A-Z */ ||\n (cc >= 97 && cc <= 122) /* a-z */ ||\n cc === 46 /* . */ ||\n cc === 95 /* _ */;\n if (isSafe) {\n out += c;\n prevIsDash = false;\n } else if (!prevIsDash) {\n out += \"-\";\n prevIsDash = true;\n }\n }\n if (out.endsWith(\"-\")) out = out.slice(0, -1);\n return out;\n}\n\n/**\n * Combine a principal-derived base namespace (e.g. `default`, `alice`) with a\n * coding-agent overlay namespace (e.g. `project-origin-abcd1234`). The result\n * is a single safe-route token that preserves principal isolation (CLAUDE.md\n * rule 42: read + write must resolve through the same namespace layer — and\n * here, through the same principal-scoped prefix) while layering project or\n * project/branch scope on top.\n *\n * Multiple principals working in the same repo thus get distinct namespaces:\n *\n * alice + project-origin-ab12 → alice-project-origin-ab12\n * bob + project-origin-ab12 → bob-project-origin-ab12\n * Alice + project-origin-ab12 → Alice-project-origin-ab12 (distinct)\n *\n * The base fragment preserves case so `Alice` and `alice` remain distinct;\n * the overlay fragment is still lowercase-sanitized because it derives from\n * deterministic, pre-lowercased git hashes.\n *\n * Output is re-capped through `capLength` so a very long base + overlay\n * combination still fits inside `isSafeRouteNamespace` (≤ 64 chars). The\n * deterministic hash suffix on truncation keeps distinct inputs distinct.\n */\nexport function combineNamespaces(base: string, overlay: string): string {\n const baseFrag = sanitizeBaseFragment(base);\n const overlayFrag = sanitizeFragment(overlay);\n if (!baseFrag) return capLength(overlayFrag || \"unknown\");\n if (!overlayFrag) return capLength(baseFrag);\n return capLength(`${baseFrag}-${overlayFrag}`);\n}\n\n/**\n * Produce the branch-scope namespace name. Format:\n * `project-<id>-branch-<name>[-<hash>]`. Uses `-` as the structural separator\n * rather than `/` or `:` so the result is a single safe route-namespace\n * token that can be used directly as a filesystem directory.\n *\n * Two failure modes must not collapse distinct branches to one namespace:\n *\n * 1. Sanitization is lossy (`feat/x` and `feat-x` both sanitize to\n * `feat-x`; `Feature` and `feature` both sanitize to `feature`). When\n * sanitization rewrote any character, we append a short hash of the\n * RAW branch so distinct inputs stay distinct.\n * 2. Truncation is applied when the total exceeds 64 chars. In that\n * mode `capLength` appends its own hash of the full pre-truncated\n * value.\n *\n * Long branches that also sanitize may receive both kinds of hashes — that\n * is acceptable: the router only requires the result be unique and\n * deterministic, and the two hashes derive from different domains so they\n * don't conflict.\n */\nexport function branchNamespaceName(projectId: string, branch: string): string {\n const projectFrag = sanitizeFragment(projectId);\n const trimmedBranch = branch.trim();\n const branchFrag = sanitizeFragment(trimmedBranch);\n // Lossy-sanitization disambiguator: append hash of the raw (trimmed)\n // branch when sanitization actually changed the string. Preserves\n // distinctness across `feat/x` vs `feat-x` and `Feature` vs `feature`.\n // The comparison uses the raw trimmed value (NOT `.toLowerCase()`) so\n // case-only variants are treated as lossy and receive their own hash.\n // Empty / already-safe-lowercase inputs get no hash so the common case\n // stays readable.\n const disambig = trimmedBranch.length > 0 && branchFrag !== trimmedBranch;\n const base = `project-${projectFrag || \"unknown\"}-branch-${branchFrag || \"unknown\"}`;\n const suffixed = disambig ? `${base}-${stableHash(trimmedBranch)}` : base;\n return capLength(suffixed);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Overlay resolver\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Compute the namespace overlay for a session.\n *\n * Returns `null` when no overlay applies — callers should then use their\n * existing `defaultNamespaceForPrincipal(...)` result unchanged. This keeps\n * CLAUDE.md #30 (escape hatch): setting `codingMode.projectScope: false`\n * exactly restores pre-#569 behaviour at every call site.\n *\n * @param codingContext — git context from the connector\n * @param config — coding mode flags (projectScope, branchScope, globalFallback)\n * @param defaultNamespace — retained for call-site compatibility; no longer\n * used. The global fallback is expressed as an empty-string sentinel in\n * `readFallbacks`, which `combineNamespaces(principal, \"\")` resolves to the\n * principal's own namespace at the call site.\n */\nexport function resolveCodingNamespaceOverlay(\n codingContext: CodingContext | null | undefined,\n config: Pick<CodingModeConfig, \"projectScope\" | \"branchScope\" | \"globalFallback\">,\n defaultNamespace?: string,\n): CodingNamespaceOverlay | null {\n // No context supplied (session isn't in a git repo, or connector didn't\n // attach one) → no overlay.\n if (!codingContext) return null;\n\n // Project scope disabled → no overlay at all. Branch scope depends on\n // project scope being on; there is no branch-only mode.\n if (!config.projectScope) return null;\n\n // Require a non-empty projectId — defensive.\n const projectId = typeof codingContext.projectId === \"string\" ? codingContext.projectId.trim() : \"\";\n if (!projectId) return null;\n\n const projectNs = projectNamespaceName(projectId);\n\n // Root/global namespace fallback: when `globalFallback` is true, include\n // the principal's self namespace in readFallbacks so cross-project knowledge\n // remains visible. CLAUDE.md #30: the gate is `globalFallback` — set to\n // false for strict project isolation.\n //\n // The fallback value is \"\" (empty string), NOT the defaultNamespace name.\n // The orchestrator passes each fallback through combineNamespaces(principal, fallback),\n // and combineNamespaces(base, \"\") returns base unchanged — yielding the\n // principal's own namespace. Using the actual namespace name (e.g., \"default\")\n // would produce \"default-default\" after combination, missing the target.\n const includeRoot = config.globalFallback === true;\n\n // Branch-scope layering (PR 3):\n // - only when config.branchScope is explicitly true\n // - only when we actually have a branch (null in detached HEAD)\n // - project namespace becomes a read fallback so project-level memories\n // remain visible from any branch (deliberate asymmetry — branch writes\n // don't leak up, but project reads leak down).\n // - when globalFallback is on, the root namespace is also appended so\n // globally useful memories surface in every branch.\n if (config.branchScope && typeof codingContext.branch === \"string\" && codingContext.branch.length > 0) {\n const branchNs = branchNamespaceName(projectId, codingContext.branch);\n const fallbacks = [projectNs];\n if (includeRoot) fallbacks.push(\"\");\n return {\n namespace: branchNs,\n readFallbacks: fallbacks,\n scope: \"branch\",\n };\n }\n\n return {\n namespace: projectNs,\n readFallbacks: includeRoot ? [\"\"] : [],\n scope: \"project\",\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Diagnostics (issue #569 PR 3 + PR 8)\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface CodingScopeDescription {\n /** \"none\" when no overlay is active; otherwise the resolved scope level. */\n scope: \"none\" | \"project\" | \"branch\";\n /** Project id (raw, not sanitized) when a context is attached. */\n projectId: string | null;\n /** Branch name (raw, not sanitized) when available. */\n branch: string | null;\n /** Effective namespace writes route to. `null` when no overlay applies. */\n effectiveNamespace: string | null;\n /** Read fallbacks included in recall (non-empty only when branch-scope is on). */\n readFallbacks: string[];\n /**\n * Why no overlay applies, when `scope === \"none\"`. One of:\n * - `\"no-context\"` — connector didn't attach a CodingContext\n * - `\"disabled\"` — codingMode.projectScope is false\n * - `\"empty-project\"` — codingContext.projectId was empty/whitespace\n */\n disabledReason: \"no-context\" | \"disabled\" | \"empty-project\" | null;\n}\n\n/**\n * Human-readable description of the coding-agent scope that currently applies\n * for a session. Consumed by `remnic doctor` (PR 8) and by logs to surface\n * why recall routes where it does.\n *\n * Pure — callers pass the coding context + config they already have.\n */\nexport function describeCodingScope(\n codingContext: CodingContext | null | undefined,\n config: Pick<CodingModeConfig, \"projectScope\" | \"branchScope\" | \"globalFallback\">,\n defaultNamespace?: string,\n): CodingScopeDescription {\n const projectId = codingContext?.projectId ?? null;\n const branch = codingContext?.branch ?? null;\n\n if (!codingContext) {\n return {\n scope: \"none\",\n projectId: null,\n branch: null,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"no-context\",\n };\n }\n if (!config.projectScope) {\n return {\n scope: \"none\",\n projectId,\n branch,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"disabled\",\n };\n }\n const trimmedId = typeof projectId === \"string\" ? projectId.trim() : \"\";\n if (!trimmedId) {\n return {\n scope: \"none\",\n projectId,\n branch,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"empty-project\",\n };\n }\n\n const overlay = resolveCodingNamespaceOverlay(codingContext, config, defaultNamespace);\n // Unreachable in practice given the guards above, but keep the return\n // shape consistent if the resolver grows new null branches later.\n if (!overlay) {\n return {\n scope: \"none\",\n projectId,\n branch,\n effectiveNamespace: null,\n readFallbacks: [],\n disabledReason: \"disabled\",\n };\n }\n return {\n scope: overlay.scope,\n projectId,\n branch,\n effectiveNamespace: overlay.namespace,\n readFallbacks: overlay.readFallbacks,\n disabledReason: null,\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// LCM session-key namespacing (#1495)\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Reserved structural sentinel for the namespaced LCM `session_id` encoding\n * (#1495 P1). U+001F (UNIT SEPARATOR) is a C0 control character that CANNOT\n * occur in a route namespace (`isSafeRouteNamespace` ⇒ `[A-Za-z0-9._-]{1,64}`,\n * see routing/engine.ts) and does not occur in any legitimate session key, so\n * it is an unforgeable structural marker for the namespace boundary.\n *\n * SECURITY — why this is unforgeable (the #1495 P1 fix):\n * The LCM archive is keyed by the `session_id` STRING (exact `session_id = ?`\n * and prefix `session_id LIKE '<prefix>%'`), NOT physically partitioned by\n * namespace. The previous encoding `${namespace}:${sessionKey}` shared the SAME\n * string space as a raw default-store key, so a caller authorized for the\n * `default` store could pass a raw `sessionKey` equal to another namespace's\n * encoded key (`\"<overlay-ns>:<victim-session>\"`) and exact-match the victim's\n * rows — a cross-tenant read leak.\n *\n * The new encoding makes the namespaced and default key-spaces PROVABLY\n * DISJOINT:\n * - Overlay key = `\\x1f<namespace>\\x1f<sessionKey>` — always begins with\n * `\\x1f` followed by a NON-`\\x1f` character (the namespace is non-empty and\n * `\\x1f`-free). The leading `\\x1f<namespace>\\x1f` is an unambiguous,\n * injective frame: the namespace cannot contain `\\x1f`, so the second `\\x1f`\n * terminates it without any escaping of the (raw) session key.\n * - Default key = the raw `sessionKey`, UNLESS it already begins with the\n * sentinel, in which case it is escaped to begin with `\\x1f\\x1f` (see\n * `escapeDefaultLcmKey`). A default key therefore NEVER matches the overlay\n * frame `\\x1f<non-\\x1f>…`.\n * Hence no caller-controlled raw `sessionKey` (default path) can reproduce an\n * overlay key, closing the forgery for BOTH the exact-`session_id` match and the\n * `sessionPrefix` LIKE match (an overlay prefix `\\x1f<ns>\\x1f<rawPrefix>` stays a\n * valid LIKE-prefix of the overlay full keys, and a default prefix can only\n * LIKE-match default keys).\n *\n * Existing default-store rows need NO migration: legitimate session keys never\n * contain `\\x1f`, so `escapeDefaultLcmKey` is a no-op for them and they remain\n * byte-for-byte their raw form. The namespaced encoding is NEW in this\n * unreleased PR, so changing its shape costs nothing.\n */\nconst LCM_NS_SENTINEL = \"\\u001f\";\n\n/**\n * Make a default-store (raw) LCM key disjoint from the namespaced key-space.\n *\n * Namespaced overlay keys always begin with `\\x1f` followed by a non-`\\x1f`\n * namespace character. A raw default key collides with that frame ONLY if it\n * begins with `\\x1f`. Legitimate session keys never contain `\\x1f`, so this is a\n * pure no-op for them; a forged key that begins with `\\x1f` is escaped to begin\n * with `\\x1f\\x1f`, which can never equal an overlay key (whose second character\n * is a `[A-Za-z0-9._-]` namespace char, never `\\x1f`).\n */\nfunction escapeDefaultLcmKey(sessionKey: string): string {\n return sessionKey.startsWith(LCM_NS_SENTINEL)\n ? `${LCM_NS_SENTINEL}${sessionKey}`\n : sessionKey;\n}\n\n/**\n * Build the LCM/structured-history `session_id` that a write-producing surface\n * archives under, and that a same-session reader must search under, so reads\n * and writes never drift (#1495, CLAUDE.md rule 42).\n *\n * The LCM archive filters strictly by the `session_id` string, so the writer's\n * archival key and the reader's lookup key MUST agree byte-for-byte. The\n * encoding frames the namespace with the reserved {@link LCM_NS_SENTINEL}\n * (`\\x1f<namespace>\\x1f<sessionKey>`) whenever that namespace diverges from the\n * single-store default; otherwise it passes the (escaped) raw `sessionKey` so\n * single-user / no-overlay deployments keep pre-#1495 behavior exactly. The two\n * key-spaces are provably disjoint, so a caller-controlled raw `sessionKey`\n * cannot forge another namespace's encoded id (see the {@link LCM_NS_SENTINEL}\n * doc comment for the full security rationale).\n *\n * `observe`, compaction flush/record, and the orchestrator recall readers all\n * route through this one helper so a project-scoped (cwd/projectTag) or\n * explicit-namespace session reads its own compressed-history / structured /\n * targeted-fact evidence instead of missing it.\n */\nexport function lcmSessionKeyForNamespace(\n namespace: string | undefined,\n sessionKey: string | undefined,\n defaultNamespace: string,\n): string | undefined {\n if (typeof sessionKey !== \"string\" || sessionKey.length === 0) return sessionKey;\n if (\n typeof namespace === \"string\" &&\n namespace.length > 0 &&\n namespace !== defaultNamespace\n ) {\n // Namespaced (overlay / explicit) key: frame the namespace with the reserved\n // sentinel so the boundary is unambiguous AND unforgeable from the default\n // key-space. The namespace is guaranteed `\\x1f`-free by `isSafeRouteNamespace`.\n return `${LCM_NS_SENTINEL}${namespace}${LCM_NS_SENTINEL}${sessionKey}`;\n }\n // Default store: raw sessionKey, escaped only if it would otherwise intrude on\n // the namespaced key-space (no-op for every legitimate key).\n return escapeDefaultLcmKey(sessionKey);\n}\n\n/**\n * Map an ORDERED, read-authorized namespace set (the SAME set normal QMD/file\n * recall searches) to the ordered set of LCM `session_id`s a same-session reader\n * must query (#1505 thread \"Include coding fallback namespaces in LCM reads\").\n *\n * The LCM archive filters strictly by `session_id`, and `observe` archives each\n * turn under `${effectiveNamespace}:${sessionKey}` for the namespace that was\n * effective when it was written. A branch-scoped session that overlays\n * `${base-project-*-branch-*}` only sees rows written under THAT namespace if it\n * reads a single overlay key — but normal recall ALSO searches the\n * `codingOverlay.readFallbacks` (project / root) namespaces, so rows archived at\n * project/root scope are surfaced by QMD/file recall yet MISSED by a single-key\n * LCM read. Deriving the LCM read keys from the SAME `recallNamespaces` set keeps\n * the LCM read path from diverging: every namespace recall is authorized to read\n * (read-auth gate already applied upstream in `recallNamespaces`) contributes one\n * LCM key, ordered primary-overlay-first then fallbacks. Unreadable namespaces\n * are never in `recallNamespaces`, so they are never searched here either (no\n * cross-tenant read leak).\n *\n * Single-user / no-overlay recall passes a single-namespace set that collapses to\n * the raw `sessionKey`, so the result is `[sessionKey]` — byte-for-byte the\n * pre-#1505 single-key behavior.\n *\n * SESSIONLESS recall (`sessionKey === undefined`): returns `[undefined]` so the\n * caller issues ONE archive-wide LCM read with no exact `session_id` filter —\n * byte-for-byte the pre-#1505 sessionless behavior. It must NOT substitute the\n * literal `\"default\"` session id (codex P2 \"Preserve unscoped LCM searches\n * without a session key\"): that would filter to a session literally named\n * `default`, silently dropping the explicit-cue / targeted / focused / response /\n * event LCM sections for every recall that omits a session key.\n *\n * The result is deduped while preserving first-seen order so the caller can query\n * keys in priority order and short-circuit on the first hit without re-querying an\n * identical key (e.g. when two namespaces both collapse to the default store).\n */\nexport function lcmReadSessionIdsForNamespaces(\n namespaces: readonly string[],\n sessionKey: string | undefined,\n defaultNamespace: string,\n): Array<string | undefined> {\n // Sessionless ⇒ a single archive-wide read (no `session_id` filter). NEVER the\n // literal \"default\" session id (codex P2).\n if (typeof sessionKey !== \"string\" || sessionKey.length === 0) {\n return [undefined];\n }\n const out: string[] = [];\n const seen = new Set<string>();\n for (const namespace of namespaces) {\n const key =\n lcmSessionKeyForNamespace(namespace, sessionKey, defaultNamespace) ??\n sessionKey;\n if (!seen.has(key)) {\n seen.add(key);\n out.push(key);\n }\n }\n if (out.length === 0) {\n out.push(sessionKey);\n }\n return out;\n}\n","/**\n * Generic reinforcement-core primitives extracted from `procedure-miner.ts`\n * (issue #687 PR 1/4). Procedure-specific scoring (success rate, step\n * normalization) intentionally stays in the miner — this module only\n * exposes category-agnostic clustering and cluster summarization helpers\n * so future PRs can run reinforcement across non-procedural categories.\n *\n * Pure refactor — no behavior change.\n */\n\n/**\n * Group `items` into clusters keyed by `keyFn(item)`.\n *\n * - Preserves the original input order within each cluster's array.\n * - The returned `Map` insertion order matches first-seen key order, so\n * downstream iteration is deterministic for a given input.\n * - Throws `TypeError` if `keyFn` returns a non-string (e.g. `undefined`,\n * `null`, or a number). Callers must produce a stable string key.\n */\nexport function clusterByKey<T>(items: readonly T[], keyFn: (item: T) => string): Map<string, T[]> {\n const clusters = new Map<string, T[]>();\n for (const item of items) {\n const key = keyFn(item);\n if (typeof key !== \"string\") {\n throw new TypeError(\n `clusterByKey: keyFn must return a string, got ${key === null ? \"null\" : typeof key}`,\n );\n }\n const existing = clusters.get(key);\n if (existing) {\n existing.push(item);\n } else {\n clusters.set(key, [item]);\n }\n }\n return clusters;\n}\n\nexport interface ClusterSummary {\n /** Number of items in the cluster. */\n count: number;\n /** Earliest timestamp seen in the cluster (string min via `localeCompare`). */\n firstSeen: string;\n /** Latest timestamp seen in the cluster (string max via `localeCompare`). */\n lastSeen: string;\n}\n\n/**\n * Summarize a cluster by counting items and tracking earliest/latest\n * timestamps. Timestamp comparison uses `String#localeCompare`, which is\n * correct for ISO-8601 strings (lexicographic order matches chronological\n * order).\n *\n * - Throws `RangeError` on empty clusters — `firstSeen`/`lastSeen` are not\n * meaningful without at least one item.\n * - When all timestamps are equal, `firstSeen === lastSeen`.\n */\nexport function summarizeCluster<T>(\n cluster: readonly T[],\n extractTimestamp: (item: T) => string,\n): ClusterSummary {\n if (cluster.length === 0) {\n throw new RangeError(\"summarizeCluster: cluster must contain at least one item\");\n }\n let firstSeen = extractTimestamp(cluster[0]);\n let lastSeen = firstSeen;\n for (let i = 1; i < cluster.length; i += 1) {\n const ts = extractTimestamp(cluster[i]);\n if (ts.localeCompare(firstSeen) < 0) firstSeen = ts;\n if (ts.localeCompare(lastSeen) > 0) lastSeen = ts;\n }\n return { count: cluster.length, firstSeen, lastSeen };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-ZRWB5D4H.js";
|
|
5
5
|
import {
|
|
6
6
|
TranscriptManager
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AGRPGAKR.js";
|
|
8
8
|
import {
|
|
9
9
|
resolveCommitmentLedgerDir,
|
|
10
10
|
validateCommitmentLedgerEntry
|
|
@@ -268,4 +268,4 @@ export {
|
|
|
268
268
|
recordResumeBundle,
|
|
269
269
|
getResumeBundleStatus
|
|
270
270
|
};
|
|
271
|
-
//# sourceMappingURL=chunk-
|
|
271
|
+
//# sourceMappingURL=chunk-RZOBQ23O.js.map
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
buildResumeBundleFromState,
|
|
80
80
|
getResumeBundleStatus,
|
|
81
81
|
recordResumeBundle
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-RZOBQ23O.js";
|
|
83
83
|
import {
|
|
84
84
|
parseXrayCliOptions
|
|
85
85
|
} from "./chunk-WLGE6KEO.js";
|
|
@@ -140,6 +140,9 @@ import {
|
|
|
140
140
|
recordWorkProductLedgerEntry,
|
|
141
141
|
searchWorkProductLedgerEntries
|
|
142
142
|
} from "./chunk-ZRWB5D4H.js";
|
|
143
|
+
import {
|
|
144
|
+
utcDayRange
|
|
145
|
+
} from "./chunk-AGRPGAKR.js";
|
|
143
146
|
import {
|
|
144
147
|
analyzeSessionIntegrity,
|
|
145
148
|
applySessionRepair,
|
|
@@ -213,16 +216,16 @@ import {
|
|
|
213
216
|
} from "./chunk-OADWQ5CR.js";
|
|
214
217
|
import {
|
|
215
218
|
EngramAccessHttpServer
|
|
216
|
-
} from "./chunk-
|
|
219
|
+
} from "./chunk-3IJEQWQX.js";
|
|
217
220
|
import {
|
|
218
221
|
WearablesInputError
|
|
219
222
|
} from "./chunk-7WV3F5DQ.js";
|
|
220
223
|
import {
|
|
221
224
|
EngramMcpServer
|
|
222
|
-
} from "./chunk-
|
|
225
|
+
} from "./chunk-TVOPSKOK.js";
|
|
223
226
|
import {
|
|
224
227
|
EngramAccessService
|
|
225
|
-
} from "./chunk-
|
|
228
|
+
} from "./chunk-YAFSTKTH.js";
|
|
226
229
|
import {
|
|
227
230
|
WorkStorage
|
|
228
231
|
} from "./chunk-GDB4J2H3.js";
|
|
@@ -6321,11 +6324,8 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
|
|
|
6321
6324
|
}
|
|
6322
6325
|
}
|
|
6323
6326
|
if (date) {
|
|
6324
|
-
const
|
|
6325
|
-
|
|
6326
|
-
`${date}T23:59:59Z`,
|
|
6327
|
-
channel
|
|
6328
|
-
);
|
|
6327
|
+
const { start, end } = utcDayRange(date);
|
|
6328
|
+
const entries = await orchestrator.transcript.readRange(start, end, channel);
|
|
6329
6329
|
console.log(formatTranscript(entries));
|
|
6330
6330
|
} else if (recent) {
|
|
6331
6331
|
const hours = parseDuration(recent);
|
|
@@ -6333,11 +6333,8 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
|
|
|
6333
6333
|
console.log(formatTranscript(entries));
|
|
6334
6334
|
} else {
|
|
6335
6335
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6336
|
-
const
|
|
6337
|
-
|
|
6338
|
-
`${today}T23:59:59Z`,
|
|
6339
|
-
channel
|
|
6340
|
-
);
|
|
6336
|
+
const { start, end } = utcDayRange(today);
|
|
6337
|
+
const entries = await orchestrator.transcript.readRange(start, end, channel);
|
|
6341
6338
|
console.log(formatTranscript(entries));
|
|
6342
6339
|
}
|
|
6343
6340
|
});
|
|
@@ -7215,4 +7212,4 @@ export {
|
|
|
7215
7212
|
resolveMemoryDirForNamespace,
|
|
7216
7213
|
registerCli
|
|
7217
7214
|
};
|
|
7218
|
-
//# sourceMappingURL=chunk-
|
|
7215
|
+
//# sourceMappingURL=chunk-TUMH6EDV.js.map
|