@remnic/core 9.63.2 → 9.63.4
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-admin-ops-surface.d.ts +4 -4
- package/dist/access-admin-ops-surface.js +1 -1
- package/dist/access-authorization-probe.d.ts +4 -4
- package/dist/access-authorization-probe.js +2 -2
- package/dist/access-boundary.d.ts +4 -4
- package/dist/access-boundary.js +1 -1
- package/dist/access-cli.js +19 -18
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +4 -4
- package/dist/access-extraction-force-flush.js +1 -1
- package/dist/access-http-lcm-compaction.d.ts +4 -4
- package/dist/access-http-lifecycle-flush.d.ts +4 -4
- package/dist/access-http-offline-stream.d.ts +4 -4
- package/dist/access-http-query.d.ts +13 -1
- package/dist/access-http-query.js +5 -2
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +12 -11
- package/dist/access-identity-continuity-surface.d.ts +3 -3
- package/dist/access-identity-continuity-surface.js +1 -1
- package/dist/access-lcm-surface.d.ts +4 -4
- package/dist/access-lcm-surface.js +1 -1
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +6 -6
- package/dist/access-namespace-preflight.js +1 -1
- package/dist/access-observe-write-surface.d.ts +4 -4
- package/dist/access-observe-write-surface.js +1 -1
- package/dist/access-offline-manifest.d.ts +3 -3
- package/dist/access-operations-batch.js +2 -2
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +5 -5
- package/dist/access-recall-concurrency.d.ts +4 -4
- package/dist/access-recall-concurrency.js +1 -1
- package/dist/access-recall-response.d.ts +4 -4
- package/dist/access-recall-response.js +1 -1
- package/dist/access-recall-surface.d.ts +4 -4
- package/dist/access-recall-surface.js +1 -1
- package/dist/{access-service-DBpEnlzA.d.ts → access-service-D-bi3r01.d.ts} +2 -8
- package/dist/access-service-helpers.d.ts +4 -4
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +1 -1
- package/dist/access-surface-catalog.d.ts +4 -4
- package/dist/access-surface-catalog.js +3 -3
- package/dist/bootstrap.d.ts +3 -3
- package/dist/buffer.js +1 -1
- package/dist/{catalog-CyYTf_ok.d.ts → catalog-BYIHQw1l.d.ts} +2 -10
- package/dist/{chunk-JJ7JW62R.js → chunk-2JL7WPXL.js} +1 -11
- package/dist/chunk-2JL7WPXL.js.map +1 -0
- package/dist/{chunk-AJSZ2EK6.js → chunk-3USQWITL.js} +2 -2
- package/dist/{chunk-RYGJGXL6.js → chunk-4EDAHS6D.js} +2 -2
- package/dist/{chunk-5W5N7XJZ.js → chunk-A4HXWNST.js} +19 -752
- package/dist/chunk-A4HXWNST.js.map +1 -0
- package/dist/{chunk-KWN6NOHQ.js → chunk-BXOZALJ6.js} +2 -2
- package/dist/{chunk-6F2UMP4H.js → chunk-CKWOMJDY.js} +2 -2
- package/dist/{chunk-MR5HWQHS.js → chunk-DFEYRCG2.js} +2 -2
- package/dist/{chunk-6UZR7KEK.js → chunk-DQV3LUQL.js} +7 -1
- package/dist/chunk-DQV3LUQL.js.map +1 -0
- package/dist/{chunk-EJQXDKYK.js → chunk-EJCN5ATI.js} +3 -3
- package/dist/{chunk-TRPX6F2G.js → chunk-GNKY5JEP.js} +10 -13
- package/dist/chunk-GNKY5JEP.js.map +1 -0
- package/dist/{chunk-XZPSBGHA.js → chunk-ISFUFLFM.js} +2 -2
- package/dist/{chunk-4NMVX5U7.js → chunk-JSH2TV34.js} +2 -2
- package/dist/{chunk-R74NCKEY.js → chunk-LNFYISZ6.js} +2 -2
- package/dist/{chunk-6ZYQ36IB.js → chunk-MRJ43KSG.js} +15 -2
- package/dist/chunk-MRJ43KSG.js.map +1 -0
- package/dist/{chunk-IM56VPKO.js → chunk-OR7KEGLR.js} +2 -2
- package/dist/{chunk-A73QHOZP.js → chunk-QOMUE4FS.js} +2 -2
- package/dist/chunk-RSLRGRBJ.js +727 -0
- package/dist/chunk-RSLRGRBJ.js.map +1 -0
- package/dist/{chunk-44KHFMCM.js → chunk-VG6DC2GO.js} +32 -17
- package/dist/{chunk-44KHFMCM.js.map → chunk-VG6DC2GO.js.map} +1 -1
- package/dist/{chunk-5UFQCU6M.js → chunk-ZUZD2DRV.js} +5 -5
- package/dist/{cli-DJwNNDL1.d.ts → cli-DEoABKXH.d.ts} +2 -2
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +15 -14
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/external-wiki-access.d.ts +4 -4
- package/dist/external-wiki-access.js +2 -2
- package/dist/external-wiki-mcp-tools.d.ts +4 -4
- package/dist/index.d.ts +8 -8
- package/dist/index.js +21 -20
- package/dist/{maintenance-uJb74j64.d.ts → maintenance-N5ur13Al.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/operator-toolkit.js +3 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +3 -2
- package/dist/{orchestrator-CFi-5JSR.d.ts → orchestrator-ByLrpav4.d.ts} +2 -2
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +19 -18
- package/dist/{public-http-43z1YT-k.d.ts → public-http-DusietP2.d.ts} +1 -1
- package/dist/support-passport/index.d.ts +7 -7
- package/dist/support-passport/index.js +4 -4
- package/package.json +2 -2
- package/src/access-http-query.ts +12 -0
- package/src/access-http.test.ts +32 -0
- package/src/access-http.ts +3 -7
- package/src/admin/admin-surfaces.test.ts +5 -10
- package/src/admin/admin-surfaces.ts +0 -16
- package/src/binary-lifecycle/pipeline.test.ts +1 -1
- package/src/binary-lifecycle/pipeline.ts +23 -9
- package/src/buffer.ts +6 -0
- package/src/maintenance/namespace-planner.ts +0 -2
- package/src/namespaces/catalog.test.ts +53 -8
- package/src/namespaces/catalog.ts +5 -37
- package/src/orchestration/turn-ingestion-snapshot.test.ts +167 -0
- package/src/orchestration/turn-ingestion.ts +2 -1
- package/dist/chunk-5W5N7XJZ.js.map +0 -1
- package/dist/chunk-6UZR7KEK.js.map +0 -1
- package/dist/chunk-6ZYQ36IB.js.map +0 -1
- package/dist/chunk-JJ7JW62R.js.map +0 -1
- package/dist/chunk-TRPX6F2G.js.map +0 -1
- /package/dist/{chunk-AJSZ2EK6.js.map → chunk-3USQWITL.js.map} +0 -0
- /package/dist/{chunk-RYGJGXL6.js.map → chunk-4EDAHS6D.js.map} +0 -0
- /package/dist/{chunk-KWN6NOHQ.js.map → chunk-BXOZALJ6.js.map} +0 -0
- /package/dist/{chunk-6F2UMP4H.js.map → chunk-CKWOMJDY.js.map} +0 -0
- /package/dist/{chunk-MR5HWQHS.js.map → chunk-DFEYRCG2.js.map} +0 -0
- /package/dist/{chunk-EJQXDKYK.js.map → chunk-EJCN5ATI.js.map} +0 -0
- /package/dist/{chunk-XZPSBGHA.js.map → chunk-ISFUFLFM.js.map} +0 -0
- /package/dist/{chunk-4NMVX5U7.js.map → chunk-JSH2TV34.js.map} +0 -0
- /package/dist/{chunk-R74NCKEY.js.map → chunk-LNFYISZ6.js.map} +0 -0
- /package/dist/{chunk-IM56VPKO.js.map → chunk-OR7KEGLR.js.map} +0 -0
- /package/dist/{chunk-A73QHOZP.js.map → chunk-QOMUE4FS.js.map} +0 -0
- /package/dist/{chunk-5UFQCU6M.js.map → chunk-ZUZD2DRV.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/namespaces/catalog.ts"],"sourcesContent":["import { resolveNamespaceCapabilities } from \"../capabilities.js\";\nimport path from \"node:path\";\nimport type { Dirent } from \"node:fs\";\nimport {\n appendFile,\n lstat,\n mkdir,\n open,\n readdir,\n realpath,\n rename,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport type { PluginConfig } from \"../types.js\";\nimport {\n MutationSerializer,\n withHeldFileLock,\n} from \"../utils/serialize-mutations.js\";\nimport { isSafeRouteNamespace } from \"../routing/engine.js\";\nimport { namespaceIdentityFromToken, namespaceIdentityLegacyToken, namespaceIdentityToken, normalizeNamespaceIdentity } from \"./identity.js\";\nimport { resolveDefaultNamespaceRoot, resolveNamespaceStorageRoot } from \"./storage.js\";\nimport { ALL_CATEGORY_DIRS } from \"../utils/category-dir.js\";\n\n/**\n * Rebuildable namespace catalog (issue #1499).\n *\n * Purpose: a downstream, rebuildable metadata index that lets Remnic ENUMERATE\n * the configured and dynamically-created namespaces that exist or should be\n * maintained. Filesystem memory remains the single source of truth; the catalog\n * is derived metadata and can always be reconstructed from disk.\n *\n * Storage format: `<memoryDir>/state/namespaces.jsonl` — an append-and-compact\n * JSON-lines log. We chose this over per-namespace sidecar files because:\n * - touches (markRead/markWrite/markMaintenance) are cheap single appends;\n * - it is naturally audit-friendly (the raw log preserves touch history);\n * - a single file makes enumeration trivial (no directory walk per call);\n * - last-record-wins compaction folds the log into the current state on read,\n * and `rebuildFromDisk` rewrites it atomically (temp file + rename).\n *\n * SECURITY:\n * - The catalog stores ONLY metadata (namespace names, kinds, timestamps,\n * resolved storage dirs). It NEVER holds raw memory content or secrets.\n * - Catalog presence grants NO authorization. Read/write access still flows\n * through the namespace policies in `principal.ts`; this module never makes\n * an access decision.\n * - All namespace tokens are validated with `isSafeRouteNamespace` (except the\n * configured default namespace, which is exempt at the routing layer) and\n * every storage dir is contained under `<memoryDir>/namespaces`.\n * - `rebuildFromDisk` rejects/reports symlinked roots that escape the memory\n * root rather than trusting them.\n *\n * LIFECYCLE: catalog write failures must NEVER crash a primary memory op.\n * Callers should wrap touch calls in try/catch (or rely on the internal\n * failure-tolerant append). The internal serialized write chain recovers from\n * rejection so one failed append cannot poison subsequent writes.\n */\n\nexport type NamespaceKind =\n | \"default\"\n | \"shared\"\n | \"project\"\n | \"branch\"\n | \"team-project\"\n | \"explicit\";\n\nexport type NamespaceDiscoverySource = \"config\" | \"write\" | \"read\" | \"scan\" | \"migration\";\n\nexport interface NamespaceRecord {\n namespace: string;\n identityToken: string;\n kind: NamespaceKind;\n createdAt: string;\n lastReadAt?: string;\n lastWriteAt?: string;\n lastMaintenanceAt?: Record<string, string>;\n storageDir: string;\n discoveredBy: NamespaceDiscoverySource;\n}\n\nexport interface NamespaceCatalogFilter {\n kind?: NamespaceKind;\n discoveredBy?: NamespaceDiscoverySource;\n /** Only include namespaces written since this instant (inclusive lower bound). */\n writtenSince?: Date;\n}\n\nexport interface NamespaceTouchMetadata {\n discoveredBy?: NamespaceDiscoverySource;\n kind?: NamespaceKind;\n /** Explicit storage dir (when the caller already resolved it). */\n storageDir?: string;\n /** Override the touch timestamp (mainly for tests / migration replay). */\n at?: Date;\n}\n\nexport interface NamespaceCatalogSkippedRoot {\n token: string;\n reason: \"symlink\" | \"escape\" | \"unsafe\" | \"error\";\n detail?: string;\n}\n\nexport interface NamespaceCatalogRebuildResult {\n dryRun: boolean;\n records: NamespaceRecord[];\n /** Roots reported as ambiguous/unsafe rather than silently misclassified. */\n skipped: NamespaceCatalogSkippedRoot[];\n /**\n * Whether the rebuild actually rewrote the on-disk catalog (round 6, codex P2\n * / cursor Medium — NBn3n/NBsGG). `false` for a dry-run, AND for an `--apply`\n * that could NOT acquire the cross-process rebuild lock within the bounded wait\n * (it ran compute-only to avoid clobbering a concurrent lock holder). Callers\n * (CLI) must NOT report unqualified success when `applied` is false for a\n * non-dry-run — the catalog was left unchanged and a retry is needed.\n */\n applied: boolean;\n}\n\nconst NAMESPACE_KINDS: readonly NamespaceKind[] = [\n \"default\",\n \"shared\",\n \"project\",\n \"branch\",\n \"team-project\",\n \"explicit\",\n];\n\nconst NAMESPACE_DISCOVERY_SOURCES: readonly NamespaceDiscoverySource[] = [\n \"config\",\n \"write\",\n \"read\",\n \"scan\",\n \"migration\",\n];\n\nconst CATALOG_FILE = \"namespaces.jsonl\";\nconst STATE_DIR = \"state\";\nconst REBUILD_LOCK_FILE = \"namespaces.rebuild.lock\";\n// A held lock older than this is treated as stale (a crashed rebuild) and broken.\nconst REBUILD_LOCK_STALE_MS = 30_000;\n// Bounded acquisition: poll briefly, then proceed best-effort rather than block\n// a CLI rebuild forever behind a busy gateway.\nconst REBUILD_LOCK_MAX_WAIT_MS = 5_000;\nconst REBUILD_LOCK_POLL_MS = 50;\n// Heartbeat: while a rebuild holds the lock it refreshes the lock file's mtime\n// on this interval so a long (>STALE_MS) scan is NOT mistaken for a crashed\n// holder and broken out from under it (round 5, cursor/codex Medium/P2). Must be\n// comfortably below STALE_MS so at least a couple of beats land per stale window.\nconst REBUILD_LOCK_HEARTBEAT_MS = 10_000;\n\n// Children that indicate a directory holds Remnic memory data (used for legacy\n// default-root detection and to skip empty/non-data roots during rebuild).\n//\n// `state` is included to MATCH the router's storage-presence check\n// (`NamespaceStorageRouter` counts the `state` runtime child via\n// `includeRuntimeState: true`). Without it (round 3, cursor Medium) a namespace\n// the router actively resolves because it has only a `state/` dir would be\n// treated as absent by rebuild and vanish from the catalog after `--apply`.\nconst MEMORY_DATA_CHILDREN = [\n ...ALL_CATEGORY_DIRS,\n \"entities\",\n \"artifacts\",\n \"identity\",\n \"config\",\n \"summaries\",\n \"profile.md\",\n \"state\",\n] as const;\n\nfunction isCatalogEnabled(config: PluginConfig): boolean {\n // Inert unless namespaces are enabled. namespaceCatalogEnabled defaults to\n // true (undefined => enabled) but is only honored when namespacesEnabled.\n if (resolveNamespaceCapabilities(config).namespaces !== true) return false;\n return (config as { namespaceCatalogEnabled?: boolean }).namespaceCatalogEnabled !== false;\n}\n\n// Marker children that MUST be a regular file rather than a directory. Everything\n// else in MEMORY_DATA_CHILDREN is a category/data DIRECTORY that downstream\n// indexers (`scanMemoryDir`) read — and which they reject when it is a symlink or\n// a non-directory. `profile.md` is the sole file marker.\nconst FILE_MEMORY_DATA_CHILDREN = new Set<string>([\"profile.md\"]);\n\ntype MemoryDataMarkerStatus =\n | { state: \"absent\" }\n | { state: \"valid\" }\n | { state: \"invalid\"; detail: string };\n\ntype MemoryDataRootStatus = {\n hasData: boolean;\n invalidMarker?: string;\n};\n\nfunction isNotFoundError(err: unknown): boolean {\n return (\n typeof err === \"object\" &&\n err !== null &&\n \"code\" in err &&\n (err as { code?: string }).code === \"ENOENT\"\n );\n}\n\n/**\n * Inspect `child` under `rootDir` as a memory-data marker (NIw0F / PR #1506).\n * Existence alone is not enough: a bogus marker — e.g. `facts` as a symlink or a\n * regular file instead of a real directory — passes `lstat` but makes\n * `scanMemoryDir` throw on the symlinked/non-directory category root. Returning\n * a distinct `invalid` status lets root scans reject a namespace when ANY known\n * marker is malformed, even if a sibling marker such as `state/` is valid.\n */\nasync function inspectMemoryDataMarker(rootDir: string, child: string): Promise<MemoryDataMarkerStatus> {\n const childPath = path.join(rootDir, child);\n let entry;\n try {\n entry = await lstat(childPath);\n } catch (err) {\n return isNotFoundError(err)\n ? { state: \"absent\" }\n : { state: \"invalid\", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };\n }\n // Reject symlinked markers outright (scan parity — never follow them).\n if (entry.isSymbolicLink()) return { state: \"invalid\", detail: `${child}: symlink` };\n if (FILE_MEMORY_DATA_CHILDREN.has(child)) {\n // `profile.md` must be a regular file.\n return entry.isFile()\n ? { state: \"valid\" }\n : { state: \"invalid\", detail: `${child}: expected file` };\n }\n // Category/data markers must be real directories whose realpath stays inside\n // the namespace root (no escape via a symlinked ancestor).\n if (!entry.isDirectory()) return { state: \"invalid\", detail: `${child}: expected directory` };\n try {\n const rootReal = await realpath(rootDir);\n const childReal = await realpath(childPath);\n return isPathInside(rootReal, childReal)\n ? { state: \"valid\" }\n : { state: \"invalid\", detail: `${child}: escapes namespace root` };\n } catch (err) {\n return { state: \"invalid\", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };\n }\n}\n\nasync function inspectMemoryDataRoot(rootDir: string): Promise<MemoryDataRootStatus> {\n let hasData = false;\n for (const child of MEMORY_DATA_CHILDREN) {\n const marker = await inspectMemoryDataMarker(rootDir, child);\n if (marker.state === \"invalid\") {\n return { hasData: false, invalidMarker: marker.detail };\n }\n if (marker.state === \"valid\") {\n hasData = true;\n }\n }\n return { hasData };\n}\n\nexport async function hasMemoryData(rootDir: string): Promise<boolean> {\n return (await inspectMemoryDataRoot(rootDir)).hasData;\n}\n\nfunction isValidIsoTimestamp(value: string): boolean {\n const ms = Date.parse(value);\n return Number.isFinite(ms);\n}\n\nexport function isNamespaceKind(value: unknown): value is NamespaceKind {\n return typeof value === \"string\" && (NAMESPACE_KINDS as readonly string[]).includes(value);\n}\n\nfunction isNamespaceDiscoverySource(value: unknown): value is NamespaceDiscoverySource {\n return typeof value === \"string\" && (NAMESPACE_DISCOVERY_SOURCES as readonly string[]).includes(value);\n}\n\n/**\n * Validate a JSONL line parsed value as a usable NamespaceRecord.\n * Rejects null / non-object / missing-field records (CLAUDE.md rule #18).\n * Persisted enum and timestamp fields are also validated here so a syntactically\n * valid but tampered/pre-fix line cannot surface impossible record states.\n */\nfunction coerceRecord(value: unknown): NamespaceRecord | null {\n if (typeof value !== \"object\" || value === null) return null;\n const v = value as Record<string, unknown>;\n if (typeof v.namespace !== \"string\") return null;\n const namespace = normalizeNamespaceIdentity(v.namespace);\n if (namespace.length === 0) return null;\n if (typeof v.identityToken !== \"string\" || v.identityToken.length === 0) return null;\n const expectedIdentityToken = namespaceIdentityToken(namespace);\n if (v.identityToken !== expectedIdentityToken) return null;\n if (typeof v.storageDir !== \"string\" || v.storageDir.length === 0) return null;\n if (typeof v.createdAt !== \"string\" || v.createdAt.length === 0) return null;\n if (!isValidIsoTimestamp(v.createdAt)) return null;\n const kind = isNamespaceKind(v.kind) ? v.kind : \"explicit\";\n const discoveredBy =\n v.discoveredBy === undefined\n ? \"scan\"\n : isNamespaceDiscoverySource(v.discoveredBy)\n ? v.discoveredBy\n : null;\n if (!discoveredBy) return null;\n const record: NamespaceRecord = {\n namespace,\n identityToken: expectedIdentityToken,\n kind,\n createdAt: v.createdAt,\n storageDir: v.storageDir,\n discoveredBy,\n };\n if (typeof v.lastReadAt === \"string\" && isValidIsoTimestamp(v.lastReadAt)) {\n record.lastReadAt = v.lastReadAt;\n }\n if (typeof v.lastWriteAt === \"string\" && isValidIsoTimestamp(v.lastWriteAt)) {\n record.lastWriteAt = v.lastWriteAt;\n }\n if (v.lastMaintenanceAt && typeof v.lastMaintenanceAt === \"object\") {\n const out: Record<string, string> = {};\n for (const [k, val] of Object.entries(v.lastMaintenanceAt as Record<string, unknown>)) {\n if (typeof val === \"string\" && isValidIsoTimestamp(val)) out[k] = val;\n }\n if (Object.keys(out).length > 0) record.lastMaintenanceAt = out;\n }\n return record;\n}\n\n/** Later of two optional ISO timestamps (undefined-safe). */\nfunction laterIso(a: string | undefined, b: string | undefined): string | undefined {\n if (!a) return b;\n if (!b) return a;\n const am = Date.parse(a);\n const bm = Date.parse(b);\n if (!Number.isFinite(am)) return b;\n if (!Number.isFinite(bm)) return a;\n return bm > am ? b : a;\n}\n\n/**\n * Fold the touch fields (lastReadAt / lastWriteAt / lastMaintenanceAt) from a\n * freshly re-read on-disk record into the rebuilt record, taking the LATER\n * timestamp per field (round 5 cross-process re-merge). Disk-derived fields\n * (storageDir, kind, discoveredBy, createdAt, principal hints) are owned by the\n * rebuilt record and left untouched — we only recover touch recency that a\n * concurrent (possibly cross-process) writer recorded after our initial load.\n */\nfunction mergeNewerTouchFields(base: NamespaceRecord, fresh: NamespaceRecord): NamespaceRecord {\n const merged: NamespaceRecord = { ...base };\n const lr = laterIso(base.lastReadAt, fresh.lastReadAt);\n if (lr) merged.lastReadAt = lr;\n const lw = laterIso(base.lastWriteAt, fresh.lastWriteAt);\n if (lw) merged.lastWriteAt = lw;\n if (base.lastMaintenanceAt || fresh.lastMaintenanceAt) {\n const jobs: Record<string, string> = { ...(base.lastMaintenanceAt ?? {}) };\n for (const [job, ts] of Object.entries(fresh.lastMaintenanceAt ?? {})) {\n const latest = laterIso(jobs[job], ts);\n if (latest) jobs[job] = latest;\n }\n if (Object.keys(jobs).length > 0) merged.lastMaintenanceAt = jobs;\n }\n return merged;\n}\n\n/**\n * Serialize a record with sorted keys (CLAUDE.md rule #38) so byte output is\n * stable across runs — required for idempotent rebuilds.\n */\nfunction serializeRecord(record: NamespaceRecord): string {\n const ordered: Record<string, unknown> = {};\n const source = record as unknown as Record<string, unknown>;\n for (const key of Object.keys(source).sort()) {\n const value = source[key];\n if (value === undefined) continue;\n if (key === \"lastMaintenanceAt\" && value && typeof value === \"object\") {\n const sortedJobs: Record<string, string> = {};\n for (const jobKey of Object.keys(value as Record<string, string>).sort()) {\n sortedJobs[jobKey] = (value as Record<string, string>)[jobKey]!;\n }\n ordered[key] = sortedJobs;\n continue;\n }\n ordered[key] = value;\n }\n return JSON.stringify(ordered);\n}\n\n/**\n * Infer the namespace kind from its name/structure using the same conventions\n * as `coding-namespace.ts` (project-*, *-branch-*, team-*-project-*). Returns\n * `explicit` when no structural signal is present. The caller can override.\n */\nfunction inferKind(namespace: string, config: PluginConfig): NamespaceKind {\n // Compare against NORMALIZED config names (NGnek, codex P2): the catalog seeds\n // normalized namespace identities, so a configured name with surrounding\n // whitespace (e.g. `sharedNamespace: \"shared \"`) must still classify the\n // normalized `\"shared\"` as `shared`, not fall through to `explicit`.\n if (namespace === normalizeNamespaceIdentity(config.defaultNamespace)) return \"default\";\n if (namespace === normalizeNamespaceIdentity(config.sharedNamespace)) return \"shared\";\n if (config.namespacePolicies.some((p) => normalizeNamespaceIdentity(p.name) === namespace)) {\n return \"explicit\";\n }\n // Branch overlays embed \"-branch-\" (project-<id>-branch-<name>).\n if (/-branch-|^project-[^-]+-branch-/.test(namespace) || namespace.includes(\"-branch-\")) {\n return \"branch\";\n }\n // Team-project promotions are prefixed team-*-project-*.\n if (/^team-.*-project-/.test(namespace) || /^team-.*project-/.test(namespace)) {\n return \"team-project\";\n }\n // Project overlays are \"project-*\" or \"<principal>-project-*\".\n if (/^project-/.test(namespace) || /-project-/.test(namespace)) {\n return \"project\";\n }\n return \"explicit\";\n}\n\ninterface CatalogFileIdentity {\n size: number;\n mtimeMs: number;\n ctimeMs: number;\n ino: number;\n}\n\ninterface CompactedCatalogCache {\n identity: CatalogFileIdentity;\n records: Map<string, NamespaceRecord>;\n}\n\nexport class NamespaceCatalog {\n private readonly memoryDir: string;\n private readonly stateDir: string;\n private readonly catalogPath: string;\n private readonly rebuildLockPath: string;\n // In-process serialization for catalog mutations (issue #1524 adoption).\n // Replaces the bespoke `writeChain` field: every touch/rebuild runs through\n // this serializer so a single failed section never poisons subsequent ones\n // (CLAUDE.md rule #40 — recovery is the util's contract, not re-implemented\n // here). Cross-process identity (the lock file's owner-uuid) is now per-CALL\n // inside the shared util, which is STRONGER than the previous per-instance\n // `lockOwnerId` — two calls on the SAME instance get different ids, so\n // neither mistakes the other's lock for self-held (round 6, codex P2 — NBsGP\n // invariant preserved and tightened).\n private readonly criticalSection = new MutationSerializer();\n private compactedCache: CompactedCatalogCache | undefined;\n // Test-only seam (round 7 — NEZkA): fires inside a touch's HELD-lock critical\n // section, after the lock is acquired but BEFORE the read→merge→append. A\n // deterministic concurrency test installs a hook here to widen the (otherwise\n // microscopic) window and prove that a cross-process rebuild CANNOT run its\n // load→rename while a touch holds the lock. Never set in production code.\n protected onTouchCriticalSectionForTest?: () => Promise<void>;\n // Test-only observation seam: fires only when the JSONL file is fully read.\n // Cached loads do not invoke it.\n protected onCatalogReadForTest?: () => void;\n // Test-only seam: fires after this process appends but before it stats the\n // file to refresh the cache identity.\n protected onAfterCatalogAppendForTest?: () => Promise<void>;\n // Test-only seam (round 7 — NEZkA): fires inside a mutating rebuild's HELD-lock\n // critical section, after the final cross-process re-merge `loadCompacted()` and\n // BEFORE the atomic `rename()`. This is the EXACT window in which a check-then-\n // append touch (the old bug) would clobber its append. A deterministic test\n // installs a hook here to attempt a cross-instance touch in this window and\n // assert the held mutex blocks it. Never set in production code.\n protected onRebuildBeforeRenameForTest?: () => Promise<void>;\n // Test-only seam (NFgCT, codex P2): fires AFTER the lockless disk scan but\n // BEFORE the rebuild acquires the cross-process file lock for its final\n // load→merge→rename window. A deterministic test installs a hook here to attempt\n // a cross-instance touch DURING the scan window and assert it is NOT blocked or\n // dropped — proving the scan no longer holds the mutex. Never set in production.\n protected onRebuildAfterScanForTest?: () => Promise<void>;\n // Test-only seam (NG7Bg, codex P2): fires inside `breakStaleRebuildLock` AFTER it\n // has judged the lock stale and captured its identity, but BEFORE the final\n // re-validation+unlink. A deterministic test installs a hook here to REPLACE the\n // lock file (a fresh holder created a new lock in the race window) and assert the\n // break is skipped — the replacement's active lock is not deleted. Never set in\n // production.\n protected onBeforeBreakStaleUnlinkForTest?: () => Promise<void>;\n\n // Normalized (trimmed) default namespace identity (NH-FH, cursor Medium).\n // Catalog records key namespaces by their NORMALIZED identity\n // (`normalizeNamespaceIdentity`), but several default-namespace exemptions and\n // memoryDir-ownership checks compared against the RAW `config.defaultNamespace`.\n // If the configured default name carries surrounding whitespace the record key\n // is trimmed while the comparison string is not, so the default row is\n // misclassified, dropped at read time, or given the wrong storage root. Compare\n // against this normalized form everywhere instead.\n private readonly defaultNamespaceIdentity: string;\n\n // Issue #1903 — touch-path performance knobs, resolved with the documented\n // defaults when an externally-built PluginConfig omits them (so a partial\n // config object still yields the production behavior). `0` is a real disable\n // switch for each and is preserved by `??` (0 is not null/undefined).\n private readonly compactBytesLimit: number;\n private readonly readCoalesceMs: number;\n private readonly writeCoalesceMs: number;\n // Size after the most recent auto-compaction (0 = none yet). Hysteresis for\n // maybeAutoCompact: a folded catalog that is itself above the limit (many\n // distinct namespaces) would otherwise be re-folded + re-rewritten on every\n // touch. We only compact again once the log has grown by >= one limit-worth\n // since the last compaction (#1903, Codex).\n private lastCompactedSize = 0;\n\n // Issue #1903 — per-(namespace, kind) coalescing buffer. Keyed `${kind}:${ns}`.\n // Holds the latest buffered timestamp/metadata for a pure-timestamp touch on\n // an already-known record; a `.unref()`'d timer flushes it once per window so\n // the append-only log does not grow one line per repeat touch. Semantically\n // observable touches (first sight, provenance/field change) bypass this buffer\n // and flush immediately (see `coalesceTouch`).\n private readonly pendingTouches = new Map<\n string,\n {\n namespace: string;\n kind: \"read\" | \"write\";\n metadata?: NamespaceTouchMetadata;\n at: Date;\n timer: NodeJS.Timeout;\n }\n >();\n\n constructor(private readonly config: PluginConfig) {\n this.memoryDir = config.memoryDir;\n this.stateDir = path.join(this.memoryDir, STATE_DIR);\n this.catalogPath = path.join(this.stateDir, CATALOG_FILE);\n this.rebuildLockPath = path.join(this.stateDir, REBUILD_LOCK_FILE);\n this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n this.compactBytesLimit = config.namespacesCatalogCompactBytes ?? 16 * 1024 * 1024;\n this.readCoalesceMs = config.namespacesCatalogReadTouchCoalesceMs ?? 60_000;\n this.writeCoalesceMs = config.namespacesCatalogWriteTouchCoalesceMs ?? 1_000;\n }\n\n /** Whether the catalog is active (namespaces enabled and catalog not opted out). */\n get enabled(): boolean {\n return isCatalogEnabled(this.config);\n }\n\n // ── Public enumeration API ──────────────────────────────────────────────\n\n /**\n * Sanitize a record at the enumeration boundary (round 5, cursor Medium + codex\n * P2; round 6 — NDXHe). Reads return whatever is in `namespaces.jsonl` after\n * schema checks only, so a tampered or pre-fix row could surface unsafe data to\n * maintenance/QMD until a rewrite occurs. Two distinct defenses:\n *\n * 1. UNSAFE NAMESPACE NAME (NGZqr, codex P2): an unsafe non-default namespace\n * (e.g. `../evil`, a name with separators, or >64 chars) is REJECTED outright\n * — return `null` so the caller drops it. The disk SCAN and the hot touch\n * path both reject such names with the SAME default-exempt `isSafeRouteNamespace`\n * gate, so the read boundary MUST agree, or `listNamespaces()`/`getNamespaceRecord()`\n * would expose a namespace those paths reject (note `isStorageDirForNamespace`\n * can still build a tokenized root even for `../evil`, so storageDir sanitation\n * alone does not catch it). The default namespace is exempt (it may be a\n * non-route literal), matching every other validation site.\n *\n * 2. UNSAFE storageDir: for an otherwise-valid namespace, apply the SAME contract\n * as the write path — full containment (`isContainedStorageDir`: lexical +\n * symlink/realpath) AND namespace ownership (`isStorageDirForNamespace`). When\n * a record fails EITHER check we substitute the trusted resolved-and-safe root\n * for that namespace (rule 42: read and write stay symmetric).\n */\n private async sanitizeRecordForRead(record: NamespaceRecord): Promise<NamespaceRecord | null> {\n // Defense 1: drop an unsafe non-default namespace name entirely. Compare\n // against the NORMALIZED default identity — record keys are trimmed, so a raw\n // whitespace-padded config default would never match the default row (NH-FH).\n if (record.namespace !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(record.namespace)) {\n return null;\n }\n // Defense 2: keep the record but substitute a safe storageDir when needed.\n if (\n (await this.isContainedStorageDir(record.storageDir)) &&\n (await this.isStorageDirForNamespace(record.namespace, record.storageDir))\n ) {\n return record;\n }\n const safe = await this.resolveSafeStorageDir(record.namespace);\n return { ...record, storageDir: safe };\n }\n\n private storageRootOwnershipRank(\n record: NamespaceRecord,\n resolvedStorageDir: string,\n configured: Set<string>,\n ): number {\n if (resolvedStorageDir === path.resolve(this.memoryDir)) {\n return record.namespace === this.defaultNamespaceIdentity ? 0 : 3;\n }\n\n const leaf = path.basename(resolvedStorageDir);\n const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;\n if (tokenOwnsRoot && configured.has(record.namespace)) {\n return 0;\n }\n if (record.namespace === leaf) return 1;\n if (tokenOwnsRoot) return 2;\n return 3;\n }\n\n private configuredNamespaceIdentities(): Set<string> {\n return new Set(\n [\n this.config.defaultNamespace,\n this.config.sharedNamespace,\n ...this.config.namespacePolicies.map((p) => p.name),\n ]\n .map((n) => normalizeNamespaceIdentity(n))\n .filter((n) => n.length > 0),\n );\n }\n\n private preferStorageRootOwner(\n current: NamespaceRecord,\n candidate: NamespaceRecord,\n resolvedStorageDir: string,\n configured: Set<string>,\n ): NamespaceRecord {\n const currentRank = this.storageRootOwnershipRank(current, resolvedStorageDir, configured);\n const candidateRank = this.storageRootOwnershipRank(candidate, resolvedStorageDir, configured);\n if (candidateRank < currentRank) return candidate;\n if (candidateRank > currentRank) return current;\n\n const byName = candidate.namespace.localeCompare(current.namespace);\n if (byName < 0) return candidate;\n if (byName > 0) return current;\n return candidate.identityToken.localeCompare(current.identityToken) < 0 ? candidate : current;\n }\n\n private dropDuplicateStorageRootAliases(records: NamespaceRecord[]): NamespaceRecord[] {\n const byStorageDir = new Map<string, NamespaceRecord>();\n const configured = this.configuredNamespaceIdentities();\n for (const record of records) {\n const resolvedStorageDir = path.resolve(record.storageDir);\n const current = byStorageDir.get(resolvedStorageDir);\n if (!current) {\n byStorageDir.set(resolvedStorageDir, record);\n continue;\n }\n const owner = this.preferStorageRootOwner(current, record, resolvedStorageDir, configured);\n const alias = owner === current ? record : current;\n byStorageDir.set(resolvedStorageDir, mergeNewerTouchFields(owner, alias));\n }\n return [...byStorageDir.values()];\n }\n private loadSanitizedRecords(): Promise<NamespaceRecord[]> {\n return this.queueCritical(async () => {\n const records = await this.loadCompacted();\n const sanitized = await Promise.all(\n [...records.values()].map((r) => this.sanitizeRecordForRead(r)),\n );\n // Drop unsafe-namespace rows (sanitizer returned null) at the read boundary.\n // Then collapse duplicate root aliases so maintenance/QMD see exactly one\n // namespace owner for a physical storage root, matching rebuild ownership,\n // while preserving touch recency from every alias row.\n return this.dropDuplicateStorageRootAliases(\n sanitized.filter((r): r is NamespaceRecord => r !== null),\n );\n });\n }\n\n async listNamespaces(filter?: NamespaceCatalogFilter): Promise<NamespaceRecord[]> {\n if (!this.enabled) return [];\n let out = await this.loadSanitizedRecords();\n if (filter?.kind) out = out.filter((r) => r.kind === filter.kind);\n if (filter?.discoveredBy) out = out.filter((r) => r.discoveredBy === filter.discoveredBy);\n if (filter?.writtenSince) {\n const sinceMs = filter.writtenSince.getTime();\n out = out.filter((r) => {\n if (!r.lastWriteAt) return false;\n const ms = Date.parse(r.lastWriteAt);\n return Number.isFinite(ms) && ms >= sinceMs;\n });\n }\n // Stable sort: namespace asc, identityToken as deterministic tiebreaker\n // (CLAUDE.md rule #19 — comparator returns 0 only for truly-equal items).\n return out.sort((a, b) => {\n const byName = a.namespace.localeCompare(b.namespace);\n if (byName !== 0) return byName;\n return a.identityToken.localeCompare(b.identityToken);\n });\n }\n\n async getNamespaceRecord(namespace: string): Promise<NamespaceRecord | null> {\n if (!this.enabled) return null;\n const ns = normalizeNamespaceIdentity(namespace);\n return (await this.loadSanitizedRecords()).find((record) => record.namespace === ns) ?? null;\n }\n\n // ── Touch API (cheap, failure-tolerant) ─────────────────────────────────\n\n async markRead(namespace: string, metadata?: NamespaceTouchMetadata): Promise<void> {\n await this.coalesceTouch(namespace, \"read\", metadata);\n }\n\n async markWrite(namespace: string, metadata?: NamespaceTouchMetadata): Promise<void> {\n await this.coalesceTouch(namespace, \"write\", metadata);\n }\n\n /**\n * Route a read/write touch through the per-(namespace, kind) coalescing buffer\n * (issue #1903). A touch flushes IMMEDIATELY (bypassing the buffer) whenever it\n * is semantically observable to an in-process reader — first sight of the\n * namespace, a `config`→`write` provenance upgrade, the first time a touch\n * field is set (so `listNamespaces({ writtenSince })` and record presence stay\n * correct), or a change to `kind`/`principal`/`projectId`/`branch`/\n * `parentNamespace`. A pure timestamp refresh on an already-known record is\n * DEFERRED and coalesced: the newest buffered `at`/metadata wins and a single\n * `touch` runs when the window elapses (or on `flushPendingTouches`). Window\n * `0` skips the buffer entirely (the pre-#1903 immediate-append behavior).\n * Best-effort: coalescing decisions read only the warm `compactedCache`, never\n * forcing a disk parse; a cold cache flushes immediately (and re-warms it).\n */\n private async coalesceTouch(\n namespace: string,\n kind: \"read\" | \"write\",\n metadata?: NamespaceTouchMetadata,\n ): Promise<void> {\n if (!this.enabled) return;\n const window = kind === \"read\" ? this.readCoalesceMs : this.writeCoalesceMs;\n if (window <= 0) {\n await this.touch(namespace, kind, metadata);\n return;\n }\n // Validate up front so an unsafe namespace rejects deterministically, exactly\n // as the immediate-append path does (touch validates too).\n const ns = this.validateNamespace(namespace);\n const cached = this.compactedCache?.records.get(ns);\n if (cached === undefined || this.touchMustFlushImmediately(kind, metadata, cached)) {\n // Semantically observable in-process → flush now (also warms the cache).\n // Any older buffered touch for this key is now redundant (this newer touch\n // updates the same field with a newer timestamp): drop it so its stale\n // timer cannot regress the timestamp after this flush.\n const stale = this.pendingTouches.get(`${kind}:${ns}`);\n if (stale) {\n clearTimeout(stale.timer);\n this.pendingTouches.delete(`${kind}:${ns}`);\n }\n await this.touch(namespace, kind, metadata);\n return;\n }\n // Pure timestamp refresh on a known record → coalesce within a FIXED window.\n const key = `${kind}:${ns}`;\n const at = metadata?.at ?? new Date();\n const existing = this.pendingTouches.get(key);\n if (existing) {\n // Keep the ORIGINAL timer and only refresh the buffered payload (#1903,\n // Codex). Re-arming on every touch would make this a debounce that never\n // flushes under continuous traffic (e.g. a sustained import), leaving\n // lastWriteAt stale and writtenSince/maintenance consumers blind to an\n // actively-written namespace. With a fixed window, each window flushes on\n // schedule carrying the newest buffered timestamp.\n existing.metadata = metadata;\n existing.at = at;\n return;\n }\n // Cap at Node's maximum setTimeout delay (2^31-1 ms ≈ 24.8 days). Values\n // above that are clamped by Node to 1ms, which would append almost every\n // touch and recreate the churn this coalescing exists to prevent (#1903, Codex).\n const safeWindow = Math.min(window, 2_147_483_647);\n const timer = setTimeout(() => {\n void this.flushPendingTouch(key);\n }, safeWindow);\n timer.unref();\n this.pendingTouches.set(key, { namespace: ns, kind, metadata, at, timer });\n }\n\n /**\n * Whether a read/write touch on an EXISTING cached record must flush\n * immediately rather than coalesce. Only pure timestamp refreshes (the value\n * returns false) are safe to defer.\n */\n private touchMustFlushImmediately(\n kind: \"read\" | \"write\",\n metadata: NamespaceTouchMetadata | undefined,\n cached: NamespaceRecord,\n ): boolean {\n // A write upgrading a config pre-registration to \"write\" changes the\n // discoveredBy filter result — must be observable at once (~:1261).\n if (kind === \"write\" && cached.discoveredBy === \"config\") return true;\n // The first time a touch field is set makes the namespace newly match\n // presence/`writtenSince` reads, so it must not be deferred.\n if (kind === \"write\" && cached.lastWriteAt === undefined) return true;\n if (kind === \"read\" && cached.lastReadAt === undefined) return true;\n if (!metadata) return false;\n if (metadata.kind !== undefined && metadata.kind !== cached.kind) return true;\n // A changed storageDir repoints routing/containment for this namespace —\n // must be observable at once, not held stale by the coalesce window (#1903, Cursor).\n if (metadata.storageDir !== undefined && metadata.storageDir !== cached.storageDir) return true;\n return false;\n }\n\n /** Fire a single buffered touch when its coalescing window elapses. */\n private async flushPendingTouch(key: string): Promise<void> {\n const pending = this.pendingTouches.get(key);\n if (!pending) return;\n this.pendingTouches.delete(key);\n clearTimeout(pending.timer);\n await this.touch(pending.namespace, pending.kind, { ...pending.metadata, at: pending.at }).catch(\n () => undefined,\n );\n }\n\n /**\n * Flush every buffered coalesced touch now (issue #1903). Called from the\n * router/orchestrator shutdown hook so a long-lived host does not drop\n * buffered read/write timestamps on teardown, and by tests to make coalesced\n * touches deterministically observable. Best-effort: a failed flush never\n * throws (each touch is `.catch()`-guarded).\n */\n async flushPendingTouches(): Promise<void> {\n const pending = [...this.pendingTouches.values()];\n this.pendingTouches.clear();\n for (const p of pending) {\n clearTimeout(p.timer);\n await this.touch(p.namespace, p.kind, { ...p.metadata, at: p.at }).catch(() => undefined);\n }\n }\n\n async markMaintenance(namespace: string, jobName: string, at?: Date): Promise<void> {\n if (typeof jobName !== \"string\" || jobName.trim().length === 0) {\n throw new Error(\"markMaintenance requires a non-empty jobName\");\n }\n await this.touch(namespace, \"maintenance\", { at }, jobName.trim());\n }\n\n /**\n * Register namespaces known purely from config (default, shared, explicit\n * policies). Source `config`. Cheap and idempotent.\n */\n async registerConfiguredNamespaces(): Promise<void> {\n if (!this.enabled) return;\n const names = new Set<string>([\n this.config.defaultNamespace,\n this.config.sharedNamespace,\n ...this.config.namespacePolicies.map((p) => p.name),\n ]);\n for (const ns of names) {\n if (!ns) continue;\n // Skip unsafe configured names (e.g. a `sharedNamespace`/policy name like\n // `../evil`) consistently with `rebuildFromDisk` (round 6, cursor Low —\n // NBn3w). `register`→`validateNamespace` THROWS on unsafe tokens; without\n // this guard one bad name would abort registration of all the rest. The\n // default namespace is exempt (it may be a non-route literal). Each call is\n // also wrapped so a single failure never blocks the remaining names.\n // `names` carries RAW config values, so normalize before the default-exempt\n // check — a whitespace-padded default must still be recognized (NH-FH).\n if (normalizeNamespaceIdentity(ns) !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {\n continue;\n }\n try {\n await this.register(ns, { discoveredBy: \"config\" });\n } catch {\n // Best-effort: a single bad/unsafe name must not abort the batch.\n }\n }\n }\n\n /**\n * Register a namespace whose storage was just resolved by the router. Used as\n * the router's integration hook (`discoveredBy: config`). Storage dir is\n * provided so we do not re-resolve it. Failure-tolerant. Returns whether the\n * registration actually APPENDED (round 6, codex P2 — NEFoX), so the router's\n * resolve-hook dedup only marks a namespace notified when it truly persisted —\n * a dropped append (disabled catalog or rebuild-lock-timeout drop) returns\n * `false` and is retried on the next resolve.\n */\n async registerResolved(namespace: string, storageDir: string): Promise<boolean> {\n if (!this.enabled) return false;\n return this.register(namespace, { discoveredBy: \"config\", storageDir });\n }\n\n /**\n * Generic register/touch without changing read/write timestamps unless the\n * source implies it. Validates the namespace and resolves a storage dir.\n * Returns whether the touch actually appended.\n */\n private async register(namespace: string, metadata: NamespaceTouchMetadata): Promise<boolean> {\n return this.touch(namespace, \"register\", metadata);\n }\n\n private validateNamespace(namespace: string): string {\n const ns = normalizeNamespaceIdentity(namespace);\n if (ns.length === 0) throw new Error(\"empty namespace\");\n // The configured default namespace is exempt from isSafeRouteNamespace at\n // the routing layer; honor the same exemption here, but everything still\n // resolves through the contained storage-dir helper below.\n if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {\n throw new Error(`unsafe namespace: ${ns}`);\n }\n return ns;\n }\n\n /**\n * Resolve the on-disk storage dir for a namespace WITHOUT trusting caller\n * input. The default namespace may use the legacy memoryDir root; everything\n * else lives under `<memoryDir>/namespaces/<token>`. Containment is enforced\n * by rejecting separators/parent-refs in the token.\n */\n private resolveStorageDir(namespace: string): string {\n if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {\n // Default may resolve to the legacy memoryDir root OR a tokenized dir; we\n // report memoryDir here as the canonical default root for the catalog.\n // rebuildFromDisk refines this when a tokenized default dir holds data.\n return this.memoryDir;\n }\n const token = namespaceIdentityToken(namespace);\n return this.namespaceTokenDir(token);\n }\n\n private namespaceTokenDir(token: string): string {\n if (\n token.length === 0 ||\n token.includes(\"/\") ||\n token.includes(\"\\\\\") ||\n token.includes(\"..\") ||\n path.isAbsolute(token)\n ) {\n throw new Error(`unsafe namespace token: ${token}`);\n }\n return path.join(this.memoryDir, \"namespaces\", token);\n }\n\n /**\n * Whether a candidate storage dir is LEXICALLY contained: it is either the\n * legacy default root (`memoryDir`) or a strict descendant of\n * `<memoryDir>/namespaces/`. The router legitimately resolves a namespace to\n * EITHER the tokenized dir or a legacy raw-name dir under `namespaces/`, so we\n * accept any contained child rather than a single exact token path. This is a\n * pure string check — symlink escape is checked separately via realpath.\n */\n private isLexicallyContained(candidate: string): boolean {\n const resolved = path.resolve(candidate);\n if (resolved === path.resolve(this.memoryDir)) return true;\n const nsBase = path.resolve(path.join(this.memoryDir, \"namespaces\"));\n const rel = path.relative(nsBase, resolved);\n // Must be a strict descendant of namespaces/ (non-empty, no parent escape).\n return rel.length > 0 && !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n }\n\n /**\n * Whether a candidate storage dir satisfies the catalog containment contract,\n * including SYMLINK-escape rejection (round 5, codex P2). A lexically-contained\n * path that is actually a symlink to an outside directory would let maintenance\n * or QMD follow it outside `memoryDir`. We mirror `rebuildFromDisk`'s posture:\n * the path must be lexically contained AND, if it exists on disk, neither the\n * path itself a symlink nor its realpath escaping the memory root. Non-existent\n * paths pass the realpath stage (nothing to follow yet) but still must be\n * lexically contained.\n */\n private async isContainedStorageDir(candidate: string): Promise<boolean> {\n if (!this.isLexicallyContained(candidate)) return false;\n // The default/legacy memoryDir root is trusted as-is.\n if (path.resolve(candidate) === path.resolve(this.memoryDir)) return true;\n let memoryReal: string;\n try {\n memoryReal = await realpath(this.memoryDir);\n } catch {\n memoryReal = path.resolve(this.memoryDir);\n }\n // Reject a candidate beneath any SYMLINKED ancestor (codex NVuq5): even when\n // the symlink currently resolves back inside memoryDir, the disk scanner\n // rejects such a root, and a later retarget of the link would let\n // maintenance/QMD follow the persisted path outside memoryDir. Mirror the\n // scanner so touch/config seeding cannot persist a root under a symlinked\n // namespace ancestor (the leaf itself is symlink-checked below).\n if (await this.hasSymlinkedAncestor(candidate)) return false;\n try {\n const stat = await lstat(candidate);\n if (stat.isSymbolicLink()) return false;\n // Reject an EXISTING non-directory root (NF21i, codex P2; CLAUDE.md rule\n // #24). A regular file (or socket/fifo) at `<memoryDir>/namespaces/<token>`\n // is lexically contained and its realpath stays inside memoryDir, so the\n // realpath check below would ACCEPT it — but a storage root must be a\n // directory. Recording a file as a namespace root yields a broken install\n // that only fails later when maintenance/QMD/mkdir treat it as a dir. The\n // disk scan already skips non-directory entries; mirror that here so every\n // containment consumer (resolve/touch/fallback/live-recheck) agrees.\n if (!stat.isDirectory()) return false;\n } catch {\n // The leaf does not exist yet. Lexical containment is NOT sufficient: an\n // EXISTING ancestor (e.g. `<memoryDir>/namespaces`) could be a symlink to\n // outside memoryDir, so a future mkdir/maintenance/QMD op would follow the\n // persisted root outside the root (round 6, codex P2 — NDo79). Verify the\n // nearest EXISTING ancestor's realpath still resolves inside memoryDir.\n return this.isNearestExistingAncestorContained(candidate, memoryReal);\n }\n try {\n const real = await realpath(candidate);\n return isPathInside(memoryReal, real);\n } catch {\n return false;\n }\n }\n\n /**\n * Reject a candidate whose path crosses a SYMLINKED ancestor strictly between\n * memoryDir and the leaf (codex NVuq5). `realpath`-based containment accepts a\n * symlinked `<memoryDir>/namespaces` that currently resolves back inside\n * memoryDir, but the disk scanner rejects such a root and a later retarget would\n * escape the memory tree — so refuse it here too. The leaf itself is\n * symlink-checked by the caller; this walks only the intermediate ancestors.\n */\n private async hasSymlinkedAncestor(candidate: string): Promise<boolean> {\n const stopAt = path.resolve(this.memoryDir);\n let dir = path.dirname(path.resolve(candidate));\n const root = path.parse(dir).root;\n while (dir !== stopAt && dir !== root && dir !== path.dirname(dir)) {\n try {\n if ((await lstat(dir)).isSymbolicLink()) return true;\n } catch {\n // Ancestor does not exist yet — it cannot be a symlink; keep walking up.\n }\n dir = path.dirname(dir);\n }\n return false;\n }\n\n /**\n * Walk up from a not-yet-existing candidate to the nearest ancestor that exists\n * on disk and verify its realpath stays inside `memoryReal` (round 6, codex P2\n * — NDo79). Rejects a non-existent leaf whose existing parent chain escapes\n * memoryDir via a symlink. Stops at memoryDir's resolved root.\n *\n * The nearest existing ancestor must also be a DIRECTORY (NHIdt, codex P2): if\n * an existing parent such as `<memoryDir>/namespaces` is a regular FILE (or\n * socket/fifo), `realpath(parent)` still succeeds and resolves inside memoryDir,\n * so a containment-only check would ACCEPT a leaf that can never be created — you\n * cannot mkdir a child under a file. We `lstat` the nearest existing ancestor and\n * reject when it is not a directory, mirroring the leaf non-directory rejection\n * (NF21i) and the disk scan, so every containment consumer agrees.\n */\n private async isNearestExistingAncestorContained(\n candidate: string,\n memoryReal: string,\n ): Promise<boolean> {\n let dir = path.resolve(candidate);\n const root = path.parse(dir).root;\n for (;;) {\n const parent = path.dirname(dir);\n // Reached the filesystem root without finding an existing ancestor.\n if (parent === dir || dir === root) return false;\n let real: string;\n try {\n real = await realpath(parent);\n } catch {\n // Parent does not exist yet either — keep walking up.\n dir = parent;\n continue;\n }\n // The nearest EXISTING ancestor must resolve inside the memory root...\n if (!(isPathInside(memoryReal, real) || real === memoryReal)) return false;\n // ...AND be a directory: a non-directory ancestor (e.g. a file occupying\n // `namespaces`) cannot hold the not-yet-created leaf (NHIdt).\n try {\n const stat = await lstat(real);\n return stat.isDirectory();\n } catch {\n // The ancestor vanished between realpath and lstat — treat as not usable.\n return false;\n }\n }\n }\n\n /**\n * Resolve the storage dir to persist for a touch, validating any caller-\n * provided `metadata.storageDir` against the catalog containment contract\n * (round 4 + round 5, codex P2). `markWrite`/`registerResolved` accept an\n * explicit storageDir, but persisting it verbatim would let a bad hook or\n * external consumer write an arbitrary path — including one outside `memoryDir`\n * or a symlink that escapes it — into the catalog, handing maintenance/QMD an\n * unsafe root. We accept an explicit (or previously-stored) dir ONLY when it\n * stays contained under memoryDir (lexically AND via realpath); otherwise we\n * drop it and fall back to the trusted resolved dir.\n */\n private async resolveTouchStorageDir(\n namespace: string,\n explicit: string | undefined,\n existingDir: string | undefined,\n ): Promise<string> {\n // An explicit storageDir is accepted ONLY when it is both contained AND\n // actually belongs to THIS namespace (round 6, codex P2 — NDATT). Containment\n // alone let a caller pass another namespace's tree (e.g.\n // `markWrite(\"project-a\", { storageDir: \".../namespaces/<project-b-token>\" })`)\n // or `memoryDir` for a non-default namespace; `listNamespaces()` would then\n // tell maintenance/QMD that `project-a` lives in another namespace's (or the\n // default) tree — a cross-namespace root confusion. We reject a mismatched\n // explicit root and fall back to the namespace's own resolved root.\n if (\n explicit !== undefined &&\n (await this.isContainedStorageDir(explicit)) &&\n (await this.isStorageDirForNamespace(namespace, explicit))\n ) {\n return explicit;\n }\n // Don't let a record poisoned by a pre-fix out-of-containment write keep an\n // unsafe dir alive across touches — only preserve a contained existing dir\n // that also belongs to this namespace.\n if (\n existingDir !== undefined &&\n (await this.isContainedStorageDir(existingDir)) &&\n (await this.isStorageDirForNamespace(namespace, existingDir))\n ) {\n return existingDir;\n }\n return this.resolveSafeStorageDir(namespace);\n }\n\n /**\n * Whether `candidate` is a legitimate storage root FOR `namespace` (round 6,\n * codex P2 — NDATT). Accepts the namespace's router-resolved root, its canonical\n * lexical tokenized dir, and (for the default namespace only) memoryDir. This\n * prevents a contained-but-CROSS-NAMESPACE path — another namespace's tree, or\n * memoryDir for a non-default namespace — from being persisted as this\n * namespace's root. Compared on resolved (absolute) paths.\n */\n private async isStorageDirForNamespace(namespace: string, candidate: string): Promise<boolean> {\n const resolvedCandidate = path.resolve(candidate);\n const valid = new Set<string>();\n // The namespace's canonical lexical TOKENIZED dir is always a valid root.\n try {\n valid.add(path.resolve(this.namespaceTokenDir(namespaceIdentityToken(namespace))));\n } catch {\n // Unsafe token cannot build a lexical dir; fall through to other roots.\n }\n // The namespace's legacy RAW-NAME dir (`namespaces/<rawname>`) is also a\n // valid root — the router serves data from it when present, even before any\n // dir exists on disk. Both forms belong to THIS namespace, never another's.\n try {\n valid.add(path.resolve(this.namespaceTokenDir(namespace)));\n } catch {\n // Unsafe raw name cannot build a lexical dir; rely on the other roots.\n }\n // The router-resolved root (whichever of the above it currently serves, a\n // migrated default, etc.).\n try {\n valid.add(path.resolve(await resolveNamespaceStorageRoot(this.config, namespace)));\n } catch {\n // Router resolution failed; rely on the lexical/default roots below.\n }\n // memoryDir is a valid root ONLY for the default namespace.\n if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {\n valid.add(path.resolve(this.memoryDir));\n try {\n valid.add(path.resolve(await resolveDefaultNamespaceRoot(this.config)));\n } catch {\n // ignore; memoryDir already covers the common default case.\n }\n }\n return valid.has(resolvedCandidate);\n }\n\n /**\n * Resolve the canonical storage dir for a namespace as the LIVE ROUTER would,\n * but NEVER return a path that escapes the memory root.\n *\n * Router alignment (round 4, cursor Medium): a read/register touch with no\n * explicit storageDir previously used the lexical `resolveStorageDir`, which\n * always picks `<memoryDir>/namespaces/<token>` (or `memoryDir` for the\n * default). That diverges from `NamespaceStorageRouter`, which can route to a\n * legacy raw-name dir or a migrated default root — so a recall touch could\n * record a contained-but-WRONG root that maintenance/rebuild then targets. We\n * now delegate to the shared `resolveNamespaceStorageRoot` (the very helper the\n * router uses) so the catalog records the same on-disk root the router serves.\n *\n * Containment (round 5, codex P2): the resolved path can still be a symlink\n * escaping memoryDir, so we run the full (lexical + realpath) containment\n * contract. When it FAILS we fall back to a NAMESPACE-SPECIFIC safe root, NOT\n * a blanket `memoryDir`. Recording `memoryDir` for a non-default namespace\n * would point enumeration/maintenance at the DEFAULT namespace's tree (round 5,\n * cursor/codex Medium/P2) — a cross-namespace fanout error. The correct safe\n * root is the namespace's own lexical tokenized dir\n * (`<memoryDir>/namespaces/<token>`), which is always contained and is that\n * namespace's canonical location (we record the lexical PATH as metadata; we do\n * not follow the escaping symlink). Only the default namespace — or a token so\n * unsafe even the lexical dir cannot be built — falls back to `memoryDir`.\n */\n private async resolveSafeStorageDir(namespace: string): Promise<string> {\n let resolved: string;\n try {\n resolved = await resolveNamespaceStorageRoot(this.config, namespace);\n } catch {\n return this.safeFallbackStorageDir(namespace);\n }\n if (await this.isContainedStorageDir(resolved)) return resolved;\n return this.safeFallbackStorageDir(namespace);\n }\n\n /**\n * The namespace-specific contained fallback root, used when the router-resolved\n * root fails containment (round 5, cursor/codex Medium/P2).\n *\n * Preference order:\n * 1. The namespace's OWN lexical tokenized dir (`namespaces/<token>`) — so a\n * non-default namespace is NOT pointed at the DEFAULT namespace's `memoryDir`\n * tree (which would misdirect maintenance fanout). Returned only when the\n * token dir itself stays CONTAINED (it is not a symlink, and its realpath\n * does not escape memoryDir — e.g. via a symlinked `namespaces/` parent).\n * 2. `memoryDir` as a LAST resort — for the default namespace, an unsafe token\n * that cannot build a contained path, OR the irreparable case where the\n * token dir's realpath escapes the root (so even its lexical path resolves\n * outside). NF21m note (codex P2): we deliberately do NOT record the lexical\n * token dir in that irreparable case — its realpath escapes memoryDir, and\n * the NDo79 contract REQUIRES that an escaping path is never persisted (a\n * later mkdir/maintenance/QMD op would follow it outside the root). Since no\n * contained namespace-specific path exists, containment wins: `memoryDir` is\n * the only safe root left. A namespace whose token dir's realpath escapes is\n * an irreparable on-disk state; recording the contained default root is\n * strictly safer than persisting an escaping one. The common case where the\n * token dir IS contained is handled by branch 1, so a healthy non-default\n * namespace never reaches `memoryDir`.\n */\n private async safeFallbackStorageDir(namespace: string): Promise<string> {\n if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) return this.memoryDir;\n let tokenDir: string;\n try {\n tokenDir = this.namespaceTokenDir(namespaceIdentityToken(namespace));\n } catch {\n return this.memoryDir;\n }\n if (await this.isContainedStorageDir(tokenDir)) return tokenDir;\n return this.memoryDir;\n }\n\n /**\n * Re-check, NOW, whether a namespace's storage root currently EXISTS on disk\n * with the SAME safety the directory scan uses (NFJV8, codex P2).\n *\n * The rebuild's final re-merge runs under the held lock and folds the freshly\n * re-read log (`latest`) into the scanned `rebuilt` set. A namespace present in\n * `latest` (a live touch row) but ABSENT from `rebuilt` is normally PURGED as\n * deleted (the NATqU \"disk scan is authoritative\" rule). But there is a TOCTOU\n * window: a dynamic namespace can be CREATED on disk AFTER `rebuildFromDisk()`\n * already enumerated `namespaces/` but BEFORE this re-merge. The scan snapshot\n * missed its new root, yet a gateway `markWrite` already appended a row for it.\n * Blindly purging that row would rewrite the catalog WITHOUT a live namespace\n * that now has data on disk, so `writtenSince`/maintenance/QMD consumers miss\n * it until another touch or rebuild.\n *\n * So before purging, we re-resolve the namespace's safe storage root (the same\n * router-aligned, containment-checked path the scan would have catalogued) and\n * confirm it is a real, contained, non-symlink directory that actually holds\n * memory data RIGHT NOW. If so the namespace was created-after-scan and is LIVE\n * — KEEP its row. This is the precise inverse of NATqU and does NOT reintroduce\n * it: a touch on a REMOVED root re-checks as ABSENT (no data on disk) and is\n * still purged; only a root that EXISTS on a fresh re-check is kept.\n *\n * Mirrors the per-entry scan checks (symlink rejection + realpath containment +\n * `hasMemoryData`) so a symlinked/escaping root is never resurrected.\n */\n private async liveStorageRootExistsForRebuild(\n namespace: string,\n memoryReal: string | null,\n ): Promise<boolean> {\n let root: string;\n try {\n // Use the SAME router-aligned, containment-enforcing resolver the catalog\n // uses everywhere else. It never returns an escaping path (falls back to a\n // namespace-specific contained root on containment failure).\n root = await this.resolveSafeStorageDir(namespace);\n } catch {\n return false;\n }\n // NH3Xy (codex P2): for a NON-default namespace, a generic fallback root is\n // NOT proof of liveness. When the namespace's own token root was skipped by\n // the scan as a symlink/escape, `resolveSafeStorageDir` can fall back to the\n // DEFAULT namespace's `memoryDir`; `hasMemoryData()` on that shared default\n // tree then returns true whenever the default namespace has any data, which\n // would wrongly KEEP a stale project row now pointing at the default tree\n // instead of purging the skipped namespace. Only the namespace's OWN root may\n // attest its liveness — so if a non-default namespace resolved to `memoryDir`,\n // it has no independent contained root and must be treated as absent (purge).\n if (\n normalizeNamespaceIdentity(namespace) !== this.defaultNamespaceIdentity &&\n path.resolve(root) === path.resolve(this.memoryDir)\n ) {\n return false;\n }\n let stat;\n try {\n stat = await lstat(root);\n } catch {\n // Root does not exist on disk → genuinely absent → allow the purge.\n return false;\n }\n // Reject a symlinked root rather than resurrecting it (scan parity).\n if (stat.isSymbolicLink()) return false;\n if (!stat.isDirectory()) return false;\n // Realpath must stay inside the memory root (scan parity).\n try {\n const real = await realpath(root);\n if (memoryReal && !isPathInside(memoryReal, real)) return false;\n } catch {\n return false;\n }\n // Only treat the root as a live namespace when it actually holds memory data,\n // exactly as the scan does (empty shells are not catalogued).\n return hasMemoryData(root);\n }\n\n /**\n * Record a namespace touch. Returns whether the touch actually APPENDED to the\n * log (round 6, codex P2 — NEFoX): a disabled catalog or a dropped append (the\n * NAUf7 rebuild-lock-timeout drop) returns `false`, so callers (e.g. the router\n * resolve-hook dedup) can avoid marking a dropped registration as completed and\n * suppressing its retry.\n */\n private async touch(\n namespace: string,\n kind: \"read\" | \"write\" | \"maintenance\" | \"register\",\n metadata?: NamespaceTouchMetadata,\n jobName?: string,\n ): Promise<boolean> {\n if (!this.enabled) return false;\n // Validate up front (outside the chain) so caller-facing rejections — e.g.\n // an unsafe namespace token — surface immediately and deterministically,\n // not interleaved with serialized I/O.\n const ns = this.validateNamespace(namespace);\n const nowIso = (metadata?.at ?? new Date()).toISOString();\n\n // Run the read → merge → append as a single serialized critical section so\n // two concurrent touches for the same namespace cannot both observe the same\n // stale record and then have the later append win compaction while dropping\n // the earlier touch's fields (CLAUDE.md rule #40 — the chain also recovers\n // from rejection). Reading inside the chain guarantees each touch sees the\n // most recent appended state, including any concurrent read/write/register.\n // Cross-process serialization (round 7, codex P2 — NEZkA: HELD MUTEX). A CLI\n // `rebuild --apply` holds the rebuild lock across its final `loadCompacted()`\n // → atomic `rename`. Previously a touch only POLLED (`waitForRebuildLockClear`)\n // for the lock before reading/appending WITHOUT holding it — a check-then-act\n // gap: a touch could see no lock, a rebuild could then acquire the lock + run\n // its final `loadCompacted()`, and the touch's later append would be clobbered\n // by the rebuild's `rename()`. We now make the touch HOLD the SAME advisory\n // lock for the WHOLE read → merge → append window. While the touch holds the\n // lock, a rebuild in another process blocks on it (and vice-versa), so no\n // append can land between a rebuild's final load and its rename. `queueCritical`\n // serializes this within ONE process (so the OS lock is never self-contended in\n // process); the file lock adds the missing CROSS-process exclusion. If the touch\n // cannot ACQUIRE the lock within the bounded wait (another process's rebuild is\n // mid-flight), it DROPS the append: the catalog is rebuildable best-effort\n // metadata, so skipping one touch is acceptable; it NEVER blocks forever, NEVER\n // appends without the lock, and NEVER crashes the primary memory op.\n return this.queueCritical(async () =>\n this.withHeldCatalogLock(async (acquired) => {\n // Could not hold the lock (a cross-process rebuild is in its load→rename\n // window). DROP rather than append into that window (the lost-append race\n // this lock exists to prevent). Returning false also lets the router's\n // resolve-hook dedup retry a dropped registration later.\n if (!acquired) return false;\n\n // Test-only seam: widen the held-lock window so a concurrency test can\n // attempt a cross-process rebuild here and assert it is BLOCKED by this\n // held lock (no-op in production).\n if (this.onTouchCriticalSectionForTest) {\n await this.onTouchCriticalSectionForTest();\n }\n\n const records = await this.loadCompacted();\n const existing = records.get(ns);\n\n // Containment-check any explicit storageDir before persisting it (round 4\n // + round 5, codex P2). Never trust a caller-provided path verbatim;\n // reject lexical escapes AND symlinks that escape via realpath.\n const storageDir = await this.resolveTouchStorageDir(\n ns,\n metadata?.storageDir,\n existing?.storageDir,\n );\n // Provenance (discoveredBy) and createdAt are CREATION-ONLY fields. Once a\n // record exists they are preserved, so a routine routing/recall touch (or\n // the router's `config` register hook firing on a cache hit) can never\n // clobber the original discovery source — e.g. a `write`-discovered record\n // is not reset to `config` by a later resolve. Touch fields (lastReadAt /\n // lastWriteAt / lastMaintenanceAt) still update on every touch below.\n const record: NamespaceRecord = existing\n ? { ...existing }\n : {\n namespace: ns,\n identityToken: namespaceIdentityToken(ns),\n kind: metadata?.kind ?? inferKind(ns, this.config),\n createdAt: nowIso,\n storageDir,\n discoveredBy:\n metadata?.discoveredBy ??\n (kind === \"register\" ? \"config\" : kind === \"maintenance\" ? \"scan\" : kind),\n };\n\n // Update mutable fields. storageDir and kind may change over a\n // namespace's lifetime, so they upsert.\n record.storageDir = storageDir;\n if (metadata?.kind) record.kind = metadata.kind;\n // PROVENANCE (creation-only, with one upgrade — round 6, codex P2 NBPmT):\n // `discoveredBy` is otherwise preserved for existing records (a routine\n // read/register/resolve never relabels it). The single exception is a real\n // WRITE upgrading a record that was only PRE-REGISTERED by the router's\n // `onResolve` hook (`discoveredBy: \"config\"`) before any data was written.\n // Without this upgrade, `listNamespaces({ discoveredBy: \"write\" })` misses\n // namespaces that were genuinely written, because `storageFor()` fires\n // `registerResolved()` (config) before `recordCatalogWrite()` runs. We\n // upgrade ONLY config→write — never downgrade write/read, never relabel a\n // read-discovered record — so the authoritative \"this namespace has been\n // written\" signal is recorded.\n if (kind === \"write\" && existing && record.discoveredBy === \"config\") {\n record.discoveredBy = \"write\";\n }\n\n if (kind === \"read\") record.lastReadAt = nowIso;\n if (kind === \"write\") record.lastWriteAt = nowIso;\n if (kind === \"maintenance\" && jobName) {\n record.lastMaintenanceAt = { ...(record.lastMaintenanceAt ?? {}), [jobName]: nowIso };\n }\n\n await this.appendUnchained(record);\n // Size-triggered auto-compaction (issue #1903), still inside the held\n // cross-process lock + queueCritical turn so the fold→rename is atomic\n // against concurrent touches and cross-process rebuilds.\n await this.maybeAutoCompact();\n return true;\n }),\n );\n }\n\n /**\n * Fold-and-rewrite the JSONL log when it exceeds `compactBytesLimit`\n * (issue #1903). MUST be called only from inside `touch`'s held-lock +\n * `queueCritical` critical section — it reuses `loadCompacted` (last-record-\n * wins fold; every namespace row survives) and `rewriteUnchained` (atomic\n * temp-file + rename), so the collapse is cross-process safe and never\n * invokes the disk-scan purge in `finishRebuild`. Best-effort and fail-open\n * (rule #40): the append already succeeded and is durable, so a failed\n * compaction must not fail the touch — the next over-limit touch retries.\n */\n private async maybeAutoCompact(): Promise<void> {\n try {\n const limit = this.compactBytesLimit; // 0 => auto-compaction disabled\n if (limit <= 0) return;\n // Exact post-append size when the cache is warm; skip this touch's check\n // when the cache is cold (a cross-process append invalidated it) — the\n // next touch re-warms and re-checks.\n const size = this.compactedCache?.identity.size;\n if (size === undefined || size <= limit) return;\n // Hysteresis (issue #1903, Codex): a folded catalog whose deduped state is\n // itself above the limit (many distinct namespaces) would otherwise be\n // re-folded + re-rewritten on EVERY touch — O(catalog) per touch. Only\n // compact again once the log has grown by >= one limit-worth since the last\n // compaction; by then the new appends are duplicates the fold can collapse.\n if (this.lastCompactedSize > 0 && size - this.lastCompactedSize < limit) return;\n const folded = await this.loadCompacted();\n await this.rewriteUnchained([...folded.values()]);\n this.lastCompactedSize = this.compactedCache?.identity.size ?? size;\n } catch {\n // Fail-open: compaction is best-effort maintenance on already-durable data.\n }\n }\n\n // ── Rebuild from disk ────────────────────────────────────────────────────\n\n async rebuildFromDisk(\n options?: { dryRun?: boolean },\n ): Promise<NamespaceCatalogRebuildResult> {\n const dryRun = options?.dryRun === true;\n if (!this.enabled) {\n return { dryRun, records: [], skipped: [], applied: false };\n }\n\n // CONCURRENCY (Issue A — round 2): the entire scan → merge → rewrite runs\n // inside ONE serialized critical section on the shared write chain. This\n // closes the round-1 residual risk where a hot-path markRead/markWrite/\n // registerResolved append could land AFTER the snapshot but BEFORE the\n // atomic rewrite and then be discarded by the rewrite. Because touches also\n // run through `queueCritical`, no append can interleave between the load\n // (which now reads the latest persisted state, including touches that\n // landed before this section started) and the rewrite. A `--dry-run` still\n // takes the section for a consistent read but performs no mutation.\n //\n // Deadlock note: the rewrite inside this section uses the unchained\n // `rewriteUnchained` helper (mirroring `appendUnchained`) rather than a\n // helper that re-enters `queueCritical` — re-entering the chain from inside\n // a held turn would await the very entry this section holds.\n //\n // CROSS-PROCESS (round 5, codex P2): `queueCritical` only serializes this\n // process's instance. A CLI `rebuild --apply` and the live gateway are\n // SEPARATE processes with independent write chains, so a gateway append can\n // still land between the CLI's load and its atomic rename. For the mutating\n // path we additionally take a cross-process file lock AND re-merge the latest\n // on-disk touches under that lock immediately before the rewrite (see\n // `rebuildInsideChain`). A dry-run never mutates, so it skips the lock.\n if (dryRun) {\n return this.queueCritical(async () => this.rebuildInsideChain(dryRun, false));\n }\n // A mutating rebuild HOLDS the same advisory lock that touches now hold (round\n // 7, codex P2 — NEZkA). Because the touch path acquires this lock across its\n // read→append window and the rebuild holds it across its final\n // `loadCompacted()` → `rename()`, the two are mutually exclusive cross-process:\n // no touch append can land between a rebuild's final load and its rename.\n //\n // SCOPED MUTEX (NFgCT, codex P2): the lock is acquired ONLY around the final\n // load→merge→rename window, NOT the (potentially long) disk scan. The scan does\n // not mutate, so holding the lock across it merely forces concurrent gateway\n // touches to wait — and they DROP their append after `REBUILD_LOCK_MAX_WAIT_MS`,\n // losing real `lastWriteAt`/new-namespace data the rewrite then misses. Keeping\n // the scan lockless shrinks the window in which a touch must contend with the\n // rebuild to just the final critical section, which is brief. `rebuildInsideChain`\n // acquires `withHeldCatalogLock` itself, immediately before its re-merge+rewrite.\n //\n // LOCK ORDERING (round 7 — NEZkA): the file lock is acquired INSIDE\n // `queueCritical`, identically to the touch path (`queueCritical` → file lock),\n // NOT around it. A consistent acquire order is what prevents an in-process\n // deadlock between a same-instance touch and rebuild: `queueCritical` fully\n // serializes the two turns in this process, so when one turn holds the file\n // lock the other is not even running — the OS lock is never self-contended\n // in-process and a same-instance touch never stalls/drops behind its own\n // rebuild. The file lock therefore adds ONLY the missing cross-process\n // exclusion. `rebuildInsideChain` still runs entirely inside `queueCritical`;\n // it just narrows the cross-process file lock to the final rewrite window.\n return this.queueCritical(async () => this.rebuildInsideChain(dryRun, true));\n }\n\n /**\n * Body of `rebuildFromDisk`, run inside a single `queueCritical` turn. MUST\n * only be invoked from within the serialized chain so the load and the\n * rewrite are atomic with respect to concurrent touches (in-process).\n *\n * `wantMutate` is true for an `--apply` (the caller intends to rewrite). The\n * cross-process file lock is acquired LATE — only around the final\n * load→merge→rename window (NFgCT, codex P2) — never across the disk scan, so a\n * long scan does not force concurrent gateway touches to wait (and drop their\n * append). Whether the rewrite actually happened is reported via the result's\n * `applied`: true only when `wantMutate` AND the lock was acquired.\n */\n private async rebuildInsideChain(\n dryRun: boolean,\n wantMutate: boolean,\n ): Promise<NamespaceCatalogRebuildResult> {\n // Read the LATEST persisted state inside the chain so any touch that landed\n // before this turn is folded in (and re-merged into the rewrite below).\n const existing = await this.loadCompacted();\n const skipped: NamespaceCatalogSkippedRoot[] = [];\n const rebuilt = new Map<string, NamespaceRecord>();\n const nowIso = new Date().toISOString();\n\n let memoryReal: string | null = null;\n try {\n memoryReal = await realpath(this.memoryDir);\n } catch {\n memoryReal = this.memoryDir;\n }\n\n // 1) Configured namespaces always belong in the catalog.\n //\n // NORMALIZE FIRST (NGnek, codex P2): the live router normalizes every namespace\n // via `normalizeNamespaceIdentity` (a trim) in `storageFor()` before resolving\n // storage, and `isSafeRouteNamespace` also trims before validating. So a\n // configured name with harmless surrounding whitespace (e.g.\n // `sharedNamespace: \"shared \"` or a policy name copied with a trailing space)\n // would otherwise seed a catalog row for the RAW string and resolve a\n // `namespaces/shared ` root the live reads/writes never use — pointing\n // maintenance/QMD at the wrong directory after `rebuild --apply`. We normalize\n // configured names here so the catalog seeds the SAME identity the router uses\n // (rule #42: read/write resolve through the same normalization). The default\n // namespace is normalized too and compared via its normalized form (`defaultNs`)\n // wherever a configured/scanned name is matched against it below.\n const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);\n const configured = new Set<string>(\n [\n this.config.defaultNamespace,\n this.config.sharedNamespace,\n ...this.config.namespacePolicies.map((p) => p.name),\n ]\n .map((n) => normalizeNamespaceIdentity(n))\n .filter((n) => n.length > 0),\n );\n\n // 2) Default-root alignment (Issue C — round 2): the catalog's default\n // record MUST point at the SAME root the runtime router resolves, or\n // maintenance/QMD consumers would read a different default root than\n // live reads. We delegate to the shared `resolveDefaultNamespaceRoot`\n // (the very helper the router uses) instead of reimplementing divergent\n // \"prefer tokenized dir if it has data\" logic — while legacy data lives\n // directly under memoryDir, this returns memoryDir, matching runtime.\n const resolvedDefaultRoot = await resolveDefaultNamespaceRoot(this.config);\n // CONTAINMENT (round 6, codex P2 — NEOFS): `resolveDefaultNamespaceRoot()` can\n // return a `namespaces/<default-token>` symlink escaping memoryDir when the\n // legacy default root is empty. The default record must never carry an\n // escaping `storageDir`; fall back to the trusted `memoryDir` root when the\n // resolved one fails containment. Computed ONCE so every later use (the\n // configured-seeding step and the scan's default-dir re-apply) stays safe.\n const defaultStorageDir = (await this.isContainedStorageDir(resolvedDefaultRoot))\n ? resolvedDefaultRoot\n : this.memoryDir;\n const legacyDefaultHasData = defaultStorageDir === this.memoryDir;\n\n for (const ns of configured) {\n if (!ns) continue;\n // SAFETY (round 6, codex P2 — NBPmO): `parseConfig` intentionally preserves\n // unsafe namespace strings (e.g. a `sharedNamespace`/`namespacePolicies[]`\n // name like `../evil`) so sinks reject them. The hot touch/scan paths\n // already reject via `isSafeRouteNamespace`; rebuild must NOT be the path\n // that admits an unsafe configured namespace into the catalog. The default\n // namespace is exempt (it may be a non-route literal), matching the scan\n // loop's exemption below.\n if (ns !== defaultNs && !isSafeRouteNamespace(ns)) {\n let token: string;\n try {\n token = namespaceIdentityToken(ns);\n } catch {\n token = ns;\n }\n skipped.push({ token, reason: \"unsafe\", detail: ns });\n continue;\n }\n // ROUTER ALIGNMENT (round 6, codex P2 — NDxiS): seed a configured\n // non-default namespace with the SAME root the runtime router resolves, not\n // a blanket tokenized dir. `resolveNamespaceStorageRoot` returns the legacy\n // RAW root when it exists and only prefers the tokenized root when that has\n // storage markers — so a configured namespace with an empty legacy raw root\n // (e.g. `namespaces/shared`) is catalogued at the runtime path, keeping\n // maintenance/QMD aligned with live reads. Falls back to the lexical token\n // dir if router resolution fails.\n let storageDir: string;\n if (ns === defaultNs) {\n storageDir = defaultStorageDir;\n } else {\n try {\n storageDir = await resolveNamespaceStorageRoot(this.config, ns);\n } catch {\n storageDir = this.namespaceTokenDir(namespaceIdentityToken(ns));\n }\n }\n // CONTAINMENT (round 6, codex P2 — NCzT4/NEOFS): verify the seeded path does\n // not ESCAPE memoryDir before recording it. The scan below rejects\n // escaping/symlinked roots, but this seeding runs FIRST, so without this\n // check rebuild would persist an escaping `storageDir`. `isContainedStorageDir`\n // enforces the full lexical + symlink + realpath contract and allows a\n // not-yet-created path (a brand-new configured namespace seeds its canonical\n // root). The DEFAULT namespace is also checked (NEOFS): if\n // `resolveDefaultNamespaceRoot()` returns a `namespaces/<default-token>`\n // symlink escaping memoryDir, we must NOT persist it. The default cannot be\n // \"skipped\" (it must always exist), so it falls back to the trusted\n // `memoryDir` root; a non-default namespace is skipped (escape).\n if (!(await this.isContainedStorageDir(storageDir))) {\n if (ns === defaultNs) {\n storageDir = this.memoryDir;\n } else {\n skipped.push({ token: namespaceIdentityToken(ns), reason: \"escape\", detail: storageDir });\n continue;\n }\n }\n rebuilt.set(\n ns,\n this.mergeForRebuild(existing.get(ns), {\n namespace: ns,\n identityToken: namespaceIdentityToken(ns),\n kind: inferKind(ns, this.config),\n createdAt: existing.get(ns)?.createdAt ?? nowIso,\n storageDir,\n discoveredBy: \"config\",\n }),\n );\n }\n\n // 3) Scan the namespaces/ directory for tokenized roots.\n const namespacesDir = path.join(this.memoryDir, \"namespaces\");\n let entries: Dirent[] = [];\n // CONTAINMENT (round 8, codex P2 — NE9K_): check the `namespaces` ROOT itself\n // BEFORE `readdir` follows it. If `<memoryDir>/namespaces` is a symlink (or its\n // realpath escapes memoryDir), `readdir()` would enumerate an arbitrary outside\n // tree — leaking names or spending time on a huge directory — even though the\n // catalog rejects symlinked/escaping per-entry roots. The per-entry lstat/realpath\n // checks below run AFTER the readdir, so they cannot prevent following an\n // escaping ROOT. We lstat the root: if it is a symlink, OR its realpath escapes\n // memoryDir, we DO NOT read it and report it as a single unsafe scan root.\n let namespacesDirSafe = true;\n try {\n const rootStat = await lstat(namespacesDir);\n if (rootStat.isSymbolicLink()) {\n namespacesDirSafe = false;\n } else {\n const realNamespacesDir = await realpath(namespacesDir);\n if (memoryReal && !isPathInside(memoryReal, realNamespacesDir)) {\n namespacesDirSafe = false;\n }\n }\n } catch {\n // The `namespaces` dir does not exist yet (or lstat failed): nothing to scan,\n // and there is no symlink to follow. Treat as an empty, safe scan.\n namespacesDirSafe = true;\n }\n if (!namespacesDirSafe) {\n skipped.push({ token: \"namespaces\", reason: \"symlink\", detail: namespacesDir });\n } else {\n try {\n entries = await readdir(namespacesDir, { withFileTypes: true });\n } catch {\n entries = [];\n }\n }\n\n // Dual-root alignment (round 5, cursor Medium): when both a legacy raw-name\n // dir and a tokenized dir hold data for the SAME namespace, the router\n // prefers the tokenized root. Track which scanned namespaces were already\n // sourced from their tokenized dir so a later legacy-named `readdir` entry\n // cannot overwrite the tokenized record (and vice-versa: a tokenized entry\n // always wins over a previously-set legacy one).\n const scannedFromTokenized = new Map<string, number>();\n for (const entry of entries) {\n const token = entry.name;\n const fullPath = path.join(namespacesDir, token);\n // Reject symlinks / escaping roots rather than trusting them.\n let stat;\n try {\n stat = await lstat(fullPath);\n } catch (err) {\n skipped.push({ token, reason: \"error\", detail: err instanceof Error ? err.message : String(err) });\n continue;\n }\n if (stat.isSymbolicLink()) {\n skipped.push({ token, reason: \"symlink\", detail: fullPath });\n continue;\n }\n if (!stat.isDirectory()) continue;\n // Containment: realpath must stay inside the memory root.\n try {\n const real = await realpath(fullPath);\n if (memoryReal && !isPathInside(memoryReal, real)) {\n skipped.push({ token, reason: \"escape\", detail: real });\n continue;\n }\n } catch (err) {\n skipped.push({ token, reason: \"error\", detail: err instanceof Error ? err.message : String(err) });\n continue;\n }\n\n // Decode the namespace from the dir name. A configured dir name is used\n // verbatim. Otherwise decode a genuine tokenized dir back to its identity,\n // falling back to the raw dir name when it is not a decodable token.\n //\n // NDATN note (round 6, codex P2): a raw dir literally named like a CANONICAL\n // token (e.g. `namespaces/ns-616c706861`, the canonical token of `alpha`) is\n // inherently ambiguous from disk alone — the bytes are identical whether the\n // namespace is `alpha` (in its tokenized dir) or the literal `ns-616c706861`\n // (in a raw dir). Decoding a canonical token is the correct default. The\n // unambiguous fix lives on the WRITE path, where the caller knows the true\n // namespace and records it verbatim (NCQI0); the scanner cannot recover a\n // name the encoding cannot distinguish, so we keep the canonical decode.\n //\n // NRcCD (round 9, codex P2 — same class as namespaceFromStorageDir/NRCve):\n // the canonical decode is WRONG when a namespace LITERALLY named like the\n // token already OWNS this root. A dynamic namespace served from a legacy raw\n // root `namespaces/ns-616c706861` (named verbatim `ns-616c706861`) records a\n // catalog row from the write path; that row is in `existing` (the prior\n // load) here. If we still decoded to `alpha`, this scan would emit an `alpha`\n // row at `fullPath`, and the final live-row remerge in `finishRebuild` would\n // re-add the literal `ns-616c706861` row (its root still has data) — leaving\n // TWO catalog rows at the SAME `storageDir`, fanning QMD/maintenance out under\n // the wrong namespace. So, mirroring `namespaceFromStorageDir`'s \"config/catalog\n // match before decode\" rule, prefer the LITERAL dir name when it is already a\n // KNOWN namespace — configured OR present as a live/cataloged row in `existing`\n // — and DO NOT also emit the decoded alias for that same root. A genuine\n // tokenized dir with no literal owner (no `existing` row keyed by the raw\n // token) still decodes as before.\n // Root ownership (codex r3499938974): preserving the literal must be\n // ROOT-based, not just key-based. A STALE cataloged row merely NAMED like\n // the token (but whose storageDir is NOT this `fullPath`) must NOT win — a\n // real dynamic `alpha` write served from this tokenized root would then be\n // rebuilt under the stale literal name and the fresh `alpha` row dropped by\n // the owned-by-other guard. So only prefer the literal when a CONFIGURED\n // name matches OR an existing cataloged row named `token` actually OWNS this\n // `fullPath`. A genuine tokenized root with no literal owner decodes.\n const literalRecord = existing.get(token);\n const literalOwnsRoot =\n configured.has(token) ||\n (literalRecord !== undefined &&\n path.resolve(literalRecord.storageDir) === path.resolve(fullPath));\n // Match `storageFor()`'s canonical namespace identity. A raw root whose\n // spelling trims to another namespace (for example `namespaces/shared `)\n // is not a routeable live root and must not be catalogued from disk.\n const tokenDecoded = literalOwnsRoot ? null : namespaceIdentityFromToken(token);\n const rawDecoded = tokenDecoded && tokenDecoded.length > 0 ? tokenDecoded : token;\n const decoded = normalizeNamespaceIdentity(rawDecoded);\n if (decoded.length === 0 || (rawDecoded !== decoded && rawDecoded.normalize(\"NFC\") !== decoded)) {\n skipped.push({ token, reason: \"unsafe\", detail: rawDecoded });\n continue;\n }\n if (decoded !== defaultNs && !isSafeRouteNamespace(decoded)) {\n skipped.push({ token, reason: \"unsafe\", detail: decoded });\n continue;\n }\n // Only catalog roots that actually hold memory data (skip empty shells).\n // A malformed PRESENT marker is different from an absent marker: if\n // `facts/` is a file/symlink but `state/` is valid, cataloging the root\n // would later make catalog-driven QMD scan the bad category directory and\n // throw. Reject the whole root on the first malformed known marker.\n const memoryData = await inspectMemoryDataRoot(fullPath);\n if (memoryData.invalidMarker) {\n skipped.push({\n token,\n reason: \"unsafe\",\n detail: `invalid memory marker: ${memoryData.invalidMarker}`,\n });\n continue;\n }\n if (!memoryData.hasData) continue;\n\n // Default-root alignment (Issue C): never let a tokenized default dir\n // overwrite the configured default's storageDir with `fullPath`. The\n // default record's root is owned by `resolveDefaultNamespaceRoot` above,\n // which mirrors the router. We still keep the default record (set in\n // step 1) but skip clobbering its root here.\n if (decoded === defaultNs) {\n const def = rebuilt.get(defaultNs);\n if (def) {\n def.storageDir = defaultStorageDir;\n def.kind = \"default\";\n }\n continue;\n }\n\n // Dual-root preference: mirror the router, which uses the tokenized root\n // over a legacy raw-name root when the tokenized one has data. `entry.name`\n // is the on-disk dir name; it is the tokenized dir iff it equals the\n // namespace's identity token. If we already recorded this namespace from\n // its tokenized dir, a later legacy-named entry must not clobber it.\n const canonicalToken = namespaceIdentityToken(decoded);\n const legacyToken = namespaceIdentityLegacyToken(decoded.normalize(\"NFD\"));\n const tokenKind = token === canonicalToken ? 2 : token === legacyToken ? 1 : 0;\n const priorTokenKind = scannedFromTokenized.get(decoded) ?? 0;\n if (rebuilt.has(decoded) && priorTokenKind > tokenKind) continue;\n if (tokenKind > 0) scannedFromTokenized.set(decoded, tokenKind);\n\n const prior = existing.get(decoded);\n rebuilt.set(\n decoded,\n this.mergeForRebuild(prior, {\n namespace: decoded,\n identityToken: namespaceIdentityToken(decoded),\n kind: inferKind(decoded, this.config),\n createdAt: prior?.createdAt ?? nowIso,\n storageDir: fullPath,\n // Configured-and-present namespaces keep config provenance; purely\n // discovered ones are scan.\n discoveredBy: configured.has(decoded) ? \"config\" : prior?.discoveredBy ?? \"scan\",\n }),\n );\n }\n\n // Mark legacy default root explicitly when applicable.\n if (legacyDefaultHasData && defaultStorageDir === this.memoryDir) {\n const def = rebuilt.get(defaultNs);\n if (def) def.kind = \"default\";\n }\n\n // ── Final critical section (SCOPED MUTEX — NFgCT, codex P2) ──────────────\n // The disk scan above ran LOCKLESS (it only reads). Now, for a mutating\n // rebuild, acquire the cross-process file lock ONLY for the\n // load→merge→rename window — the brief section a concurrent touch must be\n // excluded from. `canMutate` is true iff we ACTUALLY hold the lock: if\n // acquisition timed out (`acquired === false`) we run compute-only and never\n // re-merge+rewrite unlocked (which would race a concurrent lock holder and\n // recreate the lost-append window). A dry-run skips the lock entirely.\n if (!wantMutate) {\n return this.finishRebuild(rebuilt, skipped, dryRun, false, memoryReal, nowIso);\n }\n // Test-only seam: the SCAN is now complete but the cross-process lock has NOT\n // yet been acquired (NFgCT). A concurrency test attempts a cross-instance touch\n // here and asserts it is NOT blocked/dropped — proving the scan is lockless.\n if (this.onRebuildAfterScanForTest) {\n await this.onRebuildAfterScanForTest();\n }\n return this.withHeldCatalogLock((acquired) =>\n this.finishRebuild(rebuilt, skipped, dryRun, acquired, memoryReal, nowIso),\n );\n }\n\n /**\n * Final load→merge→rename window of a rebuild, factored out so the caller can\n * run it WITHIN the cross-process file lock (NFgCT, codex P2) without holding\n * that lock across the preceding disk scan. Re-reads the latest on-disk state,\n * folds concurrent touches, then (when `canMutate`) atomically rewrites the log.\n *\n * `canMutate` records that the cross-process lock was actually held. The\n * re-merge + rewrite run only when it is true — a dry-run, or an unlocked apply\n * (lock-acquisition timeout), computes records but does NOT rename, so it can\n * never clobber a concurrent lock holder's window. `applied` mirrors `canMutate`.\n */\n private async finishRebuild(\n rebuilt: Map<string, NamespaceRecord>,\n skipped: NamespaceCatalogSkippedRoot[],\n dryRun: boolean,\n canMutate: boolean,\n memoryReal: string | null,\n nowIso: string,\n ): Promise<NamespaceCatalogRebuildResult> {\n if (canMutate) {\n // CROSS-PROCESS re-merge (round 5, codex P2): under the rebuild lock,\n // re-read the on-disk log ONE more time and fold any touch fields that\n // landed AFTER our initial `loadCompacted()` (e.g. a gateway markWrite in\n // another process) into the rebuilt records — last-write-wins per touch\n // field. This recovers cross-process appends that completed during the\n // scan, which the in-process `queueCritical` alone cannot see. Only runs\n // when we hold the lock (round 6, codex P2 — NBPmY): an unlocked rebuild\n // must not re-merge then rename, or it races a concurrent lock holder.\n const latest = await this.loadCompacted(true);\n for (const [ns, fresh] of latest) {\n const current = rebuilt.get(ns);\n if (!current) {\n // AUTHORITATIVE PURGE (round 6, cursor Medium — NATqU): the disk scan\n // is the single source of truth for which namespaces EXIST. A namespace\n // absent from `rebuilt` was NOT discovered on disk (its root is\n // empty/deleted) and is NOT configured, so the rebuild is purging it.\n // We must NOT resurrect it from the log — not even when a CONCURRENT\n // best-effort `markRead`/`markWrite` touched it after our snapshot. A\n // touch on a dynamic namespace whose on-disk root was removed only\n // bumps a timestamp; re-inserting that row (with its stale `storageDir`)\n // would defeat the purge the rebuild is meant to perform.\n //\n // CREATED-AFTER-SCAN RE-CHECK (NFJV8, codex P2): there is a TOCTOU\n // window where a dynamic namespace is CREATED on disk AFTER the scan\n // enumerated `namespaces/` but BEFORE this re-merge. Its new root was\n // missed by the snapshot, yet a gateway `markWrite` already landed a row\n // in `latest`. Purging that row would drop a LIVE namespace that now has\n // data on disk. So before purging, re-check the namespace's storage root\n // RIGHT NOW (with the same symlink/realpath/containment + memory-data\n // safety the scan uses). If it currently EXISTS with data, the namespace\n // was created-after-scan and is live — KEEP its row. This is the precise\n // inverse of NATqU, not a regression of it: a touch on a REMOVED root\n // re-checks as absent and is still purged below; only a root that EXISTS\n // on a fresh re-check is kept.\n //\n // SAFETY REVALIDATION (NGLz5, codex P2): the `ns` key comes from the\n // UNTRUSTED log (`latest`), which may carry an unsafe namespace row from a\n // pre-fix or tampered catalog. The disk SCAN validates every decoded\n // namespace with `isSafeRouteNamespace` (default exempt) and SKIPS unsafe\n // ones — so an unsafe namespace is absent from `rebuilt` by design, NOT\n // because it was deleted. Without re-applying that exact check here, a\n // matching tokenized dir on disk would let this branch RESURRECT the\n // unsafe row, and `--apply` would rewrite the catalog with a namespace the\n // hot touch/config/scan paths all reject — leaving maintenance/QMD able to\n // enumerate an unsafe namespace after a rebuild that appeared to skip it.\n // Apply the SAME default-exempt safety gate before the live-root recheck;\n // an unsafe row is dropped (fall through to purge), never kept.\n if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {\n continue;\n }\n if (await this.liveStorageRootExistsForRebuild(ns, memoryReal)) {\n // Created-after-scan: keep the live row. Re-resolve its storageDir to\n // the safe (router-aligned, contained) root so we never persist a\n // touch's stale/escaping `storageDir`.\n const safeDir = await this.resolveSafeStorageDir(ns);\n // DUAL-ROOT GUARD (codex NR-td): if another rebuilt row already OWNS\n // this exact storageDir (e.g. the decoded/configured owner of a\n // token-shaped root that the disk scan resolved), do NOT also resurrect\n // this stale alias from the untrusted log — that leaves TWO catalog rows\n // pointing at one root and fans maintenance/QMD out over the wrong\n // namespace. Enforce at most one row per storageDir: the scan's owner\n // wins, the alias is dropped (falls through to purge) after folding\n // its touch fields into the owner so recency filters/maintenance do\n // not miss a real write.\n const resolvedSafe = path.resolve(safeDir);\n let owningNamespace: string | null = null;\n for (const [otherNs, otherRec] of rebuilt) {\n if (otherNs !== ns && path.resolve(otherRec.storageDir) === resolvedSafe) {\n owningNamespace = otherNs;\n break;\n }\n }\n if (owningNamespace) {\n const owner = rebuilt.get(owningNamespace);\n if (owner) rebuilt.set(owningNamespace, mergeNewerTouchFields(owner, fresh));\n continue;\n }\n rebuilt.set(ns, {\n ...fresh,\n storageDir: safeDir,\n identityToken: namespaceIdentityToken(ns),\n kind: fresh.kind ?? inferKind(ns, this.config),\n createdAt: fresh.createdAt ?? nowIso,\n });\n continue;\n }\n // Confirmed absent on disk. Losing a touch timestamp for a deleted root\n // is acceptable (the catalog is rebuildable best-effort metadata);\n // resurrecting a purged record is not. Drop it.\n continue;\n }\n // SURVIVING namespace (still present in the authoritative disk scan):\n // fold in any newer touch fields that landed cross-process after our\n // initial snapshot so a concurrent gateway markWrite is not lost.\n rebuilt.set(ns, mergeNewerTouchFields(current, fresh));\n }\n }\n\n const records = [...rebuilt.values()].sort((a, b) => {\n const byName = a.namespace.localeCompare(b.namespace);\n if (byName !== 0) return byName;\n return a.identityToken.localeCompare(b.identityToken);\n });\n\n // Only rewrite when we actually hold the cross-process lock (round 6, codex\n // P2 — NBPmY). A dry-run never mutates; an unlocked rebuild (acquisition\n // timed out) returns the computed records WITHOUT renaming over the log, so\n // it can never clobber a concurrent lock holder's window.\n if (canMutate) {\n // Test-only seam: the load→rename window where the old check-then-append\n // touch could be clobbered. A concurrency test attempts a cross-instance\n // touch here and asserts the held lock blocks it (no-op in production).\n if (this.onRebuildBeforeRenameForTest) {\n await this.onRebuildBeforeRenameForTest();\n }\n await this.rewriteUnchained(records);\n }\n\n // `applied` is true only when we actually rewrote the log: never for a\n // dry-run, and never for an `--apply` that ran compute-only because it could\n // not acquire the lock (canMutate=false). Surfaces the real mutation state so\n // the CLI does not report success on a skipped rewrite (NBn3n/NBsGG).\n return { dryRun, records, skipped, applied: canMutate };\n }\n\n // ── Cross-process catalog write lock (held mutex) ────────────────────────\n\n /**\n * Run `fn` while HOLDING the shared cross-process advisory lock (round 5, codex\n * P2; generalized round 7 — NEZkA). This is the SINGLE mutex shared by BOTH the\n * touch read→merge→append window AND the rebuild final load→merge→rename window,\n * so a touch and a rebuild in different processes are mutually exclusive over\n * their respective critical sections — closing the check-then-append gap where a\n * polled-only touch could append into a rebuild's load→rename window.\n *\n * Issue #1524 adoption: this is now a thin delegation to the shared\n * `withHeldFileLock` utility. The acquire loop, mtime heartbeat, stale-break\n * (NG7Bg replacement-safe), and ownership-checked release (NCzT6) all live in\n * ONE place — the util — so this module no longer re-implements them. The\n * catalog's `REBUILD_LOCK_*` constants and the `onBeforeBreakStaleUnlinkForTest`\n * seam flow straight through. The util generates a per-CALL owner uuid, which\n * is stricter than the previous per-instance `lockOwnerId` (two calls on the\n * same instance get different ids, so neither mistakes the other's lock as\n * self-held — the NBsGP invariant, preserved and tightened).\n *\n * IN-PROCESS SAFETY: every caller invokes this from inside (or wrapping) the\n * per-process `queueCritical` chain, which serializes all catalog mutations in\n * THIS process (now via `MutationSerializer`). So within one process only one\n * logical holder attempts OS-lock acquisition at a time — the file lock is never\n * self-contended in-process, and the lock is acquired and released within a\n * single in-process turn. The file lock adds only the missing CROSS-process\n * exclusion.\n *\n * ACQUISITION RESULT (round 6, codex P2 — NBPmY): `fn` receives whether WE\n * actually hold the lock. When acquisition TIMED OUT (another holder is active),\n * a MUTATING rebuild must NOT perform its load/rename window unlocked, and a\n * touch must NOT append unlocked — both would recreate the lost-append race. The\n * caller uses `acquired` to run compute-only (rebuild) or DROP the append\n * (touch) when unlocked.\n */\n private withHeldCatalogLock<T>(fn: (acquired: boolean) => Promise<T>): Promise<T> {\n return withHeldFileLock(\n this.rebuildLockPath,\n {\n staleMs: REBUILD_LOCK_STALE_MS,\n maxWaitMs: REBUILD_LOCK_MAX_WAIT_MS,\n pollMs: REBUILD_LOCK_POLL_MS,\n heartbeatMs: REBUILD_LOCK_HEARTBEAT_MS,\n // NG7Bg seam: fires inside the util's breakStaleLock after it judges the\n // lock stale and captures its identity, before the atomic rename+verify.\n onBeforeBreakStaleUnlinkForTest: this.onBeforeBreakStaleUnlinkForTest,\n },\n fn,\n );\n }\n\n /**\n * Merge a prior record's preserved metadata (timestamps, principal hints)\n * onto a freshly-discovered record. Disk-derived fields (storageDir, kind)\n * take precedence from the new record.\n *\n * PROVENANCE (round 3, cursor Low): `discoveredBy` and `createdAt` are\n * CREATION-ONLY — identical to the touch path's invariant. A rebuild must NOT\n * reset a namespace first seen via a `write`/`read` touch back to `config`\n * just because it is also listed in policies. So when a prior record exists we\n * carry its `discoveredBy` forward; only brand-new records keep the fresh\n * (config/scan) provenance.\n */\n private mergeForRebuild(prior: NamespaceRecord | undefined, fresh: NamespaceRecord): NamespaceRecord {\n if (!prior) return fresh;\n const merged: NamespaceRecord = {\n ...fresh,\n createdAt: prior.createdAt ?? fresh.createdAt,\n discoveredBy: prior.discoveredBy ?? fresh.discoveredBy,\n };\n if (prior.lastReadAt) merged.lastReadAt = prior.lastReadAt;\n if (prior.lastWriteAt) merged.lastWriteAt = prior.lastWriteAt;\n if (prior.lastMaintenanceAt) merged.lastMaintenanceAt = { ...prior.lastMaintenanceAt };\n return merged;\n }\n // ── Persistence ──────────────────────────────────────────────────────────\n\n /** Load the JSONL log and fold it into current state (last-record-wins). */\n private async loadCompacted(forceFresh = false): Promise<Map<string, NamespaceRecord>> {\n const records = new Map<string, NamespaceRecord>();\n let handle;\n try {\n handle = await open(this.catalogPath, \"r\");\n } catch {\n this.compactedCache = undefined;\n return records;\n }\n\n let identity: CatalogFileIdentity | undefined;\n try {\n const fileStat = await handle.stat();\n identity = {\n size: fileStat.size,\n mtimeMs: fileStat.mtimeMs,\n ctimeMs: fileStat.ctimeMs,\n ino: fileStat.ino,\n };\n if (\n !forceFresh &&\n this.compactedCache &&\n this.compactedCache.identity.size === identity.size &&\n this.compactedCache.identity.mtimeMs === identity.mtimeMs &&\n this.compactedCache.identity.ctimeMs === identity.ctimeMs &&\n this.compactedCache.identity.ino === identity.ino\n ) {\n await handle.close().catch(() => undefined);\n return this.cloneCompactedRecords(this.compactedCache.records);\n }\n } catch {\n // A failed fstat cannot prove freshness. Read and parse the open file, but\n // do not cache the result without a trustworthy identity.\n }\n\n try {\n this.onCatalogReadForTest?.();\n // Streaming chunked parse (issue #1903): the log can reach 100+ MB in\n // production, so read it in bounded chunks and yield to the event loop\n // every ~5000 lines instead of buffering the whole file and blocking on a\n // single giant `split(\"\\n\")`. The fold is unchanged (last-record-wins with\n // field-level touch merge across cross-process full-snapshot appends).\n const CHUNK_BYTES = 1 << 16; // 64 KiB\n const YIELD_EVERY_LINES = 5000;\n const chunk = Buffer.allocUnsafe(CHUNK_BYTES);\n // StringDecoder buffers partial multi-byte UTF-8 sequences that straddle a\n // chunk boundary, so a namespace/principal with non-ASCII bytes never\n // corrupts at the seam.\n const decoder = new StringDecoder(\"utf8\");\n let leftover = \"\";\n let linesSinceYield = 0;\n const foldLine = (line: string): void => {\n const trimmed = line.trim();\n if (trimmed.length === 0) return;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n // Skip corrupt lines (CLAUDE.md rule #18 robustness).\n return;\n }\n const record = coerceRecord(parsed);\n if (!record) return;\n const prior = records.get(record.namespace);\n records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);\n };\n for (;;) {\n const { bytesRead } = await handle.read(chunk, 0, CHUNK_BYTES, null);\n if (bytesRead === 0) break;\n leftover += decoder.write(chunk.subarray(0, bytesRead));\n let newlineIndex = leftover.indexOf(\"\\n\");\n while (newlineIndex !== -1) {\n foldLine(leftover.slice(0, newlineIndex));\n leftover = leftover.slice(newlineIndex + 1);\n if (++linesSinceYield >= YIELD_EVERY_LINES) {\n linesSinceYield = 0;\n await new Promise<void>((resolve) => setImmediate(resolve));\n }\n newlineIndex = leftover.indexOf(\"\\n\");\n }\n }\n leftover += decoder.end();\n foldLine(leftover);\n } catch {\n this.compactedCache = undefined;\n return new Map();\n } finally {\n await handle.close().catch(() => undefined);\n }\n\n this.compactedCache = identity ? { identity, records } : undefined;\n return this.cloneCompactedRecords(records);\n }\n\n private cloneCompactedRecords(\n records: Map<string, NamespaceRecord>,\n ): Map<string, NamespaceRecord> {\n return new Map(\n [...records].map(([namespace, record]) => [\n namespace,\n {\n ...record,\n lastMaintenanceAt: record.lastMaintenanceAt\n ? { ...record.lastMaintenanceAt }\n : undefined,\n },\n ]),\n );\n }\n\n /**\n * Serialize an arbitrary read-modify-write critical section through the single\n * per-instance chain. Every catalog mutation and cached catalog read runs\n * through this queue, so cache validation and updates cannot race in-process.\n *\n * Issue #1524 adoption: delegates to the shared `MutationSerializer` (stored\n * as `criticalSection`). The util owns the rejection-recovery invariant\n * (CLAUDE.md rule #40 — one failed section never poisons subsequent ones, but\n * the failing section's error still surfaces to ITS awaited promise) and the\n * no-unbounded-growth cleanup. The key is constant: the catalog has ONE\n * logical mutation queue (touches and rebuilds mutually exclude in-process).\n */\n private queueCritical<T>(fn: () => Promise<T>): Promise<T> {\n return this.criticalSection.serialize(\"catalog\", fn);\n }\n\n /**\n * Append a single record to the JSONL log WITHOUT re-serializing through the\n * write chain. MUST only be called from inside a `queueCritical(...)` section\n * (which already holds the serialized turn); calling it directly would bypass\n * the read-before-append ordering that prevents lost-field races.\n */\n private async appendUnchained(record: NamespaceRecord): Promise<void> {\n const line = serializeRecord(record) + \"\\n\";\n const lineByteLength = Buffer.byteLength(line, \"utf8\");\n await mkdir(this.stateDir, { recursive: true });\n await appendFile(this.catalogPath, line, \"utf8\");\n if (this.onAfterCatalogAppendForTest) {\n await this.onAfterCatalogAppendForTest();\n }\n await this.refreshCacheAfterAppend(record, lineByteLength);\n }\n\n /**\n * Atomic temp-file + rename rewrite (CLAUDE.md rule #54: write temp, then\n * rename — never delete-before-write) WITHOUT re-entering the write chain.\n * MUST only be called from inside a `queueCritical(...)` turn (e.g. the\n * rebuild critical section, which already holds the serialized turn so its\n * load and rewrite are atomic against concurrent touches). Re-entering the\n * chain from within a held turn would deadlock.\n */\n private async rewriteUnchained(records: NamespaceRecord[]): Promise<void> {\n const body = records.map((r) => serializeRecord(r)).join(\"\\n\") + (records.length > 0 ? \"\\n\" : \"\");\n await mkdir(this.stateDir, { recursive: true });\n const tmp = `${this.catalogPath}.${process.pid}.${Date.now()}.tmp`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, this.catalogPath);\n await this.refreshCacheIdentity(new Map(records.map((record) => [record.namespace, record])));\n }\n\n private async refreshCacheAfterAppend(\n record: NamespaceRecord,\n lineByteLength: number,\n ): Promise<void> {\n const cached = this.compactedCache;\n if (!cached) return;\n const expectedSize = cached.identity.size + lineByteLength;\n try {\n const fileStat = await stat(this.catalogPath);\n if (fileStat.size !== expectedSize || fileStat.ino !== cached.identity.ino) {\n this.compactedCache = undefined;\n return;\n }\n const prior = cached.records.get(record.namespace);\n cached.records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);\n this.compactedCache = {\n identity: {\n size: fileStat.size,\n mtimeMs: fileStat.mtimeMs,\n ctimeMs: fileStat.ctimeMs,\n ino: fileStat.ino,\n },\n records: cached.records,\n };\n } catch {\n this.compactedCache = undefined;\n }\n }\n\n private async refreshCacheIdentity(records: Map<string, NamespaceRecord>): Promise<void> {\n try {\n const fileStat = await stat(this.catalogPath);\n this.compactedCache = {\n identity: {\n size: fileStat.size,\n mtimeMs: fileStat.mtimeMs,\n ctimeMs: fileStat.ctimeMs,\n ino: fileStat.ino,\n },\n records,\n };\n } catch {\n this.compactedCache = undefined;\n }\n }\n}\n\nfunction isPathInside(root: string, child: string): boolean {\n const relative = path.relative(root, child);\n return relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n"],"mappings":"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","names":["stat"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SUPPORT_PASSPORT_DRAFT_MAX_CONTENT_CHARACTERS,
|
|
3
3
|
SUPPORT_PASSPORT_DRAFT_MAX_MEMORIES
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2JL7WPXL.js";
|
|
5
5
|
import {
|
|
6
6
|
SUPPORT_PASSPORT_SOURCE_MEMORY_ID_MAX_LENGTH
|
|
7
7
|
} from "./chunk-XDL5CYZN.js";
|
|
@@ -343,4 +343,4 @@ export {
|
|
|
343
343
|
parseSupportPassportModelJob,
|
|
344
344
|
SupportPassportModelBridge
|
|
345
345
|
};
|
|
346
|
-
//# sourceMappingURL=chunk-
|
|
346
|
+
//# sourceMappingURL=chunk-BXOZALJ6.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
readNamespaceMaintenanceLastRanStatuses,
|
|
4
4
|
readNamespaceMaintenanceStatuses,
|
|
5
5
|
runNamespaceMaintenancePlan
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-RSLRGRBJ.js";
|
|
7
7
|
import {
|
|
8
8
|
analyzeSessionIntegrity,
|
|
9
9
|
applySessionRepair,
|
|
@@ -1683,4 +1683,4 @@ export {
|
|
|
1683
1683
|
runBenchmarkRecall,
|
|
1684
1684
|
runOperatorRepair
|
|
1685
1685
|
};
|
|
1686
|
-
//# sourceMappingURL=chunk-
|
|
1686
|
+
//# sourceMappingURL=chunk-CKWOMJDY.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SupportPassportCreateGrantRequestSchema,
|
|
3
3
|
defineOperation
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2JL7WPXL.js";
|
|
5
5
|
import {
|
|
6
6
|
SupportPassportCardCategorySchema,
|
|
7
7
|
SupportPassportMemoryIdSchema,
|
|
@@ -220,4 +220,4 @@ var supportPassportGrantAskOperation = defineOperation({
|
|
|
220
220
|
result: await ctx.service.supportPassportAskGrant(grantId, secret, question, ctx.abortSignal)
|
|
221
221
|
})
|
|
222
222
|
});
|
|
223
|
-
//# sourceMappingURL=chunk-
|
|
223
|
+
//# sourceMappingURL=chunk-DFEYRCG2.js.map
|
|
@@ -410,6 +410,12 @@ var SmartBuffer = class {
|
|
|
410
410
|
log.debug(
|
|
411
411
|
`buffer[${bufferKey}]: ${mutation.turnCountInWindow} turns, signal=${mutation.signalLevel}, decision=${decision}`
|
|
412
412
|
);
|
|
413
|
+
if (decision !== "keep_buffering" && extractionTurns === void 0) {
|
|
414
|
+
extractionTurns = [
|
|
415
|
+
...mutation.retainedTurnsSnapshot,
|
|
416
|
+
...mutation.activeTurnsSnapshot
|
|
417
|
+
];
|
|
418
|
+
}
|
|
413
419
|
return extractionTurns ? { decision, extractionTurns } : { decision };
|
|
414
420
|
}
|
|
415
421
|
async recordTurnUnlocked(bufferKey, turn) {
|
|
@@ -792,4 +798,4 @@ var SmartBuffer = class {
|
|
|
792
798
|
export {
|
|
793
799
|
SmartBuffer
|
|
794
800
|
};
|
|
795
|
-
//# sourceMappingURL=chunk-
|
|
801
|
+
//# sourceMappingURL=chunk-DQV3LUQL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/buffer.ts"],"sourcesContent":["import { log } from \"./logger.js\";\nimport { throwIfAborted } from \"./abort-error.js\";\nimport { scanSignals } from \"./signal.js\";\nimport type { StorageManager } from \"./storage.js\";\nimport type {\n BufferEntryState,\n BufferState,\n BufferSurpriseEvent,\n BufferTurn,\n PluginConfig,\n SignalLevel,\n} from \"./types.js\";\nimport { resolvePresentationCapabilities } from \"./capabilities.js\";\nimport { resolvePrincipal } from \"./namespaces/principal.js\";\nimport { ExtractionDeadlineError } from \"./orchestration/extraction-run.js\";\nimport type { ExtractionBufferSnapshot } from \"./extraction-liveness.js\";\nimport {\n bufferTurnArrayIsSuffixOfSnapshot,\n bufferTurnArraysEqual,\n bufferTurnsEqual,\n copyBufferTurn,\n describeError,\n liveTurnsFromExtractionSnapshot,\n matchingQueuedExtractionPrefixLength,\n probeWithTimeout,\n} from \"./buffer-turn-helpers.js\";\n\nexport type TriggerDecision = \"extract_now\" | \"extract_batch\" | \"keep_buffering\";\n\nexport interface AddTurnOutcome {\n decision: TriggerDecision;\n extractionTurns?: BufferTurn[];\n}\n\nexport interface RetainedTurnCleanupOptions {\n abortSignal?: AbortSignal;\n deadlineMs?: number;\n}\n\n/**\n * Optional surprise probe injected into `SmartBuffer`.\n *\n * Computes a D-MEM-style novelty score in `[0, 1]` for an incoming turn.\n * The buffer treats the probe as purely additive: if it is not provided, if\n * the feature flag is off, or if the probe throws/times out, the buffer\n * falls back to the existing signal/turn-count/time triggers unchanged.\n *\n * Callers are responsible for sampling recent memories and passing them\n * through the embedding pipeline — the buffer does not want to know about\n * storage, embeddings, or QMD.\n *\n * @param bufferKey Identifier for the active buffer (session/thread).\n * @param turn The incoming turn whose novelty is being scored.\n * @param recentTurns Turns already buffered for this key (most recent first\n * is NOT guaranteed — treat as unordered corpus).\n * @returns A surprise score in `[0, 1]`, or `null` if no score could be\n * produced (e.g. empty corpus, probe declined to embed).\n */\nexport interface BufferSurpriseProbe {\n scoreTurn(\n bufferKey: string,\n turn: BufferTurn,\n recentTurns: readonly BufferTurn[],\n ): Promise<number | null>;\n}\n\nconst MAX_BUFFER_ENTRY_COUNT = 200;\n\n/**\n * Upper bound on how long a debounced buffer save may be deferred under\n * sustained activity (issue #1909). Each new turn re-arms the trailing-edge\n * timer, so a steady stream of turns could otherwise push the save out\n * indefinitely and widen the crash-loss window without bound. Once a pending\n * save has been deferred this many debounce windows, the next scheduled save\n * forces an inline flush instead of re-arming.\n */\nconst BUFFER_SAVE_MAX_DEFER_MULTIPLIER = 5;\n\n/**\n * Node's maximum 32-bit `setTimeout` delay (2^31-1 ms, ~24.8 days). A larger\n * value is overflow-clamped by Node to 1ms with a TimeoutOverflowWarning, so we\n * clamp defensively at every timer arm (issue #1909 review round 5) even though\n * parseConfig already caps `bufferSaveDebounceMs` — a directly-constructed config\n * could still exceed it.\n */\nconst MAX_SET_TIMEOUT_MS = 2_147_483_647;\n\n/**\n * Minimal data carried on the serialized telemetry write chain\n * (issue #563 PR 3).\n *\n * We intentionally do NOT capture the full `BufferTurn` here: under\n * slow filesystem latency the chain can back up, and retaining\n * `turn.content` for every pending append causes memory pressure on\n * large conversations. Only the fields the ledger row actually needs\n * cross the chain boundary.\n */\ninterface SurpriseTelemetryQueueEntry {\n bufferKey: string;\n turnRole: \"user\" | \"assistant\";\n sessionKey: string | null;\n surpriseScore: number;\n triggered: boolean;\n turnCountInWindow: number;\n /**\n * ISO timestamp captured at the moment the turn was scored, NOT when\n * the ledger append eventually runs. Backpressure on the serialized\n * write chain could otherwise shift event timestamps away from the\n * real decision moment and distort the distribution report (p90\n * inflated, current-threshold row misidentified).\n */\n timestamp: string;\n /**\n * Threshold value in force when `triggered` was computed. Must be\n * snapshot here rather than read from `config` at emit time — a\n * concurrent config change between queue and write would otherwise\n * record `triggered=true` against a newer threshold the operator\n * never set, distorting precision/recall interpretation.\n */\n threshold: number;\n}\n\ninterface AddTurnMutationResult {\n decision: TriggerDecision;\n signalLevel: SignalLevel;\n priorTurns: BufferTurn[];\n activeTurnsSnapshot: BufferTurn[];\n retainedTurnsSnapshot: BufferTurn[];\n turnCountInWindow: number;\n}\n\nexport class SmartBuffer {\n private state: BufferState;\n private loaded = false;\n private loadPromise: Promise<void> | null = null;\n private readonly surpriseProbe: BufferSurpriseProbe | null;\n private mutationChain: Promise<unknown> = Promise.resolve();\n /**\n * Serialized write chain for `BUFFER_SURPRISE` telemetry events.\n *\n * The telemetry path is fire-and-forget (`addTurn` does not await the\n * ledger append), but multiple concurrent appends would still settle\n * out of order under variable filesystem latency. The report path\n * assumes chronological ordering — it slices the tail of the ledger\n * and treats the most recent entry as the current threshold in force.\n * Chaining ensures each append only runs after the previous settles,\n * preserving wall-clock order.\n *\n * We include a `.catch` on every link so a rejected append does not\n * poison the chain (CLAUDE.md rule #40).\n */\n private surpriseTelemetryWriteChain: Promise<unknown> = Promise.resolve();\n\n /**\n * Debounced buffer-save state (issue #1909). Steady-state buffering used to\n * serialize + rewrite the whole multi-session buffer on EVERY turn. We now\n * coalesce those writes onto a trailing-edge timer; correctness boundaries\n * (extraction trigger, extraction clear, shutdown) still force an immediate\n * flush. `bufferSaveDebounceMs: 0` restores save-every-turn exactly.\n */\n private saveTimer: NodeJS.Timeout | null = null;\n private pendingSave = false;\n /** Wall-clock ms when the currently-pending save was first scheduled (issue\n * #1909). Null when nothing is pending. Used to bound deferral under sustained\n * activity (see BUFFER_SAVE_MAX_DEFER_MULTIPLIER). */\n private firstPendingAtMs: number | null = null;\n\n constructor(\n private readonly config: PluginConfig,\n private readonly storage: StorageManager,\n surpriseProbe: BufferSurpriseProbe | null = null,\n ) {\n this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };\n this.surpriseProbe = surpriseProbe;\n }\n\n private enqueueMutation<T>(op: () => Promise<T>): Promise<T> {\n const run = this.mutationChain.catch(() => {}).then(op);\n this.mutationChain = run.catch(() => {});\n return run;\n }\n\n private entryFor(key: string): BufferEntryState {\n if (!this.state.entries) {\n this.state.entries = Object.create(null) as NonNullable<BufferState[\"entries\"]>;\n }\n const entries = this.state.entries as NonNullable<BufferState[\"entries\"]>;\n if (Object.hasOwn(entries, key)) {\n const stored = entries[key];\n // Guard against corrupted state/buffer.json — if the stored entry\n // is not a valid object shape, discard it and recreate.\n if (stored && typeof stored === \"object\" && Array.isArray(stored.turns)) {\n return stored;\n }\n // Corrupted — fall through to recreate.\n }\n const created: BufferEntryState = {\n turns: [],\n lastExtractionAt: null,\n extractionCount: 0,\n };\n entries[key] = created;\n return created;\n }\n\n private peekEntry(key: string): BufferEntryState | null {\n const existing = this.state.entries?.[key];\n if (existing) return existing;\n if (key !== \"default\") return null;\n return {\n turns: Array.isArray(this.state.turns) ? this.state.turns : [],\n lastExtractionAt: this.state.lastExtractionAt ?? null,\n extractionCount:\n typeof this.state.extractionCount === \"number\" ? this.state.extractionCount : 0,\n };\n }\n\n private normalizeState(state: BufferState): BufferState {\n const entries = Object.assign(\n Object.create(null),\n state.entries ?? {},\n ) as NonNullable<BufferState[\"entries\"]>;\n if (!entries.default) {\n entries.default = {\n turns: Array.isArray(state.turns) ? [...state.turns] : [],\n lastExtractionAt: state.lastExtractionAt ?? null,\n extractionCount:\n typeof state.extractionCount === \"number\" ? state.extractionCount : 0,\n };\n }\n return {\n turns: entries.default.turns,\n lastExtractionAt: entries.default.lastExtractionAt,\n extractionCount: entries.default.extractionCount,\n entries,\n };\n }\n\n private entryActivityAt(entry: BufferEntryState): number {\n const lastTurnAt = entry.turns.reduce((latest, turn) => {\n const parsed = Date.parse(turn.timestamp);\n return Number.isFinite(parsed) ? Math.max(latest, parsed) : latest;\n }, -1);\n const lastExtractionAt =\n typeof entry.lastExtractionAt === \"string\"\n ? Date.parse(entry.lastExtractionAt)\n : Number.NaN;\n return Math.max(\n lastTurnAt,\n Number.isFinite(lastExtractionAt) ? lastExtractionAt : -1,\n );\n }\n\n private pruneEntries(retainKeys: string[]): void {\n const entries = this.state.entries;\n if (!entries) return;\n const keys = Object.keys(entries);\n if (keys.length <= MAX_BUFFER_ENTRY_COUNT) return;\n\n const insertionOrder = new Map(keys.map((key, index) => [key, index]));\n const byOldestActivity = (left: string, right: string): number => {\n const leftAt = this.entryActivityAt(entries[left] ?? {\n turns: [],\n lastExtractionAt: null,\n extractionCount: 0,\n });\n const rightAt = this.entryActivityAt(entries[right] ?? {\n turns: [],\n lastExtractionAt: null,\n extractionCount: 0,\n });\n if (leftAt !== rightAt) return leftAt - rightAt;\n return (insertionOrder.get(left) ?? 0) - (insertionOrder.get(right) ?? 0);\n };\n const candidates = keys.filter((key) => key !== \"default\" && !retainKeys.includes(key));\n const emptyRemovable = candidates\n .filter((key) => (entries[key]?.turns.length ?? 0) === 0)\n .sort(byOldestActivity);\n\n const removableCount = Math.max(0, keys.length - MAX_BUFFER_ENTRY_COUNT);\n let evicted = emptyRemovable.slice(0, removableCount);\n if (evicted.length < removableCount) {\n // Retained failed extractions (#1908 retry retention) keep their turns in\n // the buffer, so empty entries alone may not bring the map back under the\n // cap during a provider outage. The cap is the documented bound — evict\n // the oldest NON-empty sessions too rather than growing unboundedly\n // (codex review).\n const nonEmptyRemovable = candidates\n .filter((key) => (entries[key]?.turns.length ?? 0) > 0)\n .sort(byOldestActivity);\n evicted = evicted.concat(nonEmptyRemovable.slice(0, removableCount - evicted.length));\n }\n for (const key of evicted) {\n delete entries[key];\n }\n if (evicted.length > 0) {\n // Loud degradation (extraction hot-loop hardening, Step 7): during a\n // prolonged provider outage the breaker holds extraction off and buffered\n // sessions accumulate. Eviction of the oldest empty session entries past\n // MAX_BUFFER_ENTRY_COUNT is bounded and expected, but must be observable —\n // not silent — so operators can see buffer pressure.\n log.warn(\n `buffer: pruned ${evicted.length} oldest session entr${evicted.length === 1 ? \"y\" : \"ies\"} ` +\n `past MAX_BUFFER_ENTRY_COUNT=${MAX_BUFFER_ENTRY_COUNT}; buffered turn data for those sessions was dropped`,\n );\n }\n }\n\n private async loadUnlocked(): Promise<void> {\n if (this.loaded) return;\n if (!this.loadPromise) {\n this.loadPromise = this.storage.loadBuffer()\n .then((state) => {\n this.state = this.normalizeState(state);\n this.loaded = true;\n })\n .finally(() => {\n this.loadPromise = null;\n });\n }\n await this.loadPromise;\n }\n\n async load(): Promise<void> {\n await this.enqueueMutation(async () => this.loadUnlocked());\n }\n\n /** Snapshot of buffered, not-yet-extracted work across sessions (#2151): session count,\n * pending-turn total, oldest buffered turn. Counts retained (#562) + active turns per entry. */\n async getBufferSnapshot(): Promise<ExtractionBufferSnapshot> {\n await this.load();\n let bufferedSessionCount = 0;\n let pendingTurnCount = 0;\n let oldestMs = Number.POSITIVE_INFINITY;\n let oldestTurnTimestamp: string | null = null;\n const consider = (turns: BufferTurn[] | undefined): void => {\n if (!Array.isArray(turns) || turns.length === 0) return;\n bufferedSessionCount += 1;\n pendingTurnCount += turns.length;\n const first = turns[0]?.timestamp;\n const parsed = typeof first === \"string\" ? Date.parse(first) : Number.NaN;\n if (Number.isFinite(parsed) && parsed < oldestMs) {\n oldestMs = parsed;\n oldestTurnTimestamp = first ?? null;\n }\n };\n const entries = this.state.entries;\n if (entries) {\n // Retained turns (deferred #562) are pending too - count with active (oldest-first), like getTurns().\n for (const entry of Object.values(entries)) consider([...(entry?.retainedTurns ?? []), ...(entry?.turns ?? [])]);\n } else {\n consider(this.state.turns);\n }\n return { bufferedSessionCount, pendingTurnCount, oldestTurnTimestamp };\n }\n\n /**\n * Reset the buffer to an empty, usable state.\n * Called when the persisted buffer file is corrupt and load() fails,\n * so the buffer can still accept new turns for the rest of the session.\n */\n resetToEmpty(): void {\n this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };\n this.loaded = true;\n }\n\n private async saveUnlocked(): Promise<void> {\n await this.storage.saveBuffer(this.state);\n }\n\n async save(): Promise<void> {\n await this.enqueueMutation(async () => this.saveUnlocked());\n }\n\n /**\n * Schedule a coalesced, TRUE trailing-edge buffer save (issue #1909). Only\n * used for the debounced (`bufferSaveDebounceMs > 0`) steady-state buffering\n * path; the debounce-off and correctness-boundary paths save inline within the\n * mutation (see `recordTurnUnlocked`). Each call re-arms the timer from now so\n * the write lands one full window after the LAST turn, not the first. To keep\n * sustained activity from deferring the save without bound (which would widen\n * the crash-loss window), once a pending save has been deferred\n * BUFFER_SAVE_MAX_DEFER_MULTIPLIER windows it is flushed inline instead of\n * re-armed. Runs inside the record mutation, so the inline save is awaited.\n */\n private async scheduleSave(): Promise<void> {\n const ms = Math.min(this.config.bufferSaveDebounceMs, MAX_SET_TIMEOUT_MS);\n const now = Date.now();\n this.pendingSave = true;\n if (this.firstPendingAtMs === null) this.firstPendingAtMs = now;\n if (now - this.firstPendingAtMs >= ms * BUFFER_SAVE_MAX_DEFER_MULTIPLIER) {\n // Staleness cap hit: persist now rather than deferring further. Drop the\n // armed timer, attempt the write, and only mark clean on success — a\n // failure keeps the save pending + re-arms so shutdown/the next timer\n // retries (issue #1909 review round 2), never diverging memory from disk.\n clearTimeout(this.saveTimer ?? undefined);\n this.saveTimer = null;\n try {\n await this.saveUnlocked();\n this.pendingSave = false;\n this.firstPendingAtMs = null;\n } catch (err) {\n log.warn(`buffer.scheduleSave: staleness-cap save failed, keeping it pending: ${describeError(err)}`);\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.flushPendingSave();\n }, ms);\n this.saveTimer.unref?.();\n }\n return;\n }\n // True trailing edge: drop any armed timer and re-arm from now.\n clearTimeout(this.saveTimer ?? undefined);\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.flushPendingSave();\n }, ms);\n this.saveTimer.unref?.();\n }\n\n /** Cancel any armed debounced save without persisting. */\n private cancelScheduledSave(): void {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n this.pendingSave = false;\n this.firstPendingAtMs = null;\n }\n\n /**\n * Persist the current in-memory buffer state inline, keeping the global\n * pending-save state armed until the write SUCCEEDS (issue #1909). On failure\n * the save stays pending and — when debounced — a background retry timer is\n * (re-)armed before the error propagates, so a graceful shutdown flush and the\n * next timer tick both retry. A failed post-mutation save therefore can never\n * silently drop buffered turns, including turns from OTHER sessions that were\n * only in the debounced pending state. The pending flag is cleared (and any\n * armed timer dropped) only after a durable write.\n */\n private async saveNowRetainingPendingOnFailure(context: string): Promise<void> {\n this.pendingSave = true;\n if (this.firstPendingAtMs === null) this.firstPendingAtMs = Date.now();\n try {\n await this.saveUnlocked();\n } catch (err) {\n log.warn(\n `buffer.${context}: inline save failed, keeping it pending for retry: ${describeError(err)}`,\n );\n const ms = Math.min(this.config.bufferSaveDebounceMs, MAX_SET_TIMEOUT_MS);\n if (ms > 0 && !this.saveTimer) {\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.flushPendingSave();\n }, ms);\n this.saveTimer.unref?.();\n }\n throw err;\n }\n this.cancelScheduledSave();\n }\n\n /**\n * Force any pending debounced save to land now (issue #1909). Idempotent and\n * safe to call from outside a mutation (timer tick, shutdown/dispose, or the\n * surprise-promotion path in `addTurnWithOutcome`). Do NOT `await` this from\n * inside a mutation — it enqueues its own mutation and would deadlock the\n * serializer.\n *\n * `throwOnFailure` (review round 7 finding 5): when set, a failed write is\n * rethrown AFTER the keep-pending + re-arm bookkeeping, so the surprise\n * extraction boundary gets the same durability guarantee as the built-in\n * extract triggers (which propagate saveUnlocked failures) instead of\n * proceeding to extract_now on a non-durable triggering turn. Default false\n * keeps the fail-open behavior for the timer/shutdown callers.\n */\n async flushPendingSave(opts?: { throwOnFailure?: boolean }): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n if (!this.pendingSave) return;\n try {\n await this.enqueueMutation(async () => {\n // Re-check under the serializer: a concurrent flush may have already\n // persisted this pending save. Skip the redundant write (no phantom\n // writes).\n if (!this.pendingSave) return;\n await this.saveUnlocked();\n // Clear the pending flag INSIDE the same serialized mutation as the\n // write (issue #1909 review). Clearing it after the enqueue await\n // resolved raced an addTurn interleaving between the write and the\n // clear: that turn's scheduleSave re-armed pendingSave + a timer, then\n // this clear wiped it and the debounced flush was lost. Atomic\n // write+clear means any addTurn runs wholly before (its turn is in the\n // write) or wholly after (its pendingSave survives and its timer\n // persists it).\n this.pendingSave = false;\n this.firstPendingAtMs = null;\n });\n } catch (err) {\n // The write failed: memory and disk diverge (issue #1909 review round 2).\n // Keep the save PENDING (do NOT clear pendingSave/firstPendingAtMs) so a\n // graceful shutdown flush and the re-armed timer both retry it — clearing\n // the flag here would drop the buffered turns permanently. Re-arm a\n // background retry when debounced.\n log.warn(`buffer.flushPendingSave: save failed, keeping it pending for retry: ${describeError(err)}`);\n const ms = Math.min(this.config.bufferSaveDebounceMs, MAX_SET_TIMEOUT_MS);\n if (ms > 0 && !this.saveTimer) {\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.flushPendingSave();\n }, ms);\n this.saveTimer.unref?.();\n }\n // Surprise-extraction boundary needs the durability guarantee — surface\n // the failure so the caller does not proceed with a non-durable flush.\n if (opts?.throwOnFailure) throw err;\n return; // fail-open for timer/shutdown callers\n }\n }\n\n async addTurn(bufferKey: string, turn: BufferTurn): Promise<TriggerDecision> {\n return (await this.addTurnWithOutcome(bufferKey, turn)).decision;\n }\n\n async addTurnWithOutcome(\n bufferKey: string,\n turn: BufferTurn,\n ): Promise<AddTurnOutcome> {\n const mutation = await this.enqueueMutation(() => this.recordTurnUnlocked(bufferKey, turn));\n let decision = mutation.decision;\n let extractionTurns: BufferTurn[] | undefined;\n\n // Surprise-gated flush (issue #563). Additive only: if the probe is\n // disabled, unavailable, or the score is below threshold, the decision\n // from the existing trigger logic stands. The probe only ever *promotes*\n // `keep_buffering` → `extract_now`; it never suppresses an existing\n // flush. This preserves the invariant that enabling surprise cannot\n // *reduce* extraction frequency.\n if (\n decision === \"keep_buffering\" &&\n resolvePresentationCapabilities(this.config).bufferSurpriseTrigger &&\n this.surpriseProbe !== null &&\n // Matching the existing \"smart\" branch: surprise is a lower-tier\n // novelty signal that should not second-guess a high-signal hit\n // (which already flushes) or fight `every_n` / `time_based` modes.\n this.config.triggerMode === \"smart\" &&\n mutation.signalLevel !== \"high\"\n ) {\n const surprise = await this.computeSurpriseSafe(bufferKey, turn, mutation.priorTurns);\n if (surprise !== null) {\n const shouldPromote = surprise > this.config.bufferSurpriseThreshold;\n let triggered = false;\n if (shouldPromote) {\n const currentTurns = await this.getExtractionTurnsIfBufferSnapshotStillCurrent(\n bufferKey,\n mutation.activeTurnsSnapshot,\n mutation.retainedTurnsSnapshot,\n );\n if (currentTurns) {\n log.debug(\n `buffer[${bufferKey}]: surprise=${surprise.toFixed(3)} > threshold=${this.config.bufferSurpriseThreshold} → extract_now`,\n );\n decision = \"extract_now\";\n triggered = true;\n extractionTurns = currentTurns;\n // Issue #1909 (review): the record mutation for a `keep_buffering`\n // turn only SCHEDULED a debounced save. Surprise now promotes it to\n // extract_now AFTER that mutation, so force the buffer durable before\n // the caller runs extraction — otherwise state/buffer.json lags the\n // extracted turns by up to the debounce window on a crash. Safe to\n // await here: we are outside the record mutation (deadlock-free).\n // throwOnFailure (round 7 finding 5): match the built-in extract\n // triggers — a failed durability write must NOT silently proceed to\n // extract_now on a non-durable turn.\n await this.flushPendingSave({ throwOnFailure: true });\n } else {\n log.debug(\n `buffer[${bufferKey}]: surprise=${surprise.toFixed(3)} ignored because buffer changed before probe resolved`,\n );\n }\n }\n // Emit telemetry on every scored turn — both triggering and\n // non-triggering — so operators can fit the threshold to real\n // traffic distributions. Fire-and-forget: `addTurn` does NOT\n // await the ledger append, so slow/contended filesystems cannot\n // add JSONL-append latency to every `processTurn`. But we DO\n // serialize writes through a promise chain so concurrent\n // appends settle in wall-clock order — the report path assumes\n // chronological tail rows and reads the most recent as the\n // \"current\" threshold.\n //\n // Project only the fields we need into the queue entry rather\n // than capturing the full `BufferTurn` — under slow filesystem\n // latency the chain can back up, and we must not retain the\n // (potentially large) `turn.content` string for every pending\n // append.\n this.queueSurpriseTelemetryWrite({\n bufferKey,\n turnRole: turn.role,\n sessionKey:\n typeof turn.sessionKey === \"string\" ? turn.sessionKey : null,\n surpriseScore: surprise,\n triggered,\n turnCountInWindow: mutation.turnCountInWindow,\n // Stamp at decision time so backpressure on the write chain\n // does not shift the event's apparent moment away from when\n // the turn was actually scored.\n timestamp: new Date().toISOString(),\n // Snapshot the threshold used to compute `triggered` so a\n // concurrent config mutation cannot retroactively change\n // what the ledger row claims the decision was against.\n threshold: this.config.bufferSurpriseThreshold,\n });\n }\n }\n\n log.debug(\n `buffer[${bufferKey}]: ${mutation.turnCountInWindow} turns, signal=${mutation.signalLevel}, decision=${decision}`,\n );\n if (decision !== \"keep_buffering\" && extractionTurns === undefined) {\n extractionTurns = [\n ...mutation.retainedTurnsSnapshot,\n ...mutation.activeTurnsSnapshot,\n ];\n }\n return extractionTurns ? { decision, extractionTurns } : { decision };\n }\n\n private async recordTurnUnlocked(bufferKey: string, turn: BufferTurn): Promise<AddTurnMutationResult> {\n await this.loadUnlocked();\n const entry = this.entryFor(bufferKey);\n const priorTurns = entry.turns.slice();\n entry.turns.push(turn);\n const activeTurnsSnapshot = entry.turns.map(copyBufferTurn);\n const retainedTurnsSnapshot = (entry.retainedTurns ?? []).map(copyBufferTurn);\n if (bufferKey === \"default\") {\n this.state.turns = entry.turns;\n }\n\n const signal = scanSignals(turn.content, this.config.highSignalPatterns);\n const decision = this.evaluate(entry, signal.level);\n const turnCountInWindow = entry.turns.length;\n\n this.pruneEntries([bufferKey]);\n if (decision === \"keep_buffering\" && this.config.bufferSaveDebounceMs > 0) {\n // Steady-state buffering: coalesce the whole-state serialize onto a\n // trailing-edge timer (issue #1909) instead of rewriting per turn.\n await this.scheduleSave();\n } else {\n // Persist immediately within this mutation (awaited) when either:\n // - the turn triggered extraction (the buffer must be durable), or\n // - debounce is disabled (bufferSaveDebounceMs <= 0), which reproduces\n // the legacy save-every-turn behavior byte-for-byte.\n // Round 8 thread 4: the write is attempted BEFORE the pending state is\n // cleared, and a failed write leaves it pending (+ a re-armed timer) for\n // shutdown/timer retry instead of dropping the in-memory turns.\n await this.saveNowRetainingPendingOnFailure(\"recordTurn\");\n }\n return {\n decision,\n signalLevel: signal.level,\n priorTurns,\n activeTurnsSnapshot,\n retainedTurnsSnapshot,\n turnCountInWindow,\n };\n }\n\n private async getExtractionTurnsIfBufferSnapshotStillCurrent(\n bufferKey: string,\n activeTurnsSnapshot: readonly BufferTurn[],\n retainedTurnsSnapshot: readonly BufferTurn[],\n ): Promise<BufferTurn[] | null> {\n return this.enqueueMutation(async () => {\n await this.loadUnlocked();\n const entry = this.peekEntry(bufferKey);\n if (!entry) return null;\n const retained = entry.retainedTurns ?? [];\n const stillCurrent =\n bufferTurnArrayIsSuffixOfSnapshot(entry.turns, activeTurnsSnapshot) &&\n bufferTurnArraysEqual(retained, retainedTurnsSnapshot);\n if (!stillCurrent) return null;\n return [...retained, ...entry.turns];\n });\n }\n\n /**\n * Enqueue a telemetry append on the serialized write chain.\n *\n * The chain is a classic `writeChain = writeChain.then(fn).catch(...)`\n * — each link waits for the previous to settle before its append\n * starts, so out-of-order chronology cannot happen even under\n * variable filesystem latency. We always attach `.catch` so one\n * rejection does not poison the chain for the rest of the session\n * (CLAUDE.md rule #40). The error is logged through\n * `emitSurpriseEventSafe` itself, which swallows its own rejections.\n *\n * Public surface is deliberately narrow — only `addTurn` should call\n * this, so the surprise telemetry path stays centralized.\n */\n private queueSurpriseTelemetryWrite(params: SurpriseTelemetryQueueEntry): void {\n this.surpriseTelemetryWriteChain = this.surpriseTelemetryWriteChain\n .then(() => this.emitSurpriseEventSafe(params))\n .catch(() => {\n // `emitSurpriseEventSafe` already handles the logging. We\n // swallow here only so one failure does not break the chain\n // for future writes.\n });\n }\n\n /**\n * Append a single `BUFFER_SURPRISE` telemetry row (issue #563 PR 3).\n *\n * Deliberately swallows write errors: the buffer must never fail to\n * record a turn because the observation ledger is read-only, out of\n * disk, or otherwise unhappy. The log line at debug lets operators\n * confirm the path fired without polluting the error channel.\n */\n private async emitSurpriseEventSafe(\n params: SurpriseTelemetryQueueEntry,\n ): Promise<void> {\n const storage = this.storage as StorageManager & {\n appendBufferSurpriseEvents?: (\n events: BufferSurpriseEvent[],\n ) => Promise<number>;\n };\n if (typeof storage.appendBufferSurpriseEvents !== \"function\") {\n // Older StorageManager / test double without the telemetry sink.\n // Silently skip — core path is still covered by the log line above.\n return;\n }\n const event: BufferSurpriseEvent = {\n event: \"BUFFER_SURPRISE\",\n // Use the decision-time stamp captured when the event was\n // queued, NOT `Date.now()` here — backpressure on the write\n // chain could otherwise shift timestamps into the future relative\n // to when the turn was scored.\n timestamp: params.timestamp,\n bufferKey: params.bufferKey,\n sessionKey: params.sessionKey,\n turnRole: params.turnRole,\n surpriseScore: params.surpriseScore,\n // Use the snapshotted threshold from the queue entry, not the\n // live config — see `SurpriseTelemetryQueueEntry.threshold`\n // doc for the rationale.\n threshold: params.threshold,\n triggeredFlush: params.triggered,\n turnCountInWindow: params.turnCountInWindow,\n };\n try {\n await storage.appendBufferSurpriseEvents([event]);\n } catch (err) {\n // Same guard as `computeSurpriseSafe`: non-Error rejections must\n // not crash the telemetry helper, which would defeat the whole\n // point of isolating the ledger write from the hot path.\n log.debug(\n `buffer[${params.bufferKey}]: surprise telemetry write failed, continuing: ${describeError(err)}`,\n );\n }\n }\n\n /**\n * Invoke the injected surprise probe defensively. Any error (probe throws,\n * embedder unavailable, timeout) is swallowed and logged at debug: the\n * surprise path must never crash the happy-path trigger evaluation. A\n * `null` return indicates \"no score available, fall through to existing\n * triggers\".\n */\n private async computeSurpriseSafe(\n bufferKey: string,\n turn: BufferTurn,\n priorTurns: readonly BufferTurn[],\n ): Promise<number | null> {\n if (!this.surpriseProbe) return null;\n try {\n // Hard timeout around the probe so a hung embedder cannot stall\n // `addTurn()` before `save()`. A slow probe would otherwise\n // prevent the just-appended turn from ever being persisted. The\n // timeout is a soft bound — we race it against the probe, take\n // whichever settles first, and treat the timeout as\n // \"probe unavailable, fall through\" rather than an error that\n // surfaces to the caller.\n const score = await probeWithTimeout(\n this.surpriseProbe.scoreTurn(bufferKey, turn, priorTurns),\n this.config.bufferSurpriseProbeTimeoutMs,\n );\n if (score === null) return null;\n if (typeof score !== \"number\" || !Number.isFinite(score)) {\n log.debug(\n `buffer[${bufferKey}]: surprise probe returned non-finite score (${String(score)}), ignoring`,\n );\n return null;\n }\n // Defensive clamp: formula lives in buffer-surprise.ts, but we never\n // want a misbehaving probe to inject an out-of-range value into the\n // threshold comparison.\n if (score < 0) return 0;\n if (score > 1) return 1;\n return score;\n } catch (err) {\n // `err` may be any thrown value — `throw null` and\n // `Promise.reject(\"x\")` are both legal. Accessing `.message` on a\n // non-Error would itself throw and defeat the failure-isolation\n // contract, so describe the value safely.\n log.debug(\n `buffer[${bufferKey}]: surprise probe failed, falling back to existing triggers: ${describeError(err)}`,\n );\n return null;\n }\n }\n\n private evaluate(entry: BufferEntryState, signalLevel: SignalLevel): TriggerDecision {\n if (this.config.triggerMode === \"smart\") {\n if (signalLevel === \"high\") return \"extract_now\";\n\n if (entry.turns.length >= this.config.bufferMaxTurns) {\n return \"extract_batch\";\n }\n\n if (entry.lastExtractionAt) {\n const elapsed =\n Date.now() - new Date(entry.lastExtractionAt).getTime();\n if (elapsed >= this.config.bufferMaxMinutes * 60_000) {\n return \"extract_batch\";\n }\n }\n\n return \"keep_buffering\";\n }\n\n if (this.config.triggerMode === \"every_n\") {\n return entry.turns.length >= this.config.bufferMaxTurns\n ? \"extract_batch\"\n : \"keep_buffering\";\n }\n\n if (this.config.triggerMode === \"time_based\") {\n if (!entry.lastExtractionAt) {\n return entry.turns.length >= this.config.bufferMaxTurns\n ? \"extract_batch\"\n : \"keep_buffering\";\n }\n const elapsed =\n Date.now() - new Date(entry.lastExtractionAt).getTime();\n return elapsed >= this.config.bufferMaxMinutes * 60_000\n ? \"extract_batch\"\n : \"keep_buffering\";\n }\n\n return \"keep_buffering\";\n }\n\n getTurns(bufferKey = \"default\"): BufferTurn[] {\n const entry = this.peekEntry(bufferKey);\n if (!entry) return [];\n const retained = entry.retainedTurns ?? [];\n // Retained turns (from a previous defer verdict, issue #562 PR 2) are\n // prepended so the chronological order — oldest context first — is\n // preserved for the next extraction pass.\n return [...retained, ...entry.turns];\n }\n\n /**\n * Retain a subset of the current turns across `clearAfterExtraction` so a\n * future extraction pass sees the context behind a deferred candidate\n * (issue #562, PR 2). Callers pass the turns that were seen during the\n * current extraction; the buffer keeps the tail (latest `max` turns) as\n * the retention window. Passing an empty array or `max <= 0` clears the\n * retention slot instead.\n */\n async retainDeferredTurns(\n bufferKey: string,\n turns: BufferTurn[],\n max = 10,\n ): Promise<void> {\n await this.enqueueMutation(async () => {\n await this.loadUnlocked();\n const entry = this.entryFor(bufferKey);\n if (!Array.isArray(turns) || turns.length === 0 || max <= 0) {\n delete entry.retainedTurns;\n } else {\n // Guard `slice(-max)` against `max === 0` (CLAUDE.md gotcha 27):\n // `slice(-0)` equals `slice(0)` and would return ALL entries. We\n // already early-return above when max <= 0.\n const tail = turns.slice(-max);\n // Copy explicit fields only — never spread an external object into a\n // plain object because spread preserves any own `__proto__` /\n // `constructor` keys that may have arrived via JSON deserialization\n // of untrusted input (CodeQL js/prototype-polluting-assignment).\n entry.retainedTurns = tail.map<BufferTurn>((t) => {\n const copy: BufferTurn = {\n role: t.role,\n content: typeof t.content === \"string\" ? t.content : \"\",\n timestamp:\n typeof t.timestamp === \"string\"\n ? t.timestamp\n : new Date().toISOString(),\n };\n if (typeof t.sessionKey === \"string\") copy.sessionKey = t.sessionKey;\n if (typeof t.sessionOwnerPrincipal === \"string\") {\n copy.sessionOwnerPrincipal = t.sessionOwnerPrincipal;\n }\n if (typeof t.logicalSessionKey === \"string\") {\n copy.logicalSessionKey = t.logicalSessionKey;\n }\n if (\n t.providerThreadId === null ||\n typeof t.providerThreadId === \"string\"\n ) {\n copy.providerThreadId = t.providerThreadId;\n }\n if (typeof t.turnFingerprint === \"string\") {\n copy.turnFingerprint = t.turnFingerprint;\n }\n if (typeof t.persistProcessedFingerprint === \"boolean\") {\n copy.persistProcessedFingerprint = t.persistProcessedFingerprint;\n }\n if (typeof t.sourceConnector === \"string\") copy.sourceConnector = t.sourceConnector;\n return copy;\n });\n }\n await this.saveUnlocked();\n });\n }\n\n\n /**\n * Return the current retention window (issue #562, PR 2). Primarily for\n * tests and diagnostics.\n */\n getRetainedDeferredTurns(bufferKey = \"default\"): BufferTurn[] {\n const entry = this.peekEntry(bufferKey);\n return entry?.retainedTurns ? [...entry.retainedTurns] : [];\n }\n\n /**\n * Clear deferred retention copies for a force-flushed session. Normal\n * extraction preserves these copies so a deferred candidate can be retried;\n * an explicit force flush is the caller's request to drain that context.\n */\n async clearRetainedTurnsForSession(\n sessionKey: string,\n ownerPrincipal?: string,\n options: RetainedTurnCleanupOptions = {},\n ): Promise<void> {\n if (typeof sessionKey !== \"string\" || sessionKey.length === 0) return;\n const normalizedOwnerPrincipal =\n typeof ownerPrincipal === \"string\" && ownerPrincipal.trim().length > 0\n ? ownerPrincipal.trim()\n : undefined;\n const assertLifecycle = (): void => {\n throwIfAborted(options.abortSignal, \"extraction force-flush aborted\");\n if (typeof options.deadlineMs === \"number\" && Date.now() >= options.deadlineMs) {\n throw new ExtractionDeadlineError(\"retained_turn_cleanup\");\n }\n };\n assertLifecycle();\n await this.enqueueMutation(async () => {\n assertLifecycle();\n const hadPendingSave = this.pendingSave;\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n await this.loadUnlocked();\n assertLifecycle();\n const bufferKeys = this.matchingSessionBufferKeysUnlocked(sessionKey);\n if (bufferKeys.length === 0) {\n if (hadPendingSave) {\n assertLifecycle();\n await this.saveNowRetainingPendingOnFailure(\"clearRetainedTurnsForSession\");\n }\n return;\n }\n let changed = false;\n const belongsToOwner = (turn: BufferTurn): boolean => {\n if (turn.sessionKey !== sessionKey) return false;\n if (normalizedOwnerPrincipal === undefined) return true;\n if (turn.sessionOwnerPrincipal === normalizedOwnerPrincipal) return true;\n return (\n turn.sessionOwnerPrincipal === undefined &&\n resolvePrincipal(turn.sessionKey, this.config) === normalizedOwnerPrincipal\n );\n };\n for (const bufferKey of bufferKeys) {\n const entry = this.entryFor(bufferKey);\n const retainedTurns = entry.retainedTurns ?? [];\n const remainingTurns = retainedTurns.filter((turn) => !belongsToOwner(turn));\n if (remainingTurns.length === retainedTurns.length) continue;\n changed = true;\n if (remainingTurns.length > 0) entry.retainedTurns = remainingTurns;\n else delete entry.retainedTurns;\n if (bufferKey === \"default\") this.state.turns = entry.turns;\n }\n if (changed || hadPendingSave) {\n assertLifecycle();\n await this.saveNowRetainingPendingOnFailure(\"clearRetainedTurnsForSession\");\n }\n assertLifecycle();\n });\n }\n\n async findBufferKeyForSession(sessionKey: string): Promise<string | null> {\n const bufferKeys = await this.findBufferKeysForSession(sessionKey);\n return bufferKeys[0] ?? null;\n }\n\n private matchingSessionBufferKeysUnlocked(sessionKey: string): string[] {\n const matches: string[] = [];\n const hasSessionTurns = (entry: BufferEntryState | null | undefined): boolean =>\n [...(entry?.turns ?? []), ...(entry?.retainedTurns ?? [])].some(\n (turn) => typeof turn.sessionKey === \"string\" && turn.sessionKey === sessionKey,\n );\n const directEntry = this.peekEntry(sessionKey);\n if (hasSessionTurns(directEntry)) {\n matches.push(sessionKey);\n }\n\n const entries = this.state.entries ?? {};\n for (const [bufferKey, entry] of Object.entries(entries)) {\n if (!matches.includes(bufferKey) && hasSessionTurns(entry)) {\n matches.push(bufferKey);\n }\n }\n\n return matches;\n }\n\n async findBufferKeysForSession(sessionKey: string): Promise<string[]> {\n if (typeof sessionKey !== \"string\" || sessionKey.length === 0) return [];\n await this.mutationChain.catch(() => {});\n await this.load();\n return this.matchingSessionBufferKeysUnlocked(sessionKey);\n }\n\n async clearAfterExtraction(\n bufferKey = \"default\",\n extractedTurns?: readonly BufferTurn[],\n options?: { allowNonPrefix?: boolean },\n ): Promise<void> {\n await this.enqueueMutation(async () => {\n // Drop any armed debounce TIMER so it cannot fire mid-mutation and race\n // our post-clear write, but KEEP the pending-save state (issue #1909\n // review round 13): the post-clear save below can fail, and clearing the\n // pending flag here would strand buffered turns — including turns from\n // OTHER sessions that only ever entered the debounced pending state — with\n // nothing left to retry them. saveNowRetainingPendingOnFailure clears the\n // pending state only after a durable write, or re-arms it on failure.\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n await this.loadUnlocked();\n const entry = this.entryFor(bufferKey);\n if (Array.isArray(extractedTurns)) {\n const liveExtractedTurns = liveTurnsFromExtractionSnapshot(\n entry,\n extractedTurns,\n );\n let clearedLiveTurns = false;\n if (liveExtractedTurns.length > 0) {\n if (options?.allowNonPrefix === true) {\n const remainingTurns = [...entry.turns];\n for (const extractedTurn of liveExtractedTurns) {\n const matchingIndex = remainingTurns.findIndex((liveTurn) =>\n bufferTurnsEqual(liveTurn, extractedTurn),\n );\n if (matchingIndex >= 0) {\n remainingTurns.splice(matchingIndex, 1);\n clearedLiveTurns = true;\n }\n }\n if (clearedLiveTurns) entry.turns = remainingTurns;\n } else {\n const matchedCount = matchingQueuedExtractionPrefixLength(\n entry.turns,\n liveExtractedTurns,\n );\n if (matchedCount > 0) {\n entry.turns = entry.turns.slice(matchedCount);\n clearedLiveTurns = true;\n }\n }\n if (!clearedLiveTurns) {\n log.debug(\n `buffer[${bufferKey}]: extraction clear skipped because live turns changed before clear`,\n );\n }\n }\n if (!clearedLiveTurns) {\n await this.saveNowRetainingPendingOnFailure(\"clearAfterExtraction\");\n return;\n }\n } else {\n entry.turns = [];\n }\n entry.lastExtractionAt = new Date().toISOString();\n entry.extractionCount += 1;\n if (bufferKey === \"default\") {\n this.state.turns = entry.turns;\n this.state.lastExtractionAt = entry.lastExtractionAt;\n this.state.extractionCount = entry.extractionCount;\n }\n this.pruneEntries([bufferKey]);\n await this.saveNowRetainingPendingOnFailure(\"clearAfterExtraction\");\n });\n }\n\n getExtractionCount(bufferKey = \"default\"): number {\n return this.peekEntry(bufferKey)?.extractionCount ?? 0;\n }\n\n /**\n * Await any pending `BUFFER_SURPRISE` telemetry writes.\n *\n * The telemetry path is fire-and-forget from the hot path's point of\n * view, but tests and before-exit hooks sometimes need to make sure\n * the ledger has been flushed before they assert on its contents or\n * close the process. This method resolves once the current chain\n * head has settled; new writes scheduled after this call return a\n * separate, later settlement.\n *\n * Never throws — the chain already catches its own rejections.\n */\n async flushSurpriseTelemetry(): Promise<void> {\n await this.surpriseTelemetryWriteChain;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
externalWikiSearchOperation
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QOMUE4FS.js";
|
|
4
4
|
import {
|
|
5
5
|
memoryStoreRequestSchema
|
|
6
6
|
} from "./chunk-CIAI24TQ.js";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
DELTA_SUBCOMMANDS,
|
|
11
11
|
defineOperation,
|
|
12
12
|
isCodegraphToolName
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-2JL7WPXL.js";
|
|
14
14
|
import {
|
|
15
15
|
serializeMutations,
|
|
16
16
|
withHeldFileLock
|
|
@@ -1818,4 +1818,4 @@ export {
|
|
|
1818
1818
|
codegraphIngestTracesOperation,
|
|
1819
1819
|
REGISTERED_OPERATIONS
|
|
1820
1820
|
};
|
|
1821
|
-
//# sourceMappingURL=chunk-
|
|
1821
|
+
//# sourceMappingURL=chunk-EJCN5ATI.js.map
|