@remnic/core 1.1.8 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-cli.js +30 -30
- package/dist/access-http.d.ts +7 -7
- package/dist/access-http.js +13 -13
- package/dist/access-mcp.d.ts +7 -7
- package/dist/access-mcp.js +12 -12
- package/dist/{access-service-C0Rkioec.d.ts → access-service-BJCIjVRY.d.ts} +9 -9
- package/dist/access-service.d.ts +7 -7
- package/dist/access-service.js +11 -11
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +2 -2
- package/dist/active-recall.js +2 -2
- package/dist/active-recall.js.map +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +6 -6
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +6 -6
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +2 -2
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +2 -2
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-ETA2JXP5.js → chunk-2MVUXO4H.js} +2 -2
- package/dist/{chunk-GZCUW5IC.js → chunk-3IQ2TR4N.js} +5 -5
- package/dist/chunk-3IQ2TR4N.js.map +1 -0
- package/dist/{chunk-KNQ5YJTO.js → chunk-3VRIIII5.js} +149 -1
- package/dist/chunk-3VRIIII5.js.map +1 -0
- package/dist/{chunk-TUFG6VXY.js → chunk-4DWOBS2A.js} +2 -2
- package/dist/chunk-4DWOBS2A.js.map +1 -0
- package/dist/{chunk-L2IO2QPY.js → chunk-4IS4SXIQ.js} +17 -13
- package/dist/chunk-4IS4SXIQ.js.map +1 -0
- package/dist/{chunk-RLV2F337.js → chunk-6OAQEOGV.js} +2 -2
- package/dist/{chunk-QJZ77K7F.js → chunk-6Z6UH6TK.js} +26 -12
- package/dist/chunk-6Z6UH6TK.js.map +1 -0
- package/dist/{chunk-4IT6WL23.js → chunk-7SFAENUZ.js} +2 -2
- package/dist/{chunk-ODWDQNRE.js → chunk-7SI52C65.js} +7 -3
- package/dist/chunk-7SI52C65.js.map +1 -0
- package/dist/{chunk-Q5TJRAGE.js → chunk-A6PGANSE.js} +3 -3
- package/dist/{chunk-OJMD2LIW.js → chunk-BIHCWSWA.js} +3 -3
- package/dist/{chunk-MYH2IBSP.js → chunk-CTYRIJ5E.js} +3 -3
- package/dist/{chunk-T65SHTJP.js → chunk-ET4BL42V.js} +1 -1
- package/dist/chunk-ET4BL42V.js.map +1 -0
- package/dist/{chunk-DWMXVUGO.js → chunk-FLBYSB2V.js} +6 -4
- package/dist/chunk-FLBYSB2V.js.map +1 -0
- package/dist/{chunk-GRDDGNYQ.js → chunk-FPWUENQH.js} +34 -32
- package/dist/chunk-FPWUENQH.js.map +1 -0
- package/dist/chunk-FVQJYWH7.js +52 -0
- package/dist/chunk-FVQJYWH7.js.map +1 -0
- package/dist/{chunk-STB3GUYU.js → chunk-G3G3LY22.js} +2 -2
- package/dist/{chunk-FIXIX6DE.js → chunk-G6NX57V2.js} +33 -43
- package/dist/chunk-G6NX57V2.js.map +1 -0
- package/dist/{chunk-3FPTCC3Z.js → chunk-GVPWB7EY.js} +2 -2
- package/dist/{chunk-AV2WSYZY.js → chunk-ICULSMDG.js} +2 -2
- package/dist/{chunk-WXPPM426.js → chunk-J3P6WSFZ.js} +2 -2
- package/dist/{chunk-FCGWNWG4.js → chunk-KIF7QNKL.js} +28 -28
- package/dist/chunk-KIF7QNKL.js.map +1 -0
- package/dist/{chunk-XVOIMCVW.js → chunk-KMWZXT5T.js} +2 -2
- package/dist/{chunk-SWRJFKYW.js → chunk-M3DK45UM.js} +5 -5
- package/dist/{chunk-XGX4TUF6.js → chunk-MJLUHRSF.js} +5 -5
- package/dist/{chunk-4KAN3GZ3.js → chunk-NN2DKE4T.js} +1 -1
- package/dist/chunk-NN2DKE4T.js.map +1 -0
- package/dist/{chunk-R2XRID2N.js → chunk-NN3LPQ5D.js} +5 -5
- package/dist/chunk-NN3LPQ5D.js.map +1 -0
- package/dist/{chunk-RJSVRPNU.js → chunk-OWGGXPKV.js} +16 -9
- package/dist/chunk-OWGGXPKV.js.map +1 -0
- package/dist/{chunk-WSZIHQBK.js → chunk-P77UEOU2.js} +4 -1
- package/dist/{chunk-WSZIHQBK.js.map → chunk-P77UEOU2.js.map} +1 -1
- package/dist/{chunk-65ZPH7QA.js → chunk-PHQH2VUO.js} +4 -4
- package/dist/{chunk-KHJRMWO4.js → chunk-QPLYTPYL.js} +15 -15
- package/dist/{chunk-FEMOX5AD.js → chunk-QR3C7BKQ.js} +7 -7
- package/dist/chunk-QR3C7BKQ.js.map +1 -0
- package/dist/{chunk-3LCWFNVS.js → chunk-SKE7JYKA.js} +2 -2
- package/dist/{chunk-E27HOXMX.js → chunk-U4SZXGEO.js} +2 -2
- package/dist/{chunk-67YLUWLG.js → chunk-XJKFSSDW.js} +3 -3
- package/dist/chunk-XJKFSSDW.js.map +1 -0
- package/dist/{chunk-SYWJJTNL.js → chunk-XL3UCAZA.js} +22 -22
- package/dist/{chunk-ASIQZXYO.js → chunk-XMVFHBHT.js} +2 -2
- package/dist/{chunk-SRIDOT64.js → chunk-XN4D6Z7X.js} +3 -3
- package/dist/{chunk-S5SQDIF5.js → chunk-Y3VT6ZCP.js} +4 -4
- package/dist/{cli-CIATRu8o.d.ts → cli-BojuyOOp.d.ts} +4 -4
- package/dist/cli.d.ts +8 -8
- package/dist/cli.js +24 -24
- package/dist/{codex-materialize-xVqbEmcm.d.ts → codex-materialize-YVC2wb6n.d.ts} +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/day-summary.d.ts +1 -1
- package/dist/day-summary.js +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/{engine-MEAYUA7A.js → engine-EDFFOWDD.js} +7 -7
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +6 -6
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +6 -6
- package/dist/explicit-capture.js +2 -2
- package/dist/explicit-cue-recall.js +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +7 -7
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.js +147 -147
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/live-connectors-runner.js +2 -2
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-llm.js +1 -1
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/{memory-governance-G3XODEXW.js → memory-governance-AAQPBZEP.js} +7 -7
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/{memory-projection-store-lCzmu4JX.d.ts → memory-projection-store-BW8u5U0u.d.ts} +1 -1
- package/dist/memory-projection-store.d.ts +2 -2
- package/dist/memory-projection-store.js +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/{migrate-from-identity-anchor-TTEDEJGX.js → migrate-from-identity-anchor-G27MCD6A.js} +2 -2
- package/dist/model-registry.js +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/models-json.js +1 -1
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +10 -10
- package/dist/opik-exporter.js +2 -2
- package/dist/opik-exporter.js.map +1 -1
- package/dist/{orchestrator-CvUYwuaL.d.ts → orchestrator-CYqmqxco.d.ts} +5 -5
- package/dist/orchestrator.d.ts +6 -6
- package/dist/orchestrator.js +25 -25
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{peers-6OSQ3NK6.js → peers-HCVGHMAE.js} +3 -3
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/{port-BkWL7hqo.d.ts → port-Br27H8dy.d.ts} +7 -1
- package/dist/qmd-recall-cache.d.ts +2 -2
- package/dist/qmd.d.ts +3 -2
- package/dist/qmd.js +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/sanitize.js +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/{semantic-consolidation-CGiH52qa.d.ts → semantic-consolidation-GPcLr9BQ.d.ts} +2 -2
- package/dist/semantic-consolidation.d.ts +3 -3
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +6 -6
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +6 -6
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/source-attribution.d.ts +1 -1
- package/dist/source-attribution.js +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +5 -5
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +5 -5
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +3 -3
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/{types-H85grL1f.d.ts → types-Bmp9ssU2.d.ts} +3 -3
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +6 -6
- package/package.json +1 -1
- package/dist/chunk-4KAN3GZ3.js.map +0 -1
- package/dist/chunk-67YLUWLG.js.map +0 -1
- package/dist/chunk-DWMXVUGO.js.map +0 -1
- package/dist/chunk-FCGWNWG4.js.map +0 -1
- package/dist/chunk-FEMOX5AD.js.map +0 -1
- package/dist/chunk-FIXIX6DE.js.map +0 -1
- package/dist/chunk-GRDDGNYQ.js.map +0 -1
- package/dist/chunk-GZCUW5IC.js.map +0 -1
- package/dist/chunk-KNQ5YJTO.js.map +0 -1
- package/dist/chunk-L2IO2QPY.js.map +0 -1
- package/dist/chunk-M62O4P4T.js +0 -41
- package/dist/chunk-M62O4P4T.js.map +0 -1
- package/dist/chunk-ODWDQNRE.js.map +0 -1
- package/dist/chunk-QJZ77K7F.js.map +0 -1
- package/dist/chunk-R2XRID2N.js.map +0 -1
- package/dist/chunk-RJSVRPNU.js.map +0 -1
- package/dist/chunk-T65SHTJP.js.map +0 -1
- package/dist/chunk-TUFG6VXY.js.map +0 -1
- /package/dist/{chunk-ETA2JXP5.js.map → chunk-2MVUXO4H.js.map} +0 -0
- /package/dist/{chunk-RLV2F337.js.map → chunk-6OAQEOGV.js.map} +0 -0
- /package/dist/{chunk-4IT6WL23.js.map → chunk-7SFAENUZ.js.map} +0 -0
- /package/dist/{chunk-Q5TJRAGE.js.map → chunk-A6PGANSE.js.map} +0 -0
- /package/dist/{chunk-OJMD2LIW.js.map → chunk-BIHCWSWA.js.map} +0 -0
- /package/dist/{chunk-MYH2IBSP.js.map → chunk-CTYRIJ5E.js.map} +0 -0
- /package/dist/{chunk-STB3GUYU.js.map → chunk-G3G3LY22.js.map} +0 -0
- /package/dist/{chunk-3FPTCC3Z.js.map → chunk-GVPWB7EY.js.map} +0 -0
- /package/dist/{chunk-AV2WSYZY.js.map → chunk-ICULSMDG.js.map} +0 -0
- /package/dist/{chunk-WXPPM426.js.map → chunk-J3P6WSFZ.js.map} +0 -0
- /package/dist/{chunk-XVOIMCVW.js.map → chunk-KMWZXT5T.js.map} +0 -0
- /package/dist/{chunk-SWRJFKYW.js.map → chunk-M3DK45UM.js.map} +0 -0
- /package/dist/{chunk-XGX4TUF6.js.map → chunk-MJLUHRSF.js.map} +0 -0
- /package/dist/{chunk-65ZPH7QA.js.map → chunk-PHQH2VUO.js.map} +0 -0
- /package/dist/{chunk-KHJRMWO4.js.map → chunk-QPLYTPYL.js.map} +0 -0
- /package/dist/{chunk-3LCWFNVS.js.map → chunk-SKE7JYKA.js.map} +0 -0
- /package/dist/{chunk-E27HOXMX.js.map → chunk-U4SZXGEO.js.map} +0 -0
- /package/dist/{chunk-SYWJJTNL.js.map → chunk-XL3UCAZA.js.map} +0 -0
- /package/dist/{chunk-ASIQZXYO.js.map → chunk-XMVFHBHT.js.map} +0 -0
- /package/dist/{chunk-SRIDOT64.js.map → chunk-XN4D6Z7X.js.map} +0 -0
- /package/dist/{chunk-S5SQDIF5.js.map → chunk-Y3VT6ZCP.js.map} +0 -0
- /package/dist/{engine-MEAYUA7A.js.map → engine-EDFFOWDD.js.map} +0 -0
- /package/dist/{memory-governance-G3XODEXW.js.map → memory-governance-AAQPBZEP.js.map} +0 -0
- /package/dist/{migrate-from-identity-anchor-TTEDEJGX.js.map → migrate-from-identity-anchor-G27MCD6A.js.map} +0 -0
- /package/dist/{peers-6OSQ3NK6.js.map → peers-HCVGHMAE.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["export type ReasoningEffort = \"none\" | \"low\" | \"medium\" | \"high\";\nexport type TriggerMode = \"smart\" | \"every_n\" | \"time_based\";\nexport type SignalLevel = \"none\" | \"low\" | \"medium\" | \"high\";\nexport type MemoryCategory = \"fact\" | \"preference\" | \"correction\" | \"entity\" | \"decision\" | \"relationship\" | \"principle\" | \"commitment\" | \"moment\" | \"skill\" | \"rule\" | \"procedure\" | \"reasoning_trace\";\nexport type ConsolidationAction = \"ADD\" | \"MERGE\" | \"UPDATE\" | \"INVALIDATE\" | \"SKIP\";\nexport type ConfidenceTier = \"explicit\" | \"implied\" | \"inferred\" | \"speculative\";\nexport type PrincipalFromSessionKeyMode = \"map\" | \"prefix\" | \"regex\";\nexport type RecallPlanMode = \"no_recall\" | \"minimal\" | \"full\" | \"graph_mode\";\nexport type CronRecallMode = \"all\" | \"none\" | \"allowlist\";\nexport type CronConversationRecallMode = \"auto\" | \"always\" | \"never\";\nexport type IdentityInjectionMode = \"recovery_only\" | \"minimal\" | \"full\";\nexport type CaptureMode = \"implicit\" | \"explicit\" | \"hybrid\";\nexport type MemoryOsPresetName = \"conservative\" | \"balanced\" | \"research-max\" | \"local-llm-heavy\";\nexport type ExtractionPassSource = \"base\" | \"proactive\";\n/**\n * Scope classification for extracted facts (issue #XXX).\n *\n * - `\"project\"` — knowledge specific to one codebase: file paths, environment\n * configs, deployment details, project-specific workarounds, team/stakeholder\n * info tied to one project.\n * - `\"global\"` — knowledge that applies across projects: core framework bugs,\n * library behavior, API patterns, user preferences, tool configurations,\n * general coding patterns, infrastructure knowledge.\n *\n * Default is `\"project\"` when a coding context is active, `\"global\"` when no\n * coding context is present.\n */\nexport type MemoryScope = \"project\" | \"global\";\nexport type SlotMismatchMode = \"error\" | \"warn\" | \"silent\";\nexport type CodexCompactionFlushMode = \"signal\" | \"heuristic\" | \"auto\";\nexport type DreamingNarrativePromptStyle = \"reflective\" | \"diary\" | \"analytical\";\nexport type HeartbeatDetectionMode = \"runtime-signal\" | \"heuristic\" | \"auto\";\nexport type ActiveRecallQueryMode = \"message\" | \"recent\" | \"full\";\nexport type ActiveRecallPromptStyle =\n | \"balanced\"\n | \"strict\"\n | \"contextual\"\n | \"recall-heavy\"\n | \"precision-heavy\"\n | \"preference-only\";\nexport type ActiveRecallThinking =\n | \"off\"\n | \"minimal\"\n | \"low\"\n | \"medium\"\n | \"high\"\n | \"xhigh\"\n | \"adaptive\";\nexport type ActiveRecallChatType = \"direct\" | \"group\" | \"channel\";\nexport type ActiveRecallModelFallbackPolicy = \"default-remote\" | \"resolved-only\";\n\n/**\n * Retrieval tier ladder (issue #518). Identifies which tier served a recall\n * result. Ordered top-to-bottom by cost, but routing is not strictly\n * sequential — callers may jump straight to a lower tier when eligibility\n * does not hold.\n */\nexport type RetrievalTier =\n | \"exact-cache\"\n | \"fuzzy-cache\"\n | \"direct-answer\"\n | \"hybrid\"\n | \"rerank-graph\"\n | \"agentic\";\n\n/**\n * Per-recall annotation describing which retrieval tier served a result,\n * why that tier was chosen, and what was filtered along the way. Added as\n * part of issue #518 (direct-answer tier + `query --explain`).\n *\n * Not to be confused with the existing `recallExplain` operation\n * (graph-path explanation) — that is a user-invoked RPC; this is a\n * per-result annotation that can be attached to any recall response.\n */\nexport interface RecallTierExplain {\n tier: RetrievalTier;\n tierReason: string;\n filteredBy: string[];\n candidatesConsidered: number;\n latencyMs: number;\n sourceAnchors?: Array<{ path: string; lineRange?: [number, number] }>;\n}\n\n/**\n * Recall disclosure depth (issue #677). Selects how much content each\n * recall result returns:\n *\n * - `\"chunk\"` — semantic chunk excerpt (cheapest; default).\n * - `\"section\"` — full markdown section / memory body (current pre-#677 behavior).\n * - `\"raw\"` — raw transcript / archive excerpts from `lcm/` when present.\n *\n * Disclosure is **orthogonal** to the retrieval-tier ladder\n * (`RetrievalTier` / `RETRIEVAL_TIERS`). The tier ladder controls *which\n * pipeline stage served a result*; disclosure controls *how deep into the\n * underlying memory the result reaches*. A request can mix any retrieval\n * tier with any disclosure depth.\n *\n * Default is `\"chunk\"` when the caller omits the field; this preserves the\n * existing recall behavior because callers that did not request a disclosure\n * level continue to receive the same chunk-shaped previews they always had.\n * Surfaces (CLI / HTTP / MCP) and downstream telemetry are wired in later\n * PRs of #677.\n */\nexport type RecallDisclosure = \"chunk\" | \"section\" | \"raw\";\n\n/**\n * Ordered list of disclosure levels, cheapest to most expensive. Used for\n * validation, escalation policy comparisons, and future telemetry rollups.\n * Treat this as the single source of truth — do not hard-code disclosure\n * strings elsewhere.\n */\nexport const RECALL_DISCLOSURE_LEVELS: readonly RecallDisclosure[] = [\n \"chunk\",\n \"section\",\n \"raw\",\n] as const;\n\n/**\n * Default disclosure level when a caller omits `disclosure`. Set to `chunk`\n * so callers that did not opt in to deeper disclosure see the same\n * preview-shaped behavior as before #677.\n */\nexport const DEFAULT_RECALL_DISCLOSURE: RecallDisclosure = \"chunk\";\n\nexport function isRecallDisclosure(value: unknown): value is RecallDisclosure {\n return typeof value === \"string\"\n && (RECALL_DISCLOSURE_LEVELS as readonly string[]).includes(value);\n}\n\nexport interface RecallSectionConfig {\n id: string;\n enabled?: boolean;\n maxChars?: number | null;\n maxHints?: number;\n maxSupportingFacts?: number;\n maxRelatedEntities?: number;\n consolidateTriggerLines?: number;\n consolidateTargetLines?: number;\n maxEntities?: number;\n maxResults?: number;\n recentTurns?: number;\n maxTurns?: number;\n maxTokens?: number;\n lookbackHours?: number;\n maxCount?: number;\n topK?: number;\n timeoutMs?: number;\n maxPatterns?: number;\n maxRubrics?: number;\n}\n\nexport interface RecallPipelineConfig {\n recallBudgetChars: number;\n pipeline: RecallSectionConfig[];\n}\n\nexport interface SessionObserverBandConfig {\n maxBytes: number;\n triggerDeltaBytes: number;\n triggerDeltaTokens: number;\n}\n\nexport interface FileHygieneConfig {\n enabled: boolean;\n // Lint (warn before truncation risk)\n lintEnabled: boolean;\n lintBudgetBytes: number;\n lintWarnRatio: number;\n lintPaths: string[];\n // Rotation/splitting\n rotateEnabled: boolean;\n rotateMaxBytes: number;\n rotateKeepTailChars: number;\n rotatePaths: string[];\n archiveDir: string;\n // Cadence\n runMinIntervalMs: number;\n // Optional warnings log (future-proofed)\n warningsLogEnabled: boolean;\n warningsLogPath: string;\n // Optional index file (future-proofed)\n indexEnabled: boolean;\n indexPath: string;\n}\n\nexport interface NativeKnowledgeConfig {\n enabled: boolean;\n includeFiles: string[];\n maxChunkChars: number;\n maxResults: number;\n maxChars: number;\n stateDir: string;\n obsidianVaults: NativeKnowledgeObsidianVaultConfig[];\n openclawWorkspace?: NativeKnowledgeOpenClawWorkspaceConfig;\n}\n\nexport interface NativeKnowledgeFolderRuleConfig {\n pathPrefix: string;\n namespace?: string;\n privacyClass?: string;\n}\n\nexport interface NativeKnowledgeObsidianVaultConfig {\n id: string;\n rootDir: string;\n includeGlobs: string[];\n excludeGlobs: string[];\n namespace?: string;\n privacyClass?: string;\n folderRules: NativeKnowledgeFolderRuleConfig[];\n dailyNotePatterns: string[];\n materializeBacklinks: boolean;\n}\n\nexport interface NativeKnowledgeOpenClawWorkspaceConfig {\n enabled: boolean;\n bootstrapFiles: string[];\n handoffGlobs: string[];\n dailySummaryGlobs: string[];\n automationNoteGlobs: string[];\n workspaceDocGlobs: string[];\n excludeGlobs: string[];\n sharedSafeGlobs: string[];\n}\n\n/**\n * OpenClaw SecretRef shape (issue #757).\n *\n * OpenClaw resolves these at runtime via its built-in secret resolver\n * (e.g. exec providers like `kc_*` for macOS Keychain). Plugins receive\n * the raw object in `pluginConfig` and must call the gateway's resolver\n * before using the value. Standalone Remnic does NOT resolve SecretRefs;\n * operators must use plain strings or `${ENV_VAR}` expansion instead.\n */\nexport interface SecretRef {\n source: string;\n provider?: string;\n id?: string;\n command?: unknown;\n [key: string]: unknown;\n}\n\nexport type AgentAccessAuthToken = string | SecretRef;\n\nexport interface AgentAccessHttpConfig {\n enabled: boolean;\n host: string;\n port: number;\n /**\n * Bearer token. Either a literal string (env-expanded) or an unresolved\n * SecretRef object preserved verbatim from openclaw.json — resolved at\n * service-start time via {@link resolveAgentAccessAuthToken}.\n */\n authToken?: AgentAccessAuthToken;\n principal?: string;\n maxBodyBytes: number;\n}\n\nexport interface DreamingConfig {\n enabled: boolean;\n journalPath: string;\n maxEntries: number;\n injectRecentCount: number;\n minIntervalMinutes: number;\n narrativeModel: string | null;\n narrativePromptStyle: DreamingNarrativePromptStyle;\n watchFile: boolean;\n}\n\n/**\n * Light-sleep phase config (issue #678 PR 2/4).\n *\n * Groups existing top-level lifecycle-policy gates under a unified namespace.\n * When `dreams.phases.lightSleep.*` keys are set they WIN over the legacy\n * top-level keys; the legacy keys remain readable for backward compatibility.\n *\n * Light sleep: recent activity scoring + clustering (tier-routing value score,\n * observation ledger, buffer state — `runLifecyclePolicyPass` in orchestrator).\n */\nexport interface DreamsLightSleepConfig {\n /** Phase master switch. Mirrors `lifecyclePolicyEnabled` when not set explicitly. */\n enabled: boolean;\n /** Minimum interval between light-sleep passes in milliseconds. */\n cadenceMs: number;\n /** Value score above which a memory is treated as hot. Mirrors `lifecyclePromoteHeatThreshold`. */\n promoteHeatThreshold: number;\n /** Value score below which a memory starts to decay. Mirrors `lifecycleStaleDecayThreshold`. */\n staleDecayThreshold: number;\n /** Value score below which a memory is eligible for archive. Mirrors `lifecycleArchiveDecayThreshold`. */\n archiveDecayThreshold: number;\n /** Whether stale memories are filtered from recall. Mirrors `lifecycleFilterStaleEnabled`. */\n filterStaleEnabled: boolean;\n}\n\n/**\n * REM phase config (issue #678 PR 2/4).\n *\n * Groups existing top-level semantic-consolidation and supersession gates.\n * When `dreams.phases.rem.*` keys are set they WIN over the legacy top-level\n * keys.\n *\n * REM: cross-session synthesis, supersession resolution, semantic consolidation\n * (`runSemanticConsolidation` in orchestrator).\n */\nexport interface DreamsRemConfig {\n /** Phase master switch. Mirrors `semanticConsolidationEnabled` when not set explicitly. */\n enabled: boolean;\n /**\n * How often the REM pass runs, in milliseconds.\n * Derived from `semanticConsolidationIntervalHours` (×3 600 000) when not set explicitly.\n */\n cadenceMs: number;\n /** Cosine-similarity threshold for cluster membership. Mirrors `semanticConsolidationThreshold`. */\n similarityThreshold: number;\n /** Minimum cluster size before consolidation runs. Mirrors `semanticConsolidationMinClusterSize`. */\n minClusterSize: number;\n /** Max cluster operations per run. Mirrors `semanticConsolidationMaxPerRun`. */\n maxPerRun: number;\n /** Minimum gap between consolidation passes (ms). Mirrors `consolidationMinIntervalMs`. */\n minIntervalMs: number;\n}\n\n/**\n * Deep-sleep phase config (issue #678 PR 2/4).\n *\n * Groups existing versioning and tier-migration gates.\n * When `dreams.phases.deepSleep.*` keys are set they WIN over the legacy\n * top-level keys.\n *\n * Deep sleep: promotion to durable memory, hot→cold tier migration,\n * page-version snapshots, archive (`engram-nightly-governance` cron,\n * `tier-migration.ts`, `page-versioning.ts`, `hygiene.ts`).\n */\nexport interface DreamsDeepSleepConfig {\n /**\n * Phase master switch. No single direct legacy mirror; defaults false unless\n * an existing deep-sleep surface such as nightly governance auto-registration,\n * tier migration, or page versioning is explicitly enabled. Set to `false`\n * to disable those surfaces without removing legacy config keys.\n */\n enabled: boolean;\n /** True only when dreams.phases.deepSleep.enabled was explicitly configured. */\n enabledExplicitlySet?: boolean;\n /**\n * Minimum interval between deep-sleep passes in milliseconds.\n * Informational only in PR 2; PR 4 will wire this into the cron scheduler.\n */\n cadenceMs: number;\n /** Enable page-version snapshots on every overwrite. Mirrors `versioningEnabled`. */\n versioningEnabled: boolean;\n /** Max snapshots per page. Mirrors `versioningMaxPerPage`. */\n versioningMaxPerPage: number;\n}\n\n/**\n * Unified dreams phases config block (issue #678 PR 2/4).\n *\n * Operators set `dreams.phases.{lightSleep,rem,deepSleep}.*` in their plugin\n * config. Values under this block WIN over the equivalent legacy top-level keys\n * when both are set. Legacy keys continue to be parsed so existing configs do\n * not need to change.\n *\n * This block is intentionally separate from `DreamingConfig` which controls the\n * diary surface (`surfaces/dreams.ts`) — a different feature. See docs/dreams.md.\n */\nexport interface DreamsPhasesConfig {\n lightSleep: DreamsLightSleepConfig;\n rem: DreamsRemConfig;\n deepSleep: DreamsDeepSleepConfig;\n}\n\n/** Procedural memory (issue #519): mining + recall gates. All sub-features default off. */\nexport interface ProceduralConfig {\n enabled: boolean;\n /** Minimum cluster size before emitting a candidate; `0` disables mining (`minOccurrences_zero`). */\n minOccurrences: number;\n /** Minimum success rate from trajectory outcomes in [0, 1]. */\n successFloor: number;\n /** When auto-promotion is enabled, promote pending_review → active after this many occurrences. */\n autoPromoteOccurrences: number;\n autoPromoteEnabled: boolean;\n lookbackDays: number;\n /** When true, installer may register the nightly procedural mining cron (default off). */\n proceduralMiningCronAutoRegister: boolean;\n /** Max procedure memories to inject on task-initiation recall (1–10). */\n recallMaxProcedures: number;\n}\n\n/**\n * Coding-agent mode config (issue #569).\n *\n * When the connector provides a `CodingContext` (see below), Remnic overlays\n * a project- and/or branch-scoped namespace on top of the principal's default\n * namespace so that memories written while working on project A do not surface\n * while working on project B.\n *\n * Both flags default off-for-branch / on-for-project. Per CLAUDE.md #30 every\n * filter or transform needs an escape hatch: set `projectScope: false` to\n * exactly restore pre-#569 behaviour.\n */\nexport interface CodingModeConfig {\n /**\n * When true (default), a session with a resolved `CodingContext` uses a\n * project-scoped namespace. When false, the principal's default namespace\n * is used unchanged (pre-#569 behaviour).\n */\n projectScope: boolean;\n /**\n * When true, recall/write also overlay the current branch on top of the\n * project namespace. Default false — branch-scope is opt-in because active\n * development typically wants recall across branches. (Wired by PR 3 of\n * issue #569; declared here so the schema ships in one slice.)\n */\n branchScope: boolean;\n /**\n * When true (default), project-scoped and branch-scoped sessions include\n * the root/default namespace in their read fallbacks so globally useful\n * memories remain visible from any project. When false, project-scoped\n * sessions only see their own namespace (strict isolation).\n *\n * CLAUDE.md #30: configuration gate for the recall fan-out to the root\n * namespace. Does not affect writes — those always go to the project\n * namespace only.\n */\n globalFallback: boolean;\n}\n\n/**\n * Session-scoped coding context. Produced by `resolveGitContext()` in the\n * connector layer and attached to a session so that recall + write paths can\n * compute an overlay namespace.\n *\n * All fields mirror `GitContext` from `./coding/git-context.ts`; kept as a\n * separate interface because `types.ts` must stay dependency-free (it is\n * imported by every other module).\n */\nexport interface CodingContext {\n projectId: string;\n branch: string | null;\n rootPath: string;\n defaultBranch: string | null;\n}\n\n/** Configuration for the nightly contradiction-scan cron (issue #520). */\nexport interface ContradictionScanConfig {\n /** Master switch for the contradiction scan cron. Default true. */\n enabled: boolean;\n /** Embedding cosine similarity floor for candidate pair generation. Default 0.82. */\n similarityFloor: number;\n /** Minimum topic-token Jaccard overlap for unstructured pairs. Default 0.4. */\n topicOverlapFloor: number;\n /** Cap on candidate pairs evaluated per cron run. Default 500. */\n maxPairsPerRun: number;\n /** Cooldown in days before re-evaluating a pair judged independent/both-valid. Default 14. */\n cooldownDays: number;\n /** When true, pairs judged \"duplicates\" are auto-flagged for dedup (still need user approval). Default false. */\n autoMergeDuplicates: boolean;\n}\n\nexport interface HeartbeatConfig {\n enabled: boolean;\n journalPath: string;\n maxPreviousRuns: number;\n watchFile: boolean;\n detectionMode: HeartbeatDetectionMode;\n gateExtractionDuringHeartbeat: boolean;\n}\n\nexport interface SlotBehaviorConfig {\n requireExclusiveMemorySlot: boolean;\n onSlotMismatch: SlotMismatchMode;\n}\n\nexport interface CodexCompatConfig {\n enabled: boolean;\n threadIdBufferKeying: boolean;\n compactionFlushMode: CodexCompactionFlushMode;\n fingerprintDedup: boolean;\n}\n\nexport function confidenceTier(score: number): ConfidenceTier {\n if (score >= 0.95) return \"explicit\";\n if (score >= 0.70) return \"implied\";\n if (score >= 0.40) return \"inferred\";\n return \"speculative\";\n}\n\n/** Default TTL in days for speculative memories (auto-expire if unconfirmed) */\nexport const SPECULATIVE_TTL_DAYS = 30;\n\n/**\n * Shape for semantic chunking config overrides stored in PluginConfig.\n * Mirrors SemanticChunkingConfig from semantic-chunking.ts without creating\n * a circular import (types.ts is imported by everything).\n */\nexport interface SemanticChunkingConfigShape {\n targetTokens: number;\n minTokens: number;\n maxTokens: number;\n smoothingWindowSize: number;\n boundaryThresholdStdDevs: number;\n embeddingBatchSize: number;\n fallbackToRecursive: boolean;\n}\n\nexport interface PluginConfig {\n openaiApiKey: string | undefined;\n openaiBaseUrl: string | undefined;\n model: string;\n reasoningEffort: ReasoningEffort;\n triggerMode: TriggerMode;\n bufferMaxTurns: number;\n bufferMaxMinutes: number;\n /**\n * Surprise-gated buffer flush (issue #563, D-MEM).\n *\n * When enabled, every turn added to the smart buffer is scored against a\n * configurable window of recent memories using an embedding-distance proxy\n * for novelty (see `buffer-surprise.ts`). A turn whose surprise score\n * exceeds `bufferSurpriseThreshold` triggers an immediate extract flush,\n * even if the existing signal/turn-count/time triggers would otherwise keep\n * buffering. Disabled by default — when `false`, buffer behavior is\n * identical to pre-#563 code. Additive only: existing triggers are never\n * suppressed by this flag.\n */\n bufferSurpriseTriggerEnabled: boolean;\n /**\n * Threshold in `[0, 1]` above which a surprise score causes an immediate\n * flush. `0.35` is a conservative default chosen to favor precision over\n * recall during the opt-in phase. Ignored unless\n * `bufferSurpriseTriggerEnabled` is `true`.\n */\n bufferSurpriseThreshold: number;\n /**\n * Number of nearest neighbors to average over when computing the surprise\n * score (see `computeSurprise`). Default `5`. Clamped to the recent-memory\n * window size at call time.\n */\n bufferSurpriseK: number;\n /**\n * Maximum number of recent memories to sample when computing the surprise\n * score. Bounds embedding cost per turn. Default `20`. Set to `0` to\n * disable the trigger even when the flag is on (no memories to compare\n * against → treat as not-applicable rather than maximally surprising).\n */\n bufferSurpriseRecentMemoryCount: number;\n /**\n * Hard timeout (ms) for the surprise probe. If the probe does not\n * resolve within this window, the buffer treats the probe as failed,\n * logs at debug, and falls through to the existing triggers. Ensures\n * a slow or hung embedder cannot stall the turn-append path. Default\n * `2000` (2s).\n */\n bufferSurpriseProbeTimeoutMs: number;\n consolidateEveryN: number;\n highSignalPatterns: string[];\n maxMemoryTokens: number;\n memoryOsPreset?: MemoryOsPresetName;\n qmdEnabled: boolean;\n qmdCollection: string;\n qmdMaxResults: number;\n qmdColdTierEnabled?: boolean;\n qmdColdCollection?: string;\n qmdColdMaxResults?: number;\n qmdTierMigrationEnabled: boolean;\n qmdTierDemotionMinAgeDays: number;\n qmdTierDemotionValueThreshold: number;\n qmdTierPromotionValueThreshold: number;\n qmdTierParityGraphEnabled: boolean;\n qmdTierParityHiMemEnabled: boolean;\n qmdTierAutoBackfillEnabled: boolean;\n embeddingFallbackEnabled: boolean;\n embeddingFallbackProvider: \"auto\" | \"openai\" | \"local\";\n /** Optional absolute path to qmd binary. If unset, PATH/fallback discovery is used. */\n qmdPath?: string;\n memoryDir: string;\n debug: boolean;\n identityEnabled: boolean;\n identityContinuityEnabled: boolean;\n identityInjectionMode: IdentityInjectionMode;\n identityMaxInjectChars: number;\n continuityIncidentLoggingEnabled: boolean;\n continuityAuditEnabled: boolean;\n sessionObserverEnabled?: boolean;\n sessionObserverDebounceMs?: number;\n sessionObserverBands?: SessionObserverBandConfig[];\n injectQuestions: boolean;\n commitmentDecayDays: number;\n workspaceDir: string;\n captureMode: CaptureMode;\n fileHygiene?: FileHygieneConfig;\n nativeKnowledge?: NativeKnowledgeConfig;\n agentAccessHttp: AgentAccessHttpConfig;\n // Access tracking (Phase 1A)\n accessTrackingEnabled: boolean;\n accessTrackingBufferMaxSize: number;\n // Retrieval options\n recencyWeight: number;\n boostAccessCount: boolean;\n /** Record empty recall impressions (memoryIds: []) when no memories are injected. Disabled by default. */\n recordEmptyRecallImpressions: boolean;\n // v2.2 Advanced Retrieval\n queryExpansionEnabled: boolean;\n queryExpansionMaxQueries: number;\n /** Minimum token length to consider for query expansion. */\n queryExpansionMinTokenLen: number;\n rerankEnabled: boolean;\n /** Rerank provider. \"local\" uses Local LLM only; \"cloud\" uses gateway fallback chain. */\n rerankProvider: \"local\" | \"cloud\";\n rerankMaxCandidates: number;\n rerankTimeoutMs: number;\n rerankCacheEnabled: boolean;\n rerankCacheTtlMs: number;\n feedbackEnabled: boolean;\n // v2.2 Negative Examples (safe defaults: off unless enabled)\n /** If true, allow recording negative examples and apply a soft penalty during ranking. */\n negativeExamplesEnabled: boolean;\n /** Score penalty per \"not useful\" hit (typical QMD scores ~0-1). Keep small. */\n negativeExamplesPenaltyPerHit: number;\n /** Maximum penalty applied from negative examples. */\n negativeExamplesPenaltyCap: number;\n // Chunking (Phase 2A)\n chunkingEnabled: boolean;\n chunkingTargetTokens: number;\n chunkingMinTokens: number;\n chunkingOverlapSentences: number;\n // Semantic Chunking (Issue #368)\n /** Enable semantic chunking with embedding-based topic boundary detection. Default: false. */\n semanticChunkingEnabled: boolean;\n /** Optional overrides for the semantic chunking algorithm. */\n semanticChunkingConfig: Partial<SemanticChunkingConfigShape>;\n // Contradiction Detection (Phase 2B)\n contradictionDetectionEnabled: boolean;\n contradictionSimilarityThreshold: number;\n contradictionMinConfidence: number;\n contradictionAutoResolve: boolean;\n /** Nightly contradiction-scan cron config (issue #520). */\n contradictionScan: ContradictionScanConfig;\n // Temporal Supersession (issue #375)\n /**\n * When enabled, writes that carry `structuredAttributes` mark any older\n * fact with the same `entityRef + attribute_name` supersession key and a\n * conflicting value as `status: \"superseded\"`.\n */\n temporalSupersessionEnabled: boolean;\n /**\n * When enabled, superseded memories are still returned by recall (useful\n * for audit/history queries). Default: false — superseded memories are\n * filtered out.\n */\n temporalSupersessionIncludeInRecall: boolean;\n // Direct-answer retrieval tier (issue #518)\n /**\n * When true, recall checks whether a single validated memory in a\n * high-trust taxonomy bucket can answer the query before invoking QMD.\n * Default false — enable explicitly after bench validation.\n */\n recallDirectAnswerEnabled: boolean;\n /**\n * Disclosure auto-escalation policy (issue #677 PR 4/4). When set to\n * `\"auto\"`, recalls without an explicit caller-supplied disclosure\n * escalate from `chunk` to `section` if the top-K confidence falls\n * below {@link recallDisclosureEscalationThreshold}. `raw` is never\n * auto-selected — it requires an explicit caller request. Default\n * `\"manual\"` preserves pre-#677 behavior.\n */\n recallDisclosureEscalation: \"manual\" | \"auto\";\n /**\n * Top-K confidence threshold (in `[0, 1]`) below which auto-escalation\n * promotes `chunk` → `section`. Only consulted when\n * {@link recallDisclosureEscalation} is `\"auto\"`. Default `0.5`.\n */\n recallDisclosureEscalationThreshold: number;\n /**\n * Graph-based retrieval tier via Personalized PageRank (issue #559 PR 4).\n * When true, recall builds a retrieval graph from memory frontmatter\n * and runs PPR, merging the result with QMD via MMR. Default false —\n * ships off pending the retrieval-graph bench in PR 5.\n */\n recallGraphEnabled: boolean;\n /** PPR damping factor used when `recallGraphEnabled` is true. */\n recallGraphDamping: number;\n /** PPR power-iteration cap used when `recallGraphEnabled` is true. */\n recallGraphIterations: number;\n /**\n * Max memories returned by the graph tier before MMR. Set to 0 to\n * disable the graph tier's contribution without flipping the flag.\n */\n recallGraphTopK: number;\n /**\n * Minimum token-overlap ratio (query tokens ∩ memory tokens / query tokens)\n * required for direct-answer eligibility. Set to 0 to disable the gate.\n */\n recallDirectAnswerTokenOverlapFloor: number;\n /**\n * Minimum calibrated importance score required for direct-answer\n * eligibility. Set to 0 to disable the gate.\n */\n recallDirectAnswerImportanceFloor: number;\n /**\n * Ambiguity margin: if the second-best candidate scores within this\n * ratio of the top candidate, direct-answer defers to the hybrid tier.\n */\n recallDirectAnswerAmbiguityMargin: number;\n /**\n * Taxonomy category IDs eligible for direct-answer routing. Memories\n * whose resolved taxonomy category is not in this list never qualify.\n */\n recallDirectAnswerEligibleTaxonomyBuckets: string[];\n /**\n * Cross-namespace query-budget limiter (issue #565 PR 4/5). When true,\n * a principal that issues a burst of recalls against namespaces other\n * than their own is throttled once its per-window count crosses\n * `recallCrossNamespaceBudgetHardLimit`. Default false — ships disabled.\n */\n recallCrossNamespaceBudgetEnabled: boolean;\n /** Rolling window in milliseconds over which cross-namespace reads are counted. */\n recallCrossNamespaceBudgetWindowMs: number;\n /**\n * Soft threshold — the first point at which the limiter flags a burst.\n * Calls are still allowed; anomaly detection (issue #565 PR 5) will\n * surface the warning.\n */\n recallCrossNamespaceBudgetSoftLimit: number;\n /** Hard threshold — calls past this count are denied in the window. */\n recallCrossNamespaceBudgetHardLimit: number;\n // Memory Worth recall filter (issue #560 PR 4)\n /**\n * When true, recall multiplies candidate scores by the Memory Worth\n * factor computed from `mw_success` / `mw_fail` counters on each\n * memory's frontmatter (see `computeMemoryWorth`). Memories with a\n * history of failed sessions sink; neutral / uninstrumented memories\n * are untouched (multiplier 1.0). Default false — flip to true in PR 5\n * once the benchmark shows precision tie-or-win.\n */\n recallMemoryWorthFilterEnabled: boolean;\n /**\n * Recall-audit anomaly detector (issue #565 PR 5/5). When true,\n * access surfaces run the anomaly detector over a tail of the audit\n * trail after each recall and surface any flags via logs / metrics.\n * Ships disabled.\n */\n recallAuditAnomalyDetectionEnabled: boolean;\n /** Rolling window over which audit entries are analyzed. */\n recallAuditAnomalyWindowMs: number;\n /** Threshold for the `repeat-query` flag. */\n recallAuditAnomalyRepeatQueryLimit: number;\n /** Threshold for the `namespace-walk` flag (distinct namespaces). */\n recallAuditAnomalyNamespaceWalkLimit: number;\n /** Threshold for the `high-cardinality-return` flag. */\n recallAuditAnomalyHighCardinalityLimit: number;\n /** Threshold for the `rapid-fire` flag. */\n recallAuditAnomalyRapidFireLimit: number;\n /**\n * Optional half-life for Memory Worth decay, in milliseconds. When\n * positive, older outcome observations are exponentially decayed toward\n * the uniform prior. Set to 0 (default) to disable decay and use raw\n * counter values.\n */\n recallMemoryWorthHalfLifeMs: number;\n // Memory Linking (Phase 3A)\n memoryLinkingEnabled: boolean;\n // Conversation Threading (Phase 3B)\n threadingEnabled: boolean;\n threadingGapMinutes: number;\n // Memory Summarization (Phase 4A)\n summarizationEnabled: boolean;\n summarizationTriggerCount: number;\n summarizationRecentToKeep: number;\n summarizationImportanceThreshold: number;\n summarizationProtectedTags: string[];\n // Topic Extraction (Phase 4B)\n topicExtractionEnabled: boolean;\n topicExtractionTopN: number;\n // Transcript & Context Preservation (v2.0)\n // Transcript archive\n transcriptEnabled: boolean;\n transcriptRetentionDays: number;\n /** Channel types to skip from transcript logging (e.g., [\"cron\"]) */\n transcriptSkipChannelTypes: string[];\n // Transcript injection\n transcriptRecallHours: number;\n maxTranscriptTurns: number;\n maxTranscriptTokens: number;\n // Checkpoint\n checkpointEnabled: boolean;\n checkpointTurns: number;\n // Compaction reset: trigger session reset after compaction instead of continuing degraded.\n // Requires OC fork with PR #29985 (api.resetSession).\n compactionResetEnabled: boolean;\n beforeResetTimeoutMs: number;\n initGateTimeoutMs: number;\n flushOnResetEnabled: boolean;\n commandsListEnabled: boolean;\n openclawToolsEnabled: boolean;\n openclawToolSnippetMaxChars: number;\n sessionTogglesEnabled: boolean;\n verboseRecallVisibility: boolean;\n recallTranscriptsEnabled: boolean;\n recallTranscriptRetentionDays: number;\n respectBundledActiveMemoryToggle: boolean;\n activeRecallEnabled: boolean;\n activeRecallAgents: string[] | null;\n activeRecallAllowedChatTypes: ActiveRecallChatType[];\n activeRecallQueryMode: ActiveRecallQueryMode;\n activeRecallPromptStyle: ActiveRecallPromptStyle;\n activeRecallPromptOverride: string | null;\n activeRecallPromptAppend: string | null;\n activeRecallMaxSummaryChars: number;\n activeRecallRecentUserTurns: number;\n activeRecallRecentAssistantTurns: number;\n activeRecallRecentUserChars: number;\n activeRecallRecentAssistantChars: number;\n activeRecallThinking: ActiveRecallThinking;\n activeRecallTimeoutMs: number;\n activeRecallCacheTtlMs: number;\n activeRecallModel: string | null;\n activeRecallModelFallbackPolicy: ActiveRecallModelFallbackPolicy;\n activeRecallPersistTranscripts: boolean;\n activeRecallTranscriptDir: string;\n activeRecallEntityGraphDepth: number;\n activeRecallIncludeCausalTrajectories: boolean;\n activeRecallIncludeDaySummary: boolean;\n activeRecallAttachRecallExplain: boolean;\n activeRecallAllowChainedActiveMemory: boolean;\n dreaming: DreamingConfig;\n /**\n * Unified dreams-phases config block (issue #678 PR 2/4).\n * Groups existing lifecycle, REM, and deep-sleep gates under one namespace.\n * Values here WIN over equivalent legacy top-level keys when set. See docs/dreams.md.\n */\n dreamsPhases: DreamsPhasesConfig;\n procedural: ProceduralConfig;\n /**\n * At-rest encryption configuration (issue #690 PR 3/4).\n *\n * When `secureStoreEnabled` is true, `StorageManager` reads and\n * writes memory files through the `secure-fs` encryption layer.\n * The store must be unlocked via `remnic secure-store unlock` before\n * any recall or store operations will succeed.\n *\n * When `secureStoreEncryptOnWrite` is true (the default when enabled),\n * every new memory write is encrypted. Set to false to pause new\n * encryptions while still being able to decrypt existing files.\n */\n secureStoreEnabled: boolean;\n /** Encrypt new writes when the secure-store is unlocked. Default true. */\n secureStoreEncryptOnWrite: boolean;\n // Coding-agent project/branch scoping (issue #569)\n codingMode: CodingModeConfig;\n heartbeat: HeartbeatConfig;\n slotBehavior: SlotBehaviorConfig;\n codexCompat: CodexCompatConfig;\n /**\n * When true (default), the extraction prompt instructs the LLM to classify\n * each fact as `\"project\"` or `\"global\"` scope. Global-scoped facts are\n * promoted to the shared namespace so they are visible across all projects.\n * When false, all facts go to whatever namespace the session is in (pre-\n * scope-classification behavior). Rule 30: configuration gate.\n */\n extractionScopeClassificationEnabled: boolean;\n // Extraction judge (issue #376)\n /** Enable the LLM-as-judge fact-worthiness gate on extracted facts. Default false (opt-in). */\n extractionJudgeEnabled: boolean;\n /** Model override for the judge LLM. Empty string means use the local model. */\n extractionJudgeModel: string;\n /** Maximum number of candidate facts per judge LLM batch call. */\n extractionJudgeBatchSize: number;\n /** Shadow mode: log judge verdicts but do not filter facts. Default false. */\n extractionJudgeShadow: boolean;\n /**\n * Maximum number of times the same candidate text may be deferred before\n * the judge forcibly converts the verdict to `\"reject\"`. Prevents\n * pathological LLM responses from looping forever on ambiguous facts.\n * Defaults to 2 (issue #562, PR 2).\n */\n extractionJudgeMaxDeferrals: number;\n /**\n * Emit structured telemetry rows to\n * `state/observation-ledger/extraction-judge-verdicts.jsonl` on every\n * judge verdict. Off by default; enable to collect defer-rate / latency\n * metrics for operator dashboards (issue #562, PR 3).\n */\n extractionJudgeTelemetryEnabled: boolean;\n /**\n * Collect `(candidate_text, verdict_kind, reason)` tuples into\n * `~/.remnic/judge-training/<date>.jsonl` for use by a future GRPO\n * training pipeline (issue #562, PR 4). Off by default. Rows live in\n * the user's home directory rather than the shared memory directory so\n * they are not committed, sync'd, or bundled into memory exports.\n */\n collectJudgeTrainingPairs: boolean;\n /**\n * Override directory for judge training-pair collection. Empty string\n * means use the default (`~/.remnic/judge-training`). Primarily for\n * tests and for operators who want the output to land in a specific\n * location.\n */\n judgeTrainingDir: string;\n // Hourly summaries\n hourlySummariesEnabled: boolean;\n daySummaryEnabled: boolean;\n /** If true, Engram may attempt to auto-register an hourly summary cron job (default off). */\n hourlySummaryCronAutoRegister: boolean;\n /** If true, Engram may attempt to auto-register the nightly governance cron job (default off). */\n nightlyGovernanceCronAutoRegister: boolean;\n summaryRecallHours: number;\n maxSummaryCount: number;\n summaryModel: string;\n // v2.4 Extended hourly summaries\n hourlySummariesExtendedEnabled: boolean;\n hourlySummariesIncludeToolStats: boolean;\n hourlySummariesIncludeSystemMessages: boolean;\n hourlySummariesMaxTurnsPerRun: number;\n // v2.4 Conversation index (optional)\n conversationIndexEnabled: boolean;\n conversationIndexBackend: \"qmd\" | \"faiss\";\n conversationIndexQmdCollection: string;\n conversationIndexRetentionDays: number;\n conversationIndexMinUpdateIntervalMs: number;\n conversationIndexEmbedOnUpdate: boolean;\n conversationIndexFaissScriptPath?: string;\n conversationIndexFaissPythonBin?: string;\n conversationIndexFaissModelId: string;\n conversationIndexFaissIndexDir: string;\n conversationIndexFaissUpsertTimeoutMs: number;\n conversationIndexFaissSearchTimeoutMs: number;\n conversationIndexFaissHealthTimeoutMs: number;\n conversationIndexFaissMaxBatchSize: number;\n conversationIndexFaissMaxSearchK: number;\n conversationRecallTopK: number;\n conversationRecallMaxChars: number;\n conversationRecallTimeoutMs: number;\n // Evaluation harness foundation\n evalHarnessEnabled: boolean;\n evalShadowModeEnabled: boolean;\n benchmarkBaselineSnapshotsEnabled: boolean;\n benchmarkDeltaReporterEnabled: boolean;\n benchmarkStoredBaselineEnabled: boolean;\n evalStoreDir: string;\n // Objective-state memory foundation\n objectiveStateMemoryEnabled: boolean;\n objectiveStateSnapshotWritesEnabled: boolean;\n objectiveStateRecallEnabled: boolean;\n objectiveStateStoreDir: string;\n // Causal trajectory memory foundation\n causalTrajectoryMemoryEnabled: boolean;\n causalTrajectoryStoreDir: string;\n causalTrajectoryRecallEnabled: boolean;\n actionGraphRecallEnabled: boolean;\n // Trust-zone memory foundation\n trustZonesEnabled: boolean;\n quarantinePromotionEnabled: boolean;\n trustZoneStoreDir: string;\n trustZoneRecallEnabled: boolean;\n memoryPoisoningDefenseEnabled: boolean;\n memoryRedTeamBenchEnabled: boolean;\n // Harmonic retrieval foundation\n harmonicRetrievalEnabled: boolean;\n abstractionAnchorsEnabled: boolean;\n abstractionNodeStoreDir: string;\n // Episodic/semantic split foundation\n verifiedRecallEnabled: boolean;\n semanticRulePromotionEnabled: boolean;\n semanticRuleVerificationEnabled: boolean;\n semanticConsolidationEnabled: boolean;\n semanticConsolidationModel: string;\n semanticConsolidationThreshold: number;\n semanticConsolidationMinClusterSize: number;\n semanticConsolidationExcludeCategories: string[];\n semanticConsolidationIntervalHours: number;\n semanticConsolidationMaxPerRun: number;\n /**\n * When true (default), semantic-consolidation prompts the LLM with an\n * operator-aware format asking for JSON `{operator, output}` and records\n * the resulting SPLIT/MERGE/UPDATE operator on `derived_via`. When\n * false, falls back to the legacy plain-text prompt — `derived_via` is\n * still populated via the cluster-shape heuristic in\n * `chooseConsolidationOperator`. Issue #561 PR 3.\n */\n operatorAwareConsolidationEnabled: boolean;\n // Pattern reinforcement (issue #687 PR 2/4)\n /**\n * When true, the pattern-reinforcement maintenance job runs on its\n * configured cadence and clusters duplicate non-procedural memories\n * by normalized content. Clusters with `>= patternReinforcementMinCount`\n * members produce a canonical (most-recent) memory tagged with\n * `reinforcement_count` + `last_reinforced_at`; the older duplicates\n * are marked `superseded` and pointed at the canonical. Default\n * `false` — opt-in until bench validation lands.\n */\n patternReinforcementEnabled: boolean;\n /**\n * Minimum interval (ms) between pattern-reinforcement runs. Default\n * `7 * 24 * 60 * 60 * 1000` (7 days). Set to `0` to disable cadence\n * gating (useful for tests / manual invocation).\n */\n patternReinforcementCadenceMs: number;\n /**\n * Minimum cluster size before pattern reinforcement promotes a\n * canonical and supersedes duplicates. Default `3`. Clamped to\n * `>= 2` at config-parse time — a \"cluster of 1\" is just a single\n * memory and a \"cluster of 0\" is meaningless.\n */\n patternReinforcementMinCount: number;\n /**\n * Memory categories the pattern-reinforcement job considers.\n * Default `[\"preference\", \"fact\", \"decision\"]`. The job\n * intentionally skips procedural memories so it stays disjoint from\n * the procedural mining pipeline.\n */\n patternReinforcementCategories: string[];\n /** issue #687 PR 3/4: opt-in recall score boost for reinforced memories. Default false. */\n reinforcementRecallBoostEnabled: boolean;\n /** Score bonus per unit of reinforcement_count. Range [0, 1]. Default 0.05. */\n reinforcementRecallBoostWeight: number;\n /** Maximum additive reinforcement boost per result. Range [0, 1]. Default 0.3. */\n reinforcementRecallBoostMax: number;\n /**\n * Async peer profile reasoner — issue #679 PR 2/5.\n *\n * Default `false` (opt-in). When enabled, the reasoner runs after\n * `runSemanticConsolidation` (the REM phase of the dreams pipeline)\n * and updates per-peer profile.md files with provenance-tagged\n * field updates derived from the peer's interaction log.\n */\n peerProfileReasonerEnabled: boolean;\n /**\n * Model identifier used by the peer profile reasoner. Logged for\n * telemetry only — actual dispatch is via the same FallbackLlmClient\n * the orchestrator uses for semantic consolidation. Default `gpt-5.2`.\n */\n peerProfileReasonerModel: string;\n /**\n * Minimum new interaction-log entries a peer must accumulate since\n * the previous reasoner run before being processed again. Default 5.\n * Setting to 0 forces every run to consider every peer.\n */\n peerProfileReasonerMinInteractions: number;\n /**\n * Hard cap on the total number of profile fields the reasoner will\n * apply across all peers in a single run. Default 8.\n */\n peerProfileReasonerMaxFieldsPerRun: number;\n /**\n * When true, inject the active peer's profile fields into the recall\n * context as a \"## Peer Profile\" section. Default false (opt-in,\n * Gotcha #30/#48 — least-privileged default). Requires the session's\n * peer ID to be registered via `setPeerIdForSession` before recall.\n */\n peerProfileRecallEnabled: boolean;\n /**\n * Maximum number of peer profile fields to inject per recall. Only\n * the most-recently-updated N fields are included to keep the context\n * budget predictable. Default 5. Setting to 0 disables field\n * injection even when `peerProfileRecallEnabled` is true.\n */\n peerProfileRecallMaxFields: number;\n // Creation-memory foundation\n creationMemoryEnabled: boolean;\n memoryUtilityLearningEnabled: boolean;\n promotionByOutcomeEnabled: boolean;\n commitmentLedgerEnabled: boolean;\n commitmentLifecycleEnabled: boolean;\n commitmentStaleDays: number;\n commitmentLedgerDir: string;\n resumeBundlesEnabled: boolean;\n resumeBundleDir: string;\n workProductRecallEnabled: boolean;\n workProductLedgerDir: string;\n workTasksEnabled: boolean;\n workProjectsEnabled: boolean;\n workTasksDir: string;\n workProjectsDir: string;\n workIndexEnabled: boolean;\n workIndexDir: string;\n workTaskIndexEnabled: boolean;\n workProjectIndexEnabled: boolean;\n workIndexAutoRebuildEnabled: boolean;\n workIndexAutoRebuildDebounceMs: number;\n // Local LLM Provider (v2.1)\n localLlmEnabled: boolean;\n localLlmUrl: string;\n localLlmModel: string;\n /** Optional API key for authenticated OpenAI-compatible endpoints. */\n localLlmApiKey?: string;\n /** Additional headers for local/compatible endpoint requests. */\n localLlmHeaders?: Record<string, string>;\n /** If false, do not send Authorization header even when localLlmApiKey is set. */\n localLlmAuthHeader: boolean;\n localLlmFallback: boolean;\n /** Optional home directory override for local LLM helpers (LM Studio settings, CLI PATH). */\n localLlmHomeDir?: string;\n /** Optional absolute path to LMS CLI binary (preferred over auto-detection). */\n localLmsCliPath?: string;\n /** Optional bin directory prepended to PATH for LMS CLI execution. */\n localLmsBinDir?: string;\n /** Hard timeout for local LLM requests (ms). */\n localLlmTimeoutMs: number;\n /** Max context window for local LLM (override auto-detection). Set lower if your LLM server defaults to smaller contexts. */\n localLlmMaxContext?: number;\n // Observability\n /** If true, log slow operations (local LLM + related I/O) with durations and metadata (no content). */\n slowLogEnabled: boolean;\n /**\n * If true, include the full recalled memory text in `RecallTraceEvent.recalledContent`.\n * Disabled by default — enable only when you want external trace subscribers (e.g. Langfuse)\n * to see the exact memory context injected into each conversation turn.\n * This adds payload to trace events but does not log to files or the gateway log.\n */\n traceRecallContent: boolean;\n /** Threshold for slow operation logging (ms). */\n slowLogThresholdMs: number;\n // Performance profiling (opt-in)\n /** If true, collect and persist timing traces for recall and extraction pipelines. */\n profilingEnabled: boolean;\n /** Directory for profiling trace JSONL files. Defaults to <memoryDir>/profiling. */\n profilingStorageDir: string;\n /** Maximum number of trace files to keep (rolling window). */\n profilingMaxTraces: number;\n // Extraction stability guards (P0/P1)\n extractionDedupeEnabled: boolean;\n extractionDedupeWindowMs: number;\n extractionMinChars: number;\n extractionMinUserTurns: number;\n extractionMaxTurnChars: number;\n extractionMaxFactsPerRun: number;\n extractionMaxEntitiesPerRun: number;\n extractionMaxQuestionsPerRun: number;\n extractionMaxProfileUpdatesPerRun: number;\n /**\n * Minimum importance level required to persist an extracted fact. Facts\n * whose locally-scored level falls below this threshold are dropped before\n * write and counted toward the `importance_gated` metric. Defaults to\n * \"low\" so trivial content (greetings, single-word replies, filler) is\n * silently dropped while everything else still passes.\n */\n extractionMinImportanceLevel: ImportanceLevel;\n /**\n * Inline source attribution (issue #369).\n * When enabled, extracted facts carry a compact provenance tag (agent,\n * session, timestamp) inlined into the fact text — not just in YAML\n * frontmatter — so the citation survives prompt injection, copy/paste,\n * and LLM quoting. Off by default to preserve backwards compatibility\n * with existing downstream consumers that expect raw fact text.\n */\n inlineSourceAttributionEnabled: boolean;\n /**\n * Template used when injecting inline citations. Supported placeholders:\n * `{agent}`, `{session}`, `{sessionId}`, `{ts}`, `{date}`. Defaults to\n * `[Source: agent={agent}, session={sessionId}, ts={ts}]`.\n */\n inlineSourceAttributionFormat: string;\n consolidationRequireNonZeroExtraction: boolean;\n consolidationMinIntervalMs: number;\n // QMD maintenance (debounced singleflight)\n qmdMaintenanceEnabled: boolean;\n qmdMaintenanceDebounceMs: number;\n qmdAutoEmbedEnabled: boolean;\n qmdEmbedMinIntervalMs: number;\n qmdUpdateTimeoutMs: number;\n qmdUpdateMinIntervalMs: number;\n // Local LLM resilience\n localLlmRetry5xxCount: number;\n localLlmRetryBackoffMs: number;\n localLlm400TripThreshold: number;\n localLlm400CooldownMs: number;\n // Local LLM fast tier (v9.1) — smaller model for quick ops\n localLlmFastEnabled: boolean;\n localLlmFastModel: string;\n localLlmFastUrl: string;\n localLlmFastTimeoutMs: number;\n /**\n * Suppress chain-of-thought / thinking mode on the main local LLM\n * (issue #548). When true, Remnic injects\n * `chat_template_kwargs: { enable_thinking: false }` on every\n * request so thinking-capable models (Qwen 3.5, Gemma 4, DeepSeek,\n * etc.) skip reasoning tokens that structured-output tasks like\n * extraction and consolidation cannot benefit from. Default: true\n * — the dominant localLlm use case is JSON-shaped extraction where\n * thinking is pure latency tax and a common cause of 60s timeouts.\n * Set to false to restore thinking for narrative tasks.\n *\n * The fast-tier client (`fastLlm`) always disables thinking; that\n * contract is baked into \"fast tier\" and is unaffected by this flag.\n */\n localLlmDisableThinking: boolean;\n // Gateway config for fallback AI\n gatewayConfig?: GatewayConfig;\n // Gateway model source (v9.2) — route LLM calls through gateway agent model chain\n modelSource: \"plugin\" | \"gateway\";\n gatewayAgentId: string;\n fastGatewayAgentId: string;\n\n // v3.0 Multi-agent memory (namespaces)\n namespacesEnabled: boolean;\n defaultNamespace: string;\n sharedNamespace: string;\n principalFromSessionKeyMode: PrincipalFromSessionKeyMode;\n principalFromSessionKeyRules: PrincipalRule[];\n namespacePolicies: NamespacePolicy[];\n defaultRecallNamespaces: Array<\"self\" | \"shared\">;\n cronRecallMode: CronRecallMode;\n cronRecallAllowlist: string[];\n cronRecallPolicyEnabled: boolean;\n cronRecallNormalizedQueryMaxChars: number;\n cronRecallInstructionHeavyTokenCap: number;\n cronConversationRecallMode: CronConversationRecallMode;\n autoPromoteToSharedEnabled: boolean;\n autoPromoteToSharedCategories: Array<\"fact\" | \"correction\" | \"decision\" | \"preference\">;\n autoPromoteMinConfidenceTier: ConfidenceTier;\n routingRulesEnabled: boolean;\n routingRulesStateFile: string;\n\n // v4.0 Shared-context (cross-agent shared intelligence)\n sharedContextEnabled: boolean;\n sharedContextDir?: string;\n sharedContextMaxInjectChars: number;\n crossSignalsSemanticEnabled: boolean;\n crossSignalsSemanticTimeoutMs: number;\n sharedCrossSignalSemanticEnabled?: boolean;\n sharedCrossSignalSemanticTimeoutMs?: number;\n sharedCrossSignalSemanticMaxCandidates?: number;\n\n // v5.0 Compounding engine\n compoundingEnabled: boolean;\n compoundingWeeklyCronEnabled: boolean;\n compoundingSemanticEnabled: boolean;\n compoundingSynthesisTimeoutMs: number;\n compoundingInjectEnabled: boolean;\n\n // IRC (Inductive Rule Consolidation) — preference synthesis\n ircEnabled: boolean;\n ircMaxPreferences: number;\n ircIncludeCorrections: boolean;\n ircMinConfidence: number;\n\n // CMC (Causal Memory Consolidation) — cross-session causal reasoning\n cmcEnabled: boolean;\n cmcStitchLookbackDays: number;\n cmcStitchMinScore: number;\n cmcStitchMaxEdgesPerTrajectory: number;\n cmcConsolidationEnabled: boolean;\n cmcConsolidationMinRecurrence: number;\n cmcConsolidationMinSessions: number;\n cmcConsolidationSuccessThreshold: number;\n cmcRetrievalEnabled: boolean;\n cmcRetrievalMaxDepth: number;\n cmcRetrievalMaxChars: number;\n cmcRetrievalCounterfactualBoost: number;\n cmcBehaviorLearningEnabled: boolean;\n cmcBehaviorMinFrequency: number;\n cmcBehaviorMinSessions: number;\n cmcBehaviorConfidenceThreshold: number;\n cmcLifecycleCausalImpactWeight: number;\n\n // PEDC (Prediction-Error-Driven Calibration) — model-user alignment\n calibrationEnabled: boolean;\n calibrationMaxRulesPerRecall: number;\n calibrationMaxChars: number;\n\n // Search backend abstraction\n searchBackend?: \"qmd\" | \"remote\" | \"noop\" | \"lancedb\" | \"meilisearch\" | \"orama\";\n remoteSearchBaseUrl?: string;\n remoteSearchApiKey?: string;\n remoteSearchTimeoutMs?: number;\n\n // LanceDB backend\n lancedbEnabled: boolean;\n lanceDbPath?: string;\n lanceEmbeddingDimension?: number;\n\n // Meilisearch backend\n meilisearchEnabled: boolean;\n meilisearchHost?: string;\n meilisearchApiKey?: string;\n meilisearchTimeoutMs?: number;\n meilisearchAutoIndex?: boolean;\n\n // Orama backend\n oramaEnabled: boolean;\n oramaDbPath?: string;\n oramaEmbeddingDimension?: number;\n\n // QMD daemon mode\n qmdDaemonEnabled: boolean;\n qmdDaemonUrl: string;\n qmdDaemonRecheckIntervalMs: number;\n qmdIntentHintsEnabled: boolean;\n qmdExplainEnabled: boolean;\n\n // v7.0 Knowledge Graph Enhancement\n knowledgeIndexEnabled: boolean;\n knowledgeIndexMaxEntities: number;\n knowledgeIndexMaxChars: number;\n entityRetrievalEnabled: boolean;\n entityRetrievalMaxChars: number;\n entityRetrievalMaxHints: number;\n entityRetrievalMaxSupportingFacts: number;\n entityRetrievalMaxRelatedEntities: number;\n entityRetrievalRecentTurns: number;\n entitySchemas?: Record<string, EntitySchemaDefinition>;\n // Recall assembly controls\n recallBudgetChars: number;\n recallOuterTimeoutMs: number;\n recallCoreDeadlineMs: number;\n recallEnrichmentDeadlineMs: number;\n recallPipeline: RecallSectionConfig[];\n /** Apply Maximal Marginal Relevance to the final recall selection per-section. */\n recallMmrEnabled: boolean;\n /** MMR λ parameter. 1.0 = pure relevance, 0.0 = pure diversity. Default 0.7. */\n recallMmrLambda: number;\n /** MMR is applied over the top N candidates per section. Default 40. */\n recallMmrTopN: number;\n /**\n * Boost stored `reasoning_trace` memories in recall results when the\n * incoming query reads like a problem-solving ask (e.g. \"how do I…\",\n * \"step by step\", \"walk me through…\"). Default false — opt in after\n * benchmarking (issue #564 PR 3).\n */\n recallReasoningTraceBoostEnabled: boolean;\n qmdRecallCacheTtlMs: number;\n qmdRecallCacheStaleTtlMs: number;\n qmdRecallCacheMaxEntries: number;\n entityRelationshipsEnabled: boolean;\n entityActivityLogEnabled: boolean;\n entityActivityLogMaxEntries: number;\n entityAliasesEnabled: boolean;\n entitySummaryEnabled: boolean;\n entitySynthesisMaxTokens: number;\n\n // v6.0 Fact deduplication & archival\n /** Enable content-hash deduplication to prevent storing semantically identical facts. */\n factDeduplicationEnabled: boolean;\n /**\n * Issue #373 — Write-time semantic similarity guard. When enabled (default),\n * the orchestrator embeds each candidate fact and queries the existing\n * embedding index for its top-K nearest neighbors. If the best cosine\n * similarity is at or above `semanticDedupThreshold`, the fact is dropped\n * as a near-duplicate. Fails open (keeps the fact) if the embedding backend\n * is unavailable.\n */\n semanticDedupEnabled: boolean;\n /** Cosine similarity threshold in [0, 1] above which a candidate fact is skipped. */\n semanticDedupThreshold: number;\n /** Number of nearest-neighbor candidates to consider during semantic dedup. */\n semanticDedupCandidates: number;\n /** Enable automatic archival of old, low-importance, rarely-accessed facts. */\n factArchivalEnabled: boolean;\n /** Minimum age in days before a fact is eligible for archival. */\n factArchivalAgeDays: number;\n /** Maximum importance score for archival eligibility (0-1). Only facts below this are archived. */\n factArchivalMaxImportance: number;\n /** Maximum access count for archival eligibility. Only rarely-accessed facts are archived. */\n factArchivalMaxAccessCount: number;\n /** Tags that protect a fact from archival regardless of other criteria. */\n factArchivalProtectedCategories: string[];\n // v8.3 Lifecycle policy engine\n lifecyclePolicyEnabled: boolean;\n lifecycleFilterStaleEnabled: boolean;\n lifecyclePromoteHeatThreshold: number;\n lifecycleStaleDecayThreshold: number;\n lifecycleArchiveDecayThreshold: number;\n lifecycleProtectedCategories: MemoryCategory[];\n lifecycleMetricsEnabled: boolean;\n // v8.3 proactive + policy learning\n proactiveExtractionEnabled: boolean;\n contextCompressionActionsEnabled: boolean;\n compressionGuidelineLearningEnabled: boolean;\n compressionGuidelineSemanticRefinementEnabled: boolean;\n compressionGuidelineSemanticTimeoutMs: number;\n maxProactiveQuestionsPerExtraction: number;\n proactiveExtractionTimeoutMs: number;\n proactiveExtractionMaxTokens: number;\n extractionMaxOutputTokens: number;\n proactiveExtractionCategoryAllowlist?: MemoryCategory[];\n maxCompressionTokensPerHour: number;\n behaviorLoopAutoTuneEnabled: boolean;\n behaviorLoopLearningWindowDays: number;\n behaviorLoopMinSignalCount: number;\n behaviorLoopMaxDeltaPerCycle: number;\n behaviorLoopProtectedParams: string[];\n // v8.0 Phase 1: recall planner + intent routing + verbatim artifacts\n recallPlannerEnabled: boolean;\n recallPlannerModel: string;\n recallPlannerTimeoutMs: number;\n recallPlannerUseResponsesApi: boolean;\n recallPlannerMaxPromptChars: number;\n recallPlannerMaxMemoryHints: number;\n recallPlannerShadowMode: boolean;\n recallPlannerTelemetryEnabled: boolean;\n recallPlannerMaxQmdResultsMinimal: number;\n recallPlannerMaxQmdResultsFull: number;\n intentRoutingEnabled: boolean;\n intentRoutingBoost: number;\n verbatimArtifactsEnabled: boolean;\n verbatimArtifactsMinConfidence: number;\n verbatimArtifactsMaxRecall: number;\n verbatimArtifactCategories: MemoryCategory[];\n // v8.0 Phase 2A: Memory Boxes + Trace Weaving\n memoryBoxesEnabled: boolean;\n /** Jaccard overlap threshold below which a topic shift triggers box sealing (0-1, default 0.35) */\n boxTopicShiftThreshold: number;\n /** Time gap in ms before an open box is sealed (default 30 min) */\n boxTimeGapMs: number;\n /** Max memories per box before forced seal */\n boxMaxMemories: number;\n traceWeaverEnabled: boolean;\n /** Days back to search for trace links */\n traceWeaverLookbackDays: number;\n /** Minimum Jaccard overlap to assign the same traceId (0-1, default 0.4) */\n traceWeaverOverlapThreshold: number;\n /** Number of recent days of boxes to inject during recall */\n boxRecallDays: number;\n // v8.0 Phase 2B: Episode/Note dual store (HiMem)\n /** Classify extracted memories as episode or note and tag with memoryKind */\n episodeNoteModeEnabled: boolean;\n // v8.1 Temporal + Tag Indexes (SwiftMem-inspired)\n /** Build and maintain temporal (state/index_time.json) and tag (state/index_tags.json) indexes */\n queryAwareIndexingEnabled: boolean;\n /** Max candidate paths returned from index prefilter (0 = no cap) */\n queryAwareIndexingMaxCandidates: number;\n temporalIndexWindowDays: number;\n temporalIndexMaxEntries: number;\n temporalBoostRecentDays: number;\n temporalBoostScore: number;\n temporalDecayEnabled: boolean;\n tagMemoryEnabled: boolean;\n tagMaxPerMemory: number;\n tagIndexMaxEntries: number;\n tagRecallBoost: number;\n tagRecallMaxMatches: number;\n // v8.2 multi-graph memory (PR 18)\n multiGraphMemoryEnabled: boolean;\n // v8.2 PR 19A: graph recall planner gating\n graphRecallEnabled: boolean;\n graphRecallMaxExpansions: number;\n graphRecallMaxPerSeed: number;\n graphRecallMinEdgeWeight: number;\n graphRecallShadowEnabled: boolean;\n graphRecallSnapshotEnabled: boolean;\n graphRecallShadowSampleRate: number;\n graphRecallExplainToolEnabled: boolean;\n graphRecallStoreColdMirror: boolean;\n graphRecallColdMirrorCollection?: string;\n graphRecallColdMirrorMinAgeDays: number;\n graphRecallUseEntityPriors: boolean;\n graphRecallEntityPriorBoost: number;\n graphRecallPreferHubSeeds: boolean;\n graphRecallHubBias: number;\n graphRecallRecencyHalfLifeDays: number;\n graphRecallDampingFactor: number;\n graphRecallMaxSeedNodes: number;\n graphRecallMaxExpandedNodes: number;\n graphRecallMaxTrailPerNode: number;\n graphRecallMinSeedScore: number;\n graphRecallExpansionScoreThreshold: number;\n graphRecallExplainMaxPaths: number;\n graphRecallExplainMaxChars: number;\n graphRecallExplainEdgeLimit: number;\n graphRecallExplainEnabled: boolean;\n graphRecallEntityHintsEnabled: boolean;\n graphRecallEntityHintMax: number;\n graphRecallEntityHintMaxChars: number;\n graphRecallSnapshotDir: string;\n graphRecallEnableTrace: boolean;\n graphRecallEnableDebug: boolean;\n /** Allow graph_mode escalation for broader causal/timeline phrasing beyond strict keywords. */\n graphExpandedIntentEnabled?: boolean;\n /** Run bounded graph expansion in full mode when enough recall seeds exist. */\n graphAssistInFullModeEnabled?: boolean;\n /** In full mode, compute graph assist for telemetry/snapshotting but do not inject merged results. */\n graphAssistShadowEvalEnabled?: boolean;\n /** Minimum seed results required before full-mode graph assist runs. */\n graphAssistMinSeedResults?: number;\n entityGraphEnabled: boolean;\n timeGraphEnabled: boolean;\n /** When true, write fallback temporal adjacency edges for consecutive extracted memories. */\n graphWriteSessionAdjacencyEnabled?: boolean;\n causalGraphEnabled: boolean;\n maxGraphTraversalSteps: number;\n graphActivationDecay: number;\n /** Weight of graph activation score when blending with seed QMD score (0-1). */\n graphExpansionActivationWeight: number;\n /** Lower bound for blended graph-expanded recall scores (0-1). */\n graphExpansionBlendMin: number;\n /** Upper bound for blended graph-expanded recall scores (0-1). */\n graphExpansionBlendMax: number;\n maxEntityGraphEdgesPerMemory: number;\n /** SimpleMem-inspired de-linearization: resolve pronouns and anchor relative dates after extraction. */\n delinearizeEnabled: boolean;\n /** Synapse-inspired confidence gate — skip memory injection when top score is below threshold. */\n recallConfidenceGateEnabled: boolean;\n recallConfidenceGateThreshold: number;\n /** PlugMem-inspired causal rule extraction: mine IF→THEN rules during consolidation. */\n causalRuleExtractionEnabled: boolean;\n /** E-Mem-inspired memory reconstruction: targeted retrieval for missing entity context. */\n memoryReconstructionEnabled: boolean;\n /** Maximum number of entity expansions per recall. */\n memoryReconstructionMaxExpansions: number;\n /** Synapse-inspired lateral inhibition to suppress hub-node dominance. */\n graphLateralInhibitionEnabled: boolean;\n /** Inhibition strength (default 0.15). Higher = more suppression. */\n graphLateralInhibitionBeta: number;\n /** Number of top competing nodes considered for inhibition (default 7). */\n graphLateralInhibitionTopM: number;\n\n // Issue #681 PR 2/3 — graph-edge confidence decay maintenance.\n /** Enable the periodic graph-edge confidence decay job. Default false (opt-in). */\n graphEdgeDecayEnabled: boolean;\n /** Cadence in milliseconds at which the cron triggers the decay job. Default 7d. */\n graphEdgeDecayCadenceMs: number;\n /** Decay window passed through to `decayEdgeConfidence`. Default 90 days. */\n graphEdgeDecayWindowMs: number;\n /** Per-window confidence drop. Default 0.1. */\n graphEdgeDecayPerWindow: number;\n /** Floor confidence will not decay below. Default 0.1. */\n graphEdgeDecayFloor: number;\n /** Confidence threshold for the \"below visibility\" telemetry counter. Default 0.2. */\n graphEdgeDecayVisibilityThreshold: number;\n\n /**\n * Issue #681 PR 3/3 — minimum edge confidence required for an edge to be\n * traversed during spreading activation. Edges with `confidence` below this\n * floor are pruned and contribute neither activation nor downstream\n * neighbors. Legacy edges without `confidence` are treated as 1.0 so they\n * always pass the floor. Range `[0, 1]`; default `0.2`.\n */\n graphTraversalConfidenceFloor: number;\n /**\n * Issue #681 PR 3/3 — number of PageRank-style refinement iterations applied\n * on top of the BFS spreading-activation scores. Each iteration redistributes\n * a node's confidence-weighted activation along its outgoing edges. Set to 0\n * to disable refinement and use raw BFS scores. Default `8`.\n */\n graphTraversalPageRankIterations: number;\n // v8.2: Temporal Memory Tree\n temporalMemoryTreeEnabled: boolean;\n tmtHourlyMinMemories: number;\n tmtSummaryMaxTokens: number;\n // Explicit cue recall\n /** Front-load exact stored evidence for query-visible cues like turns, dates, ids, files, and tools. */\n explicitCueRecallEnabled: boolean;\n /** Character budget for the explicit cue evidence section. */\n explicitCueRecallMaxChars: number;\n /** Maximum query-visible cues expanded per recall. */\n explicitCueRecallMaxReferences: number;\n // Lossless Context Management (LCM)\n lcmEnabled: boolean;\n lcmLeafBatchSize: number;\n lcmRollupFanIn: number;\n lcmFreshTailTurns: number;\n lcmMaxDepth: number;\n lcmRecallBudgetShare: number;\n lcmDeterministicMaxTokens: number;\n lcmArchiveRetentionDays: number;\n /** Opt-in structured message-part capture/recall sidecar for LCM. Default false. */\n messagePartsEnabled: boolean;\n /** Max structured file/tool matches injected into recall. */\n messagePartsRecallMaxResults: number;\n\n // v9.1 Parallel Specialized Retrieval (ASMR-inspired)\n /** Enable three-agent parallel retrieval (DirectFact + Contextual + Temporal). Default false. */\n parallelRetrievalEnabled: boolean;\n /** Per-agent source weights for score blending during merge. */\n parallelAgentWeights: { direct: number; contextual: number; temporal: number };\n /** Max results fetched per agent before merge. */\n parallelMaxResultsPerAgent: number;\n\n // Daily Context Briefing (Issue #370)\n /** Briefing configuration knobs — see BriefingConfig for field docs. */\n briefing: BriefingConfig;\n\n // Codex CLI connector settings (install-time)\n codex: CodexConnectorConfig;\n\n // Live connectors (issue #683). Concrete implementations live under\n // packages/remnic-core/src/connectors/live/. Each child block maps to one\n // connector. All defaults are off — operators opt in.\n connectors: LiveConnectorsConfig;\n\n // MECE Taxonomy (#366)\n /** Enable the MECE taxonomy knowledge directory. Default false. */\n taxonomyEnabled: boolean;\n /** Auto-regenerate RESOLVER.md when taxonomy changes. Default true. */\n taxonomyAutoGenResolver: boolean;\n\n // Codex CLI — native memory materialization (#378)\n /** Materialize Remnic memories into Codex's expected ~/.codex/memories/ layout. Default true. */\n codexMaterializeMemories: boolean;\n /** Namespace to materialize; \"auto\" derives from the connector context. Default \"auto\". */\n codexMaterializeNamespace: string;\n /** Max whitespace-tokenized size of memory_summary.md. Default 4500. */\n codexMaterializeMaxSummaryTokens: number;\n /** Max age in days for rollout_summaries/*.md before pruning. Default 30. */\n codexMaterializeRolloutRetentionDays: number;\n /** Run materialization after semantic/causal consolidation completes. Default true. */\n codexMaterializeOnConsolidation: boolean;\n /** Run materialization at Codex session-end hook. Default true. */\n codexMaterializeOnSessionEnd: boolean;\n /** Enable Codex marketplace integration. Default true. */\n codexMarketplaceEnabled: boolean;\n\n // Page-level versioning (issue #371)\n /** Enable page-level versioning with sidecar snapshots. Default false. */\n versioningEnabled: boolean;\n /** Maximum number of version snapshots to keep per page. Default 50. Set to 0 to disable pruning. */\n versioningMaxPerPage: number;\n /** Name of the sidecar directory inside memoryDir. Default \".versions\". */\n versioningSidecarDir: string;\n\n // Binary file lifecycle management (#367)\n /** Enable binary file lifecycle management (mirror, redirect, clean). Default: false. */\n binaryLifecycleEnabled: boolean;\n /** Grace period in days before a mirrored binary is eligible for local cleanup. Default: 7. */\n binaryLifecycleGracePeriodDays: number;\n /** Storage backend type: \"filesystem\" copies to a local dir, \"none\" is no-op. Default: \"none\". */\n binaryLifecycleBackendType: \"filesystem\" | \"s3\" | \"none\";\n /** Base path for the filesystem backend. Required when backendType is \"filesystem\". */\n binaryLifecycleBackendPath: string;\n\n // Codex citation parity (issue #379)\n /** Enable oai-mem-citation blocks in recall responses. Default false. */\n citationsEnabled: boolean;\n /** Auto-enable citations when the Codex adapter is detected. Default true. */\n citationsAutoDetect: boolean;\n\n // External enrichment pipeline (issue #365)\n /** Enable the external enrichment pipeline. Default false. */\n enrichmentEnabled: boolean;\n /** Automatically enrich new entities on creation. Default false. */\n enrichmentAutoOnCreate: boolean;\n /** Max candidates accepted per entity per enrichment run. Default 20. */\n enrichmentMaxCandidatesPerEntity: number;\n\n // Memory extensions discovery (#382)\n /** Whether third-party memory extensions are discovered and injected into consolidation. Default true. */\n memoryExtensionsEnabled: boolean;\n /**\n * Root directory for memory extensions. Empty string means derive from\n * memoryDir: go up to the Remnic home dir and append memory_extensions.\n */\n memoryExtensionsRoot: string;\n}\n\n/** Runtime configuration for the daily context briefing feature. */\nexport interface BriefingConfig {\n /** Whether `remnic briefing` CLI and MCP tool are enabled. */\n enabled: boolean;\n /** Default lookback window token (e.g. \"yesterday\", \"3d\", \"1w\", \"24h\"). */\n defaultWindow: string;\n /** Default output format for the CLI. */\n defaultFormat: \"markdown\" | \"json\";\n /** Maximum number of LLM-generated suggested follow-ups. */\n maxFollowups: number;\n /** Optional path to an ICS or JSON calendar file. null disables the section. */\n calendarSource: string | null;\n /** If true, CLI writes a dated briefing file by default. */\n saveByDefault: boolean;\n /** Override directory for saved briefings. null → $REMNIC_HOME/briefings/. */\n saveDir: string | null;\n /** Whether to call the Responses API for follow-up suggestions. */\n llmFollowups: boolean;\n}\n\n/** Parsed representation of a briefing lookback window. */\nexport type BriefingWindow = \"yesterday\" | \"today\" | string;\n\n/** Filter the briefing to a single entity / project / topic. */\nexport interface BriefingFocus {\n type: \"person\" | \"project\" | \"topic\";\n value: string;\n}\n\n/** Calendar event surfaced by a CalendarSource implementation. */\nexport interface CalendarEvent {\n /** Stable identifier for dedupe / linking. */\n id: string;\n /** Event title (short). */\n title: string;\n /** ISO 8601 start timestamp. */\n start: string;\n /** Optional ISO 8601 end timestamp. */\n end?: string;\n /** Optional freeform location. */\n location?: string;\n /** Optional short notes. */\n notes?: string;\n}\n\n/** Abstraction over any calendar backend. Concrete implementations: `FileCalendarSource`. */\nexport interface CalendarSource {\n /** Return events that fall on the given UTC date (YYYY-MM-DD). */\n eventsForDate(dateIso: string): Promise<CalendarEvent[]>;\n}\n\n/** A single \"active thread\" surfaced in a briefing. */\nexport interface BriefingActiveThread {\n id: string;\n title: string;\n updatedAt: string;\n reason: string;\n}\n\n/** A single \"recent entity\" entry. */\nexport interface BriefingRecentEntity {\n name: string;\n type: string;\n updatedAt: string;\n score: number;\n summary?: string;\n}\n\n/** A single unresolved commitment or open question. */\nexport interface BriefingOpenCommitment {\n id: string;\n kind: \"question\" | \"commitment\" | \"pending_memory\";\n text: string;\n source?: string;\n createdAt?: string;\n}\n\n/** An LLM-generated short follow-up suggestion. */\nexport interface BriefingFollowup {\n text: string;\n rationale?: string;\n}\n\n/** Structured sections of a briefing result. */\nexport interface BriefingSections {\n activeThreads: BriefingActiveThread[];\n recentEntities: BriefingRecentEntity[];\n openCommitments: BriefingOpenCommitment[];\n suggestedFollowups: BriefingFollowup[];\n /** Only populated when a calendar source is configured and returns events. */\n todayCalendar?: CalendarEvent[];\n}\n\n/** A calendar source failure recorded when a CalendarSource throws during briefing generation. */\nexport interface BriefingCalendarSourceError {\n /** Human-readable description of the source (e.g. file path or source name). */\n source: string;\n /** Stringified error message from the failed source. */\n error: string;\n}\n\n/** Result returned by `buildBriefing`. */\nexport interface BriefingResult {\n markdown: string;\n json: Record<string, unknown>;\n sections: BriefingSections;\n /** Reason why suggested follow-ups were omitted (e.g. missing API key, LLM error). */\n followupsUnavailableReason?: string;\n /** Effective lookback window (ISO date range) used for this briefing. */\n window: { from: string; to: string };\n /**\n * Calendar sources that failed during this briefing run.\n * Only present (non-empty) when at least one source threw.\n * Allows callers to distinguish \"no events today\" from \"source unavailable\".\n */\n calendarSourceErrors?: BriefingCalendarSourceError[];\n}\n\n/**\n * Settings for the Codex CLI connector. These are consumed by\n * `remnic connectors install codex-cli` to decide where the phase-2 memory\n * extension is dropped and whether to install it at all.\n */\nexport interface CodexConnectorConfig {\n /**\n * Whether to install the Remnic memory extension into\n * `<codex_home>/memories_extensions/remnic/` when the `codex-cli`\n * connector is installed. Default `true`. Set to `false` for users who\n * self-manage the Codex memory extensions folder.\n */\n installExtension: boolean;\n /**\n * Optional override for the Codex home directory. When `null`, the\n * connector reads `$CODEX_HOME` and falls back to `~/.codex`. Setting\n * this is useful for integration tests and non-default installs.\n */\n codexHome: string | null;\n}\n\n/**\n * Container for live-connector config blocks (issue #683 PR 2/N).\n *\n * Lives at `connectors.*` rather than the top level so future connectors\n * (Notion, Gmail, GitHub) can slot in without bloating `PluginConfig`.\n *\n * Every child block must default to `enabled: false` per CLAUDE.md gotcha\n * #30 (escape hatch by default) and gotcha #48 (least-privileged enum\n * defaults). Concrete connectors are also expected to short-circuit at\n * registration time when their credentials are not populated.\n */\nexport interface LiveConnectorsConfig {\n /** Google Drive live connector (issue #683 PR 2/N). */\n googleDrive: GoogleDriveLiveConnectorConfig;\n /** Notion live connector (issue #683 PR 3/N). */\n notion: NotionLiveConnectorConfig;\n /** Gmail live connector (issue #683 PR 4/6). */\n gmail: GmailLiveConnectorConfig;\n /** GitHub live connector (issue #683 PR 5/6). */\n github: GitHubLiveConnectorConfig;\n}\n\n/**\n * Operator-facing config for the Google Drive live connector. The connector\n * module itself defines a separate, *validated* `GoogleDriveConnectorConfig`\n * shape (frozen, post-validation). This interface is the pre-validation\n * shape that `parseConfig` round-trips through.\n *\n * `clientId` / `clientSecret` / `refreshToken` are stored as strings here so\n * the schema can ship in `openclaw.plugin.json` and operators can populate\n * them from a secret store (e.g. an env-substituted plist or systemd\n * EnvironmentFile). They MUST NEVER be committed to source. The repo-wide\n * privacy policy in CLAUDE.md applies.\n */\nexport interface GoogleDriveLiveConnectorConfig {\n /** Master gate. Default false — operators must opt in explicitly. */\n enabled: boolean;\n /** OAuth2 client id. Populate from a secret store; never commit. */\n clientId: string;\n /** OAuth2 client secret. Populate from a secret store; never commit. */\n clientSecret: string;\n /** OAuth2 refresh token. Populate from a secret store; never commit. */\n refreshToken: string;\n /** Poll interval in ms. Default 300000 (5 min); min 1000; max 86400000 (24h). */\n pollIntervalMs: number;\n /** Optional folder-id scope. Empty array = all accessible files. */\n folderIds: string[];\n}\n\n/**\n * Operator-facing config for the Notion live connector (issue #683 PR 3/N).\n * The connector module defines a separate validated `NotionConnectorConfig`\n * shape (frozen, post-validation). This interface is the pre-validation shape\n * that `parseConfig` round-trips through.\n *\n * `token` is stored as a string here so operators can populate it from a\n * secret store (e.g. an env-substituted plist or systemd EnvironmentFile).\n * It MUST NEVER be committed to source. The repo-wide privacy policy in\n * CLAUDE.md applies.\n */\nexport interface NotionLiveConnectorConfig {\n /** Master gate. Default false — operators must opt in explicitly. */\n enabled: boolean;\n /** Notion integration token. Starts with `secret_`. Populate from a secret store; never commit. */\n token: string;\n /** Array of Notion database ids to import pages from. Empty = connector is a no-op. */\n databaseIds: string[];\n /** Poll interval in ms. Default 300000 (5 min); min 1000; max 86400000 (24h). */\n pollIntervalMs: number;\n}\n\n/**\n * Operator-facing config for the Gmail live connector (issue #683 PR 4/6).\n * The connector module defines a separate validated `GmailConnectorConfig`\n * shape (frozen, post-validation). This interface is the pre-validation shape\n * that `parseConfig` round-trips through.\n *\n * OAuth2 credentials are stored as strings here so operators can populate\n * them from a secret store (e.g. env-substituted plist or systemd\n * EnvironmentFile). They MUST NEVER be committed to source. The repo-wide\n * privacy policy in CLAUDE.md applies.\n */\nexport interface GmailLiveConnectorConfig {\n /** Master gate. Default false — operators must opt in explicitly. */\n enabled: boolean;\n /** OAuth2 client id. Populate from a secret store; never commit. */\n clientId: string;\n /** OAuth2 client secret. Populate from a secret store; never commit. */\n clientSecret: string;\n /** OAuth2 refresh token issued for the Gmail scope. Populate from a secret store; never commit. */\n refreshToken: string;\n /** Gmail userId. Defaults to \"me\" (the authenticated user). */\n userId: string;\n /** Gmail search query applied in addition to the watermark filter. Default \"in:inbox\". */\n query: string;\n /** Poll interval in ms. Default 300000 (5 min); min 1000; max 86400000 (24h). */\n pollIntervalMs: number;\n}\n\n/**\n * Operator-facing config for the GitHub live connector (issue #683 PR 5/6).\n * The connector module defines a separate validated `GitHubConnectorConfig`\n * shape (frozen, post-validation). This interface is the pre-validation shape\n * that `parseConfig` round-trips through.\n *\n * `token` is stored as a string here so operators can populate it from a\n * secret store (e.g. an env-substituted plist or systemd EnvironmentFile).\n * It MUST NEVER be committed to source. The repo-wide privacy policy in\n * CLAUDE.md applies.\n */\nexport interface GitHubLiveConnectorConfig {\n /** Master gate. Default false — operators must opt in explicitly. */\n enabled: boolean;\n /** GitHub personal access token. Populate from a secret store; never commit. */\n token: string;\n /** GitHub login of the user whose comments will be imported. Required. */\n userLogin: string;\n /** Repos to poll in \"owner/repo\" format. Empty = connector is a no-op. */\n repos: string[];\n /** Poll interval in ms. Default 300000 (5 min); min 1000; max 86400000 (24h). */\n pollIntervalMs: number;\n /** Whether to fetch Discussion comments in addition to issue/PR comments. Default false. */\n includeDiscussions: boolean;\n}\n\nexport interface BootstrapOptions {\n dryRun?: boolean;\n sessionsDir?: string;\n limit?: number;\n since?: Date;\n}\n\nexport interface BootstrapResult {\n sessionsScanned: number;\n turnsProcessed: number;\n highSignalTurns: number;\n memoriesCreated: number;\n skipped: number;\n}\n\nexport interface PrincipalRule {\n match: string;\n principal: string;\n}\n\nexport interface NamespacePolicy {\n name: string;\n readPrincipals: string[];\n writePrincipals: string[];\n includeInRecallByDefault?: boolean;\n}\n\nexport interface RelevanceFeedback {\n up: number;\n down: number;\n lastUpdatedAt: string;\n notes?: string[];\n}\n\nexport interface BufferTurn {\n role: \"user\" | \"assistant\";\n content: string;\n timestamp: string;\n sessionKey?: string;\n logicalSessionKey?: string;\n providerThreadId?: string | null;\n turnFingerprint?: string;\n persistProcessedFingerprint?: boolean;\n parts?: import(\"./message-parts/index.js\").LcmMessagePartInput[];\n rawContent?: unknown;\n sourceFormat?: import(\"./message-parts/index.js\").MessagePartSourceFormat;\n}\n\nexport interface BufferEntryState {\n turns: BufferTurn[];\n lastExtractionAt: string | null;\n extractionCount: number;\n /**\n * Turns retained across `clearAfterExtraction` so a later extraction pass\n * sees the context that caused a defer verdict (issue #562, PR 2). Bounded\n * to the configured retention cap by `retainDeferredTurns`. Empty / absent\n * means no retention in effect.\n */\n retainedTurns?: BufferTurn[];\n}\n\nexport interface BufferState {\n turns: BufferTurn[];\n lastExtractionAt: string | null;\n extractionCount: number;\n entries?: Record<string, BufferEntryState>;\n}\n\nexport interface BehaviorLoopAdjustment {\n parameter: string;\n previousValue: number;\n nextValue: number;\n delta: number;\n evidenceCount: number;\n confidence: number;\n reason: string;\n appliedAt: string;\n}\n\nexport interface BehaviorLoopPolicyState {\n version: number;\n windowDays: number;\n minSignalCount: number;\n maxDeltaPerCycle: number;\n protectedParams: string[];\n adjustments: BehaviorLoopAdjustment[];\n updatedAt: string;\n}\n\nexport type BehaviorSignalType = \"correction_override\" | \"preference_affinity\" | \"topic_revisitation\" | \"action_pattern\" | \"outcome_preference\" | \"phrasing_style\";\nexport type BehaviorSignalDirection = \"positive\" | \"negative\";\n\nexport interface BehaviorSignalEvent {\n timestamp: string;\n namespace: string;\n memoryId: string;\n category: Extract<MemoryCategory, \"correction\" | \"preference\">;\n signalType: BehaviorSignalType;\n direction: BehaviorSignalDirection;\n confidence: number;\n signalHash: string;\n source: \"extraction\" | \"correction\";\n}\n\n/**\n * One row of the buffer-surprise telemetry ledger (issue #563 PR 3).\n *\n * Emitted by `SmartBuffer` each time the surprise probe produces a score\n * for an incoming turn (i.e. the feature flag is on and the existing\n * trigger-logic path called through to the probe). Not written when the\n * probe is skipped — the absence of a row is meaningful and matches the\n * \"probe was not consulted\" state.\n *\n * The ledger is intentionally lean: we record the score, the threshold in\n * force, whether the turn caused a flush, and the turn count so operators\n * can re-derive precision/recall without replaying traffic. Turn content\n * is never persisted — this ledger is safe to commit to shared storage.\n */\nexport interface BufferSurpriseEvent {\n /** Literal tag to simplify multiplexed log consumers. */\n event: \"BUFFER_SURPRISE\";\n /** ISO timestamp when the decision was made. Server-side, not turn ts. */\n timestamp: string;\n /** Buffer identifier (session / thread). Opaque string. */\n bufferKey: string;\n /** Session key if available; null when the turn has no session binding. */\n sessionKey: string | null;\n /** Role of the scored turn. */\n turnRole: \"user\" | \"assistant\";\n /** Surprise score in `[0, 1]`, already clamped. */\n surpriseScore: number;\n /** Threshold in force when the decision was made. */\n threshold: number;\n /** Whether this turn upgraded `keep_buffering` → `extract_now`. */\n triggeredFlush: boolean;\n /** Number of turns in the buffer (including the current turn). */\n turnCountInWindow: number;\n}\n\n/** Memory status for lifecycle management */\nexport type MemoryStatus =\n | \"active\"\n | \"pending_review\"\n | \"rejected\"\n | \"quarantined\"\n | \"superseded\"\n | \"archived\"\n /**\n * Operator explicitly forgot the memory (issue #686 PR 4/6). Soft\n * delete: the file stays on disk and a page-version snapshot is kept\n * so the act is reversible during a configurable retention window\n * (default 90 days), but the memory is excluded from recall, browse,\n * and entity attribution. After the retention window passes, a\n * future maintenance cron will hard-delete forgotten memories.\n */\n | \"forgotten\";\nexport type LifecycleState = \"candidate\" | \"validated\" | \"active\" | \"stale\" | \"archived\";\nexport type VerificationState = \"unverified\" | \"user_confirmed\" | \"system_inferred\" | \"disputed\";\nexport type PolicyClass = \"ephemeral\" | \"durable\" | \"protected\";\n\n/** Importance level tiers */\nexport type ImportanceLevel = \"critical\" | \"high\" | \"normal\" | \"low\" | \"trivial\";\n\n/** Importance scoring result */\nexport interface ImportanceScore {\n /** Numeric score 0-1 */\n score: number;\n /** Tier level */\n level: ImportanceLevel;\n /** Reasons for this score */\n reasons: string[];\n /** Salient keywords extracted */\n keywords: string[];\n}\n\nexport interface MemoryFrontmatter {\n id: string;\n category: MemoryCategory;\n created: string;\n updated: string;\n source: string;\n confidence: number;\n confidenceTier: ConfidenceTier;\n tags: string[];\n entityRef?: string;\n supersedes?: string;\n /** ISO 8601 date — memory expires and gets cleaned up after this date */\n expiresAt?: string;\n /** IDs of parent memories this was derived from (lineage tracking) */\n lineage?: string[];\n /** Memory status: active (default), pending_review, rejected, quarantined, superseded, archived, or forgotten */\n status?: MemoryStatus;\n /** ID of memory that superseded this one */\n supersededBy?: string;\n /** Timestamp when superseded */\n supersededAt?: string;\n /** Timestamp when archived */\n archivedAt?: string;\n /**\n * Explicit fact validity start (issue #680). ISO 8601 timestamp.\n *\n * When present, marks the moment at which the fact begins being\n * \"true\" / authoritative. When absent at read time, callers fall\n * back to `created` so legacy memories written before #680 still\n * participate in `as_of` recall filtering without a migration.\n */\n valid_at?: string;\n /**\n * Explicit fact validity end (issue #680). ISO 8601 timestamp.\n *\n * Set automatically by the temporal-supersession pipeline when a\n * newer fact supersedes this one — the value is the superseder's\n * `valid_at` (or `created` if no `valid_at` was set). May also be\n * set manually for facts that are known to expire at a specific\n * point in time.\n */\n invalid_at?: string;\n /**\n * Timestamp when the operator explicitly forgot this memory\n * (issue #686 PR 4/6). Set by `remnic forget <id>`. Memories with\n * `status === \"forgotten\"` are excluded from recall, browse, and\n * entity attribution; the file remains on disk until the retention\n * window passes.\n */\n forgottenAt?: string;\n /** Optional human-readable reason captured by `remnic forget --reason`. */\n forgottenReason?: string;\n /** Policy-driven lifecycle state used for retrieval eligibility/ranking. */\n lifecycleState?: LifecycleState;\n /** Verification provenance used by lifecycle policy. */\n verificationState?: VerificationState;\n /** Policy class used by lifecycle guardrails. */\n policyClass?: PolicyClass;\n /** Last lifecycle validation timestamp (ISO 8601). */\n lastValidatedAt?: string;\n /** Lifecycle decay score in [0,1]. */\n decayScore?: number;\n /** Lifecycle heat score in [0,1]. */\n heatScore?: number;\n // Access tracking (Phase 1A)\n /** Number of times this memory has been retrieved */\n accessCount?: number;\n /** Last time this memory was accessed (ISO 8601) */\n lastAccessed?: string;\n // Memory Worth counters (issue #560)\n //\n // Per-fact outcome counters used to derive a dynamic utility score —\n // `p(success | retrieved)` — as a complement to the static `importance`\n // field. Absent on legacy memories written before #560; readers must treat\n // `undefined` as zero observations (uniform Beta(1,1) prior).\n //\n // Both values must be non-negative integers on write. PR 1 wires only the\n // schema + storage round-trip — no increments, scoring, or filtering yet.\n /** Number of sessions where this memory was retrieved and the outcome was judged a success. */\n mw_success?: number;\n /** Number of sessions where this memory was retrieved and the outcome was judged a failure. */\n mw_fail?: number;\n // Importance scoring (Phase 1B)\n /** Importance score with level, reasons, and keywords */\n importance?: ImportanceScore;\n // Chunking (Phase 2A)\n /** Parent memory ID if this is a chunk */\n parentId?: string;\n /** Chunk index within parent (0-based) */\n chunkIndex?: number;\n /** Total number of chunks for this parent */\n chunkTotal?: number;\n // Memory Linking (Phase 3A)\n /** Links to other memories */\n links?: MemoryLink[];\n // Intent-grounded memory routing (v8.0 phase 1)\n intentGoal?: string;\n intentActionType?: string;\n intentEntityTypes?: string[];\n // Verbatim artifact lineage (v8.0 phase 1)\n artifactType?: \"decision\" | \"constraint\" | \"todo\" | \"definition\" | \"commitment\" | \"correction\" | \"fact\";\n sourceMemoryId?: string;\n sourceTurnId?: string;\n // v8.0 Phase 2B: HiMem episode/note classification\n /** episode = time-specific event; note = stable belief/preference/decision */\n memoryKind?: \"episode\" | \"note\" | \"box\" | \"dream\" | \"procedural\";\n /** Structured key-value attributes extracted from the content (e.g., product attributes, dates, quantities). */\n structuredAttributes?: Record<string, string>;\n /**\n * SHA-256 (via ContentHashIndex.computeHash) of the raw content that was\n * used as the dedup key at write time. Persists through archive and\n * consolidation so the hash can be removed from the index even if the stored\n * content has been transformed (e.g. an inline citation was appended).\n *\n * When present, archive/consolidation paths use this directly instead of\n * calling stripCitation(memory.content), which only handles the default\n * [Source: ...] format and silently fails for custom citation templates.\n */\n contentHash?: string;\n /**\n * Consolidation provenance — pointers to the page-versioning snapshots\n * that this memory was derived from (issue #561). Each entry is a\n * `\"<memory-path>:<version-number>\"` string (e.g.\n * `\"facts/preferences.md:3\"`) referencing a snapshot recorded by\n * `page-versioning.ts`.\n *\n * PR 1 introduces this field as read-through only — storage preserves\n * it verbatim but no code produces it yet. PR 2 populates it on\n * consolidation writes; PR 4 adds a `remnic doctor` integrity check\n * that validates each referent actually exists.\n */\n derived_from?: string[];\n /**\n * Which consolidation operator produced this memory (issue #561,\n * extended in #687). See `ConsolidationOperator` in\n * `semantic-consolidation.ts` for the operator algebra. Absent on\n * memories that were not produced by a consolidation pass.\n *\n * `\"pattern-reinforcement\"` (issue #687 PR 2/4) tags memories that\n * were promoted to canonical by the pattern-reinforcement\n * maintenance job after observing the same content across\n * multiple sessions.\n */\n derived_via?: \"split\" | \"merge\" | \"update\" | \"pattern-reinforcement\";\n /**\n * Number of source memories that reinforced this canonical memory\n * (issue #687 PR 2/4). Set by the pattern-reinforcement\n * maintenance job when it clusters duplicate memories and promotes\n * the most recent member to canonical. Counts the cluster size at\n * the time of the run; subsequent runs update this monotonically.\n *\n * Always a positive integer when present. Absent on memories that\n * have not been touched by pattern reinforcement.\n */\n reinforcement_count?: number;\n /**\n * ISO 8601 timestamp recording the most recent pattern-reinforcement\n * run that touched this memory (issue #687 PR 2/4). Updated each\n * time the cluster size grows. Absent when `reinforcement_count`\n * is absent.\n */\n last_reinforced_at?: string;\n}\n\n/** Memory link relationship types */\nexport type MemoryLinkType = \"follows\" | \"references\" | \"contradicts\" | \"supports\" | \"related\";\n\n/** A link between memories */\nexport interface MemoryLink {\n targetId: string;\n linkType: MemoryLinkType;\n strength: number;\n reason?: string;\n}\n\n// Conversation Threading (Phase 3B)\nexport interface ConversationThread {\n id: string;\n title: string;\n createdAt: string;\n updatedAt: string;\n sessionKey?: string;\n episodeIds: string[];\n linkedThreadIds: string[];\n}\n\n// Memory Summarization (Phase 4A)\nexport interface MemorySummary {\n id: string;\n createdAt: string;\n timeRangeStart: string;\n timeRangeEnd: string;\n summaryText: string;\n keyFacts: string[];\n keyEntities: string[];\n sourceEpisodeIds: string[];\n}\n\nexport interface DaySummaryResult {\n summary: string;\n bullets: string[];\n next_actions: string[];\n risks_or_open_loops: string[];\n}\n\n// Topic Extraction (Phase 4B)\nexport interface TopicScore {\n term: string;\n score: number;\n count: number;\n}\n\nexport interface MemoryFile {\n path: string;\n frontmatter: MemoryFrontmatter;\n content: string;\n}\n\n/**\n * Public type representing the **Observation** stage in the\n * Trace → Observation → Primitive pipeline (issue #685).\n *\n * - **Trace**: raw conversation turns captured in `buffer.ts`. Noisy,\n * verbose, ephemeral.\n * - **Observation** (this type): post-extraction, importance-scored\n * fact candidate emitted by `extraction.ts` / `extraction-judge.ts`.\n * Already distilled — but not yet consolidated against the corpus.\n * - **Primitive**: the durable `MemoryFile` written by `storage.ts`,\n * reinforced over time by `compounding/engine.ts`.\n *\n * `MemoryObservation` is the named handle on the intermediate stage\n * the codebase has always produced but never publicly typed. It lets\n * callers (telemetry, doctor surfaces, tests, downstream tooling)\n * inspect the post-extraction shape without reaching into extraction\n * internals.\n *\n * Naming note: this is intentionally NOT the same as the existing\n * `state/observation-ledger/` directory, which is telemetry storage\n * for the extraction pipeline (turn-count aggregates rebuilt by\n * `maintenance/rebuild-observations.ts` and judge verdict events\n * appended by `extraction-judge-telemetry.ts`). Lifecycle events on\n * primitives — status flips, supersessions, archival, forget — live\n * in `state/memory-lifecycle-ledger.jsonl`, written by\n * `StorageManager`. A `MemoryObservation` describes the in-flight\n * candidate that became (or didn't become) a primitive; the ledger\n * directory is how the pipeline reports on itself. See\n * `docs/trace-to-primitive.md` for the full pipeline walkthrough.\n */\nexport interface MemoryObservation {\n /** Stable id for this observation, distinct from any primitive id. */\n id: string;\n /** Source session id the trace came from. */\n sessionId?: string;\n /** ISO timestamp the observation was emitted. */\n observedAt: string;\n /** The extracted fact candidate (category, content, confidence, tags, etc.). */\n fact: ExtractedFact;\n /** Importance score in [0,1], from `importance.ts`. */\n importance?: number;\n /**\n * Whether the observation passed the extraction judge\n * (`extraction-judge.ts`). When `false`, the observation was\n * captured for telemetry but not persisted as a primitive.\n */\n judgeAccepted?: boolean;\n /** Optional reason the judge gave when rejecting. */\n judgeRejectionReason?: string;\n /**\n * Id of the resulting `MemoryFile` primitive once consolidation runs.\n * Absent until consolidation decides to ADD/MERGE/UPDATE the\n * observation into the corpus.\n */\n resultingPrimitiveId?: string;\n}\n\n/** Ordered step for extracted procedure memories (issue #519). */\nexport interface ExtractedProcedureStep {\n order: number;\n intent: string;\n toolCall?: { kind: string; signature: string };\n expectedOutcome?: string;\n optional?: boolean;\n}\n\nexport interface ExtractedFact {\n category: MemoryCategory;\n content: string;\n confidence: number;\n tags: string[];\n entityRef?: string;\n source?: ExtractionPassSource;\n promptedByQuestion?: string;\n /**\n * Whether this fact is project-scoped or globally applicable.\n * When `extractionScopeClassificationEnabled` is true, the extraction LLM\n * classifies each fact. Default is `\"project\"` when a coding context is\n * active, `\"global\"` when no coding context is present.\n */\n scope?: MemoryScope;\n /** Structured key-value attributes extracted from the content (e.g., product attributes, dates, quantities). */\n structuredAttributes?: Record<string, string>;\n /** When category is `procedure`, ordered steps with intents (persisted under procedures/). */\n procedureSteps?: ExtractedProcedureStep[];\n /**\n * When category is `reasoning_trace`, the stored solution chain the user\n * walked through. Persisted under reasoning-traces/.\n */\n reasoningTrace?: ExtractedReasoningTrace;\n}\n\nexport interface ExtractedReasoningTraceStep {\n order: number;\n description: string;\n}\n\nexport interface ExtractedReasoningTrace {\n steps: ExtractedReasoningTraceStep[];\n finalAnswer: string;\n observedOutcome?: string;\n}\n\nexport interface MemoryIntent {\n goal: string;\n actionType: string;\n entityTypes: string[];\n /** True when the prompt reads like starting a concrete task (ship/deploy/tests/PR, etc.). */\n taskInitiation?: boolean;\n}\n\nexport interface ExtractedQuestion {\n question: string;\n context: string;\n priority: number;\n}\n\nexport interface QuestionEntry {\n id: string;\n question: string;\n context: string;\n priority: number; // 0-1, higher = more important\n created: string;\n resolved: boolean;\n resolvedAt?: string;\n}\n\nexport interface ExtractionResult {\n facts: ExtractedFact[];\n profileUpdates: string[];\n entities: EntityMention[];\n questions: ExtractedQuestion[];\n identityReflection?: string;\n relationships?: ExtractedRelationship[];\n}\n\nexport interface EntityMention {\n name: string;\n type: \"person\" | \"project\" | \"tool\" | \"company\" | \"place\" | \"other\";\n facts: string[];\n structuredSections?: EntityStructuredSection[];\n source?: ExtractionPassSource;\n promptedByQuestion?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Knowledge Graph Enhancement (Entity Relationships, Activity, Scoring)\n// ---------------------------------------------------------------------------\n\nexport interface EntityRelationship {\n target: string;\n label: string;\n}\n\nexport interface EntityActivityEntry {\n date: string;\n note: string;\n}\n\nexport interface EntityTimelineEntry {\n timestamp: string;\n text: string;\n source?: string;\n sessionKey?: string;\n principal?: string;\n}\n\nexport interface EntityStructuredSection {\n key: string;\n title: string;\n facts: string[];\n}\n\nexport interface EntitySchemaSectionDefinition {\n key: string;\n title: string;\n description: string;\n aliases?: string[];\n}\n\nexport interface EntitySchemaDefinition {\n sections: EntitySchemaSectionDefinition[];\n}\n\nexport interface EntityFile {\n name: string;\n type: string;\n created?: string;\n updated: string;\n extraFrontmatterLines?: string[];\n preSectionLines?: string[];\n facts: string[];\n summary?: string;\n synthesis?: string;\n synthesisUpdatedAt?: string;\n synthesisTimelineCount?: number;\n synthesisStructuredFactCount?: number;\n synthesisStructuredFactDigest?: string;\n synthesisVersion?: number;\n timeline: EntityTimelineEntry[];\n structuredSections?: EntityStructuredSection[];\n relationships: EntityRelationship[];\n activity: EntityActivityEntry[];\n aliases: string[];\n extraSections?: Array<{\n title: string;\n lines: string[];\n }>;\n}\n\nexport interface ScoredEntity {\n name: string;\n type: string;\n score: number;\n factCount: number;\n summary?: string;\n topRelationships: string[];\n}\n\nexport interface ExtractedRelationship {\n source: string;\n target: string;\n label: string;\n extractionSource?: ExtractionPassSource;\n promptedByQuestion?: string;\n}\n\nexport interface ConsolidationItem {\n existingId: string;\n action: ConsolidationAction;\n mergeWith?: string;\n updatedContent?: string;\n reason: string;\n}\n\nexport interface ConsolidationResult {\n items: ConsolidationItem[];\n profileUpdates: string[];\n entityUpdates: EntityMention[];\n}\n\nexport interface ConsolidationObservation {\n runAt: string;\n recentMemories: MemoryFile[];\n existingMemories: MemoryFile[];\n profile: string;\n result: ConsolidationResult;\n merged: number;\n invalidated: number;\n}\n\nexport interface QmdSearchResult {\n docid: string;\n path: string;\n snippet: string;\n score: number;\n explain?: QmdSearchExplain;\n transport?: \"daemon\" | \"subprocess\" | \"hybrid\" | \"scoped_prefilter\";\n}\n\nexport interface QmdSearchExplain {\n ftsScores?: number[];\n vectorScores?: number[];\n rrf?: number;\n rerankScore?: number;\n blendedScore?: number;\n /** Additive boost applied from `reinforcement_count` frontmatter (issue #687 PR 3/4). */\n reinforcementBoost?: number;\n}\n\nexport interface MetaState {\n extractionCount: number;\n lastExtractionAt: string | null;\n lastConsolidationAt: string | null;\n totalMemories: number;\n totalEntities: number;\n processedExtractionFingerprints?: Array<{\n fingerprint: string;\n observedAt: string;\n }>;\n}\n\nexport type MemoryActionType =\n | \"store_episode\"\n | \"store_note\"\n | \"update_note\"\n | \"create_artifact\"\n | \"summarize_node\"\n | \"discard\"\n | \"link_graph\";\n\nexport type MemoryActionOutcome = \"applied\" | \"skipped\" | \"failed\";\n\nexport type MemoryActionPolicyDecision = \"allow\" | \"defer\" | \"deny\";\n\nexport type MemoryActionStatus = \"validated\" | \"applied\" | \"rejected\";\n\nexport type MemoryActionEligibilitySource =\n | \"extraction\"\n | \"consolidation\"\n | \"replay\"\n | \"manual\"\n | \"unknown\";\n\nexport interface MemoryActionEligibilityContext {\n confidence: number;\n lifecycleState: LifecycleState;\n importance: number;\n source: MemoryActionEligibilitySource;\n}\n\nexport interface MemoryActionPolicyResult {\n action: MemoryActionType;\n decision: MemoryActionPolicyDecision;\n rationale: string;\n eligibility: MemoryActionEligibilityContext;\n}\n\nexport interface MemoryActionEvent {\n schemaVersion?: number;\n actionId?: string;\n timestamp: string;\n action: MemoryActionType;\n outcome: MemoryActionOutcome;\n status?: MemoryActionStatus;\n actor?: string;\n subsystem?: string;\n reason?: string;\n memoryId?: string;\n namespace?: string;\n sessionKey?: string;\n sourceSessionKey?: string;\n checkpointCapturedAt?: string;\n checkpointTtl?: string;\n checkpointTurnCount?: number;\n inputSummary?: string;\n outputMemoryIds?: string[];\n dryRun?: boolean;\n policyVersion?: string;\n promptHash?: string;\n policyDecision?: MemoryActionPolicyDecision;\n policyRationale?: string;\n policyEligibility?: MemoryActionEligibilityContext;\n}\n\nexport type MemoryLifecycleEventType =\n | \"created\"\n | \"updated\"\n | \"superseded\"\n | \"archived\"\n | \"rejected\"\n | \"restored\"\n | \"merged\"\n | \"imported\"\n | \"promoted\"\n | \"explicit_capture_accepted\"\n | \"explicit_capture_queued\";\n\nexport interface MemoryLifecycleStateSummary {\n category?: MemoryCategory;\n path?: string;\n status?: MemoryStatus;\n lifecycleState?: LifecycleState;\n}\n\nexport interface MemoryLifecycleEvent {\n eventId: string;\n memoryId: string;\n eventType: MemoryLifecycleEventType;\n timestamp: string;\n actor: string;\n reasonCode?: string;\n ruleVersion: string;\n relatedMemoryIds?: string[];\n before?: MemoryLifecycleStateSummary;\n after?: MemoryLifecycleStateSummary;\n correlationId?: string;\n}\n\nexport interface MemoryProjectionCurrentState {\n memoryId: string;\n category: MemoryCategory;\n status: MemoryStatus;\n lifecycleState?: LifecycleState;\n path: string;\n pathRel: string;\n created: string;\n updated: string;\n archivedAt?: string;\n supersededAt?: string;\n entityRef?: string;\n source: string;\n confidence: number;\n confidenceTier: ConfidenceTier;\n memoryKind?: MemoryFrontmatter[\"memoryKind\"];\n accessCount?: number;\n lastAccessed?: string;\n tags?: string[];\n preview?: string;\n}\n\nexport interface CompressionGuidelineOptimizerSourceWindow {\n from: string;\n to: string;\n}\n\nexport interface CompressionGuidelineOptimizerEventCounts {\n total: number;\n applied: number;\n skipped: number;\n failed: number;\n}\n\nexport type CompressionGuidelineActivationState = \"draft\" | \"active\";\n\nexport interface CompressionGuidelineOptimizerActionSummary {\n action: MemoryActionType;\n total: number;\n outcomes: Record<MemoryActionOutcome, number>;\n quality: {\n good: number;\n poor: number;\n unknown: number;\n };\n}\n\nexport interface CompressionGuidelineOptimizerRuleUpdate {\n action: MemoryActionType;\n delta: number;\n direction: \"increase\" | \"decrease\" | \"hold\";\n confidence: \"low\" | \"medium\" | \"high\";\n notes: string[];\n}\n\nexport interface CompressionGuidelineOptimizerState {\n version: number;\n updatedAt: string;\n sourceWindow: CompressionGuidelineOptimizerSourceWindow;\n eventCounts: CompressionGuidelineOptimizerEventCounts;\n guidelineVersion: number;\n contentHash?: string;\n activationState?: CompressionGuidelineActivationState;\n actionSummaries?: CompressionGuidelineOptimizerActionSummary[];\n ruleUpdates?: CompressionGuidelineOptimizerRuleUpdate[];\n}\n\nexport type ContinuityIncidentState = \"open\" | \"closed\";\n\nexport interface ContinuityIncidentRecord {\n id: string;\n state: ContinuityIncidentState;\n openedAt: string;\n updatedAt: string;\n triggerWindow?: string;\n symptom: string;\n suspectedCause?: string;\n fixApplied?: string;\n verificationResult?: string;\n preventiveRule?: string;\n closedAt?: string;\n filePath?: string;\n}\n\nexport interface ContinuityIncidentOpenInput {\n triggerWindow?: string;\n symptom: string;\n suspectedCause?: string;\n}\n\nexport interface ContinuityIncidentCloseInput {\n fixApplied: string;\n verificationResult: string;\n preventiveRule?: string;\n}\n\nexport type ContinuityLoopCadence = \"daily\" | \"weekly\" | \"monthly\" | \"quarterly\";\nexport type ContinuityLoopStatus = \"active\" | \"paused\" | \"retired\";\n\nexport interface ContinuityImprovementLoop {\n id: string;\n cadence: ContinuityLoopCadence;\n purpose: string;\n status: ContinuityLoopStatus;\n killCondition: string;\n lastReviewed: string;\n notes?: string;\n}\n\nexport interface ContinuityLoopUpsertInput {\n id: string;\n cadence: ContinuityLoopCadence;\n purpose: string;\n status: ContinuityLoopStatus;\n killCondition: string;\n lastReviewed?: string;\n notes?: string;\n}\n\nexport interface ContinuityLoopReviewInput {\n status?: ContinuityLoopStatus;\n notes?: string;\n reviewedAt?: string;\n}\n\n/** Entry in the access tracking buffer (batched updates) */\nexport interface AccessTrackingEntry {\n memoryId: string;\n newCount: number;\n lastAccessed: string;\n}\n\nexport interface SignalScanResult {\n level: SignalLevel;\n patterns: string[];\n}\n\n// ============================================================================\n// LLM Trace Callback (for external observability plugins)\n// ============================================================================\n\nexport interface LlmTraceEvent {\n kind: \"llm_start\" | \"llm_end\" | \"llm_error\";\n traceId: string;\n model: string;\n operation: \"extraction\" | \"consolidation\" | \"profile_consolidation\" | \"identity_consolidation\" | \"day_summary\";\n input?: string;\n output?: string;\n durationMs?: number;\n error?: string;\n tokenUsage?: { input?: number; output?: number; total?: number };\n}\n\nexport interface RecallTraceEvent {\n kind: \"recall_summary\";\n traceId: string;\n operation: \"recall\";\n sessionKey?: string;\n promptHash: string;\n promptLength: number;\n retrievalQueryHash: string;\n retrievalQueryLength: number;\n recallMode: RecallPlanMode;\n recallResultLimit: number;\n qmdEnabled: boolean;\n qmdAvailable: boolean;\n recallNamespaces: string[];\n source: \"none\" | \"hot_qmd\" | \"hot_embedding\" | \"cold_fallback\" | \"recent_scan\";\n recalledMemoryCount: number;\n injected: boolean;\n contextChars: number;\n policyVersion?: string;\n identityInjectionMode?: IdentityInjectionMode | \"none\";\n identityInjectedChars?: number;\n identityInjectionTruncated?: boolean;\n durationMs: number;\n timings?: Record<string, string>;\n /**\n * The full recalled memory context injected into the system prompt.\n * Only populated when `traceRecallContent` config option is `true`.\n * Omitted by default to avoid sending potentially sensitive memory content\n * to external trace collectors unless explicitly opted in.\n */\n recalledContent?: string;\n}\n\nexport type EngramTraceEvent = LlmTraceEvent | RecallTraceEvent;\nexport type LlmTraceCallback = (event: EngramTraceEvent) => void;\n\n// ============================================================================\n// Gateway Configuration Types (for fallback AI)\n// ============================================================================\n\nexport type ModelApi = \"openai-completions\" | \"anthropic-messages\" | \"google-generative\" | string;\n\nexport type ModelProviderAuthMode = \"bearer\" | \"header\" | \"query\";\n\nexport interface ModelDefinitionConfig {\n id: string;\n name?: string;\n contextWindow?: number;\n maxOutputTokens?: number;\n costPer1MInput?: number;\n costPer1MOutput?: number;\n aliases?: string[];\n}\n\nexport interface ModelProviderConfig {\n baseUrl: string;\n apiKey?: string | Record<string, unknown>;\n auth?: ModelProviderAuthMode;\n api?: ModelApi;\n headers?: Record<string, string>;\n authHeader?: boolean;\n models: ModelDefinitionConfig[];\n}\n\nexport interface AgentDefaultsConfig {\n model?: {\n primary?: string;\n backup?: string;\n fallbacks?: string[];\n };\n thinking?: {\n mode?: \"off\" | \"on\" | \"adaptive\";\n budget?: number;\n };\n}\n\nexport interface AgentPersonaModelConfig {\n primary?: string;\n fallbacks?: string[];\n}\n\nexport interface AgentPersona {\n id: string;\n name?: string;\n model?: AgentPersonaModelConfig;\n}\n\nexport interface GatewayConfig {\n agents?: {\n defaults?: AgentDefaultsConfig;\n list?: AgentPersona[];\n };\n models?: {\n providers?: Record<string, ModelProviderConfig>;\n };\n}\n\n// ============================================================================\n// Transcript & Context Preservation (v2.0)\n// ============================================================================\n\nexport interface TranscriptEntry {\n timestamp: string;\n role: \"user\" | \"assistant\";\n content: string;\n sessionKey: string;\n turnId: string;\n metadata?: {\n compactAfter?: boolean;\n compactionId?: string | null;\n };\n}\n\nexport interface Checkpoint {\n sessionKey: string;\n capturedAt: string;\n turns: TranscriptEntry[];\n ttl: string; // ISO timestamp when checkpoint expires\n}\n\nexport interface HourlySummary {\n hour: string; // \"2026-02-08T14:00:00Z\"\n sessionKey: string;\n bullets: string[];\n turnCount: number;\n generatedAt: string;\n}\n\n// ============================================================================\n// Dreams Pipeline Telemetry (issue #678 PR 3/4)\n// ============================================================================\n\n// Re-export from the authoritative source to avoid duplicate definitions.\n// dreams-ledger.ts is the single source of truth; types.ts re-exports so\n// callers that import from types.js continue to work unchanged.\nexport type {\n DreamsPhase,\n DreamsPhaseStatus,\n DreamsStatusResult,\n DreamsRunResult,\n} from \"./maintenance/dreams-ledger.js\";\n"],"mappings":";AA+GO,IAAM,2BAAwD;AAAA,EACnE;AAAA,EACA;AAAA,EACA;AACF;AAOO,IAAM,4BAA8C;AAEpD,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,OAAO,UAAU,YAClB,yBAA+C,SAAS,KAAK;AACrE;AAiWO,SAAS,eAAe,OAA+B;AAC5D,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,SAAS,IAAM,QAAO;AAC1B,MAAI,SAAS,IAAM,QAAO;AAC1B,SAAO;AACT;AAGO,IAAM,uBAAuB;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/peers/migrate-from-identity-anchor.ts","../src/peers/storage.ts","../src/peers/types.ts"],"sourcesContent":["/**\n * Identity-anchor → peer-registry migration — issue #679 PR 5/5.\n *\n * Reads the legacy `identity/identity-anchor.md` file (written by\n * `StorageManager.writeIdentityAnchor`) and the `IDENTITY.md`\n * reflections file, then seeds the peer registry with a `self` peer\n * whose `peers/self/identity.md` kernel captures the relevant\n * source material.\n *\n * Design principles:\n *\n * 1. **Idempotent** — if `peers/self/identity.md` already exists this\n * function returns `{ skipped: true }` without overwriting it. The\n * second-run guarantee prevents accidental double-migration and\n * makes `remnic peer migrate` safe to run in CI / post-install hooks.\n *\n * 2. **Non-destructive** — the legacy files are never deleted. The\n * operator is responsible for archive / clean-up after verifying the\n * migration result. Legacy `engram.identity_anchor_*` MCP tools\n * continue to work against the on-disk anchor file.\n *\n * 3. **Dry-run** — when `options.dryRun` is `true` the function\n * computes the proposed `Peer` record and returns it alongside a\n * `{ dryRun: true }` marker, without writing anything to disk.\n *\n * 4. **Transparent** — the return value always includes the full\n * proposed `Peer` so callers can print what was (or would be) written.\n *\n * ## Source data\n *\n * The migrator reads two optional legacy sources:\n *\n * - `{memoryDir}/identity/identity-anchor.md` — structured sections\n * (`## Identity Traits`, `## Communication Preferences`, etc.) written\n * by `identityAnchorUpdate`. When present, the full content is\n * embedded in `peer.notes` under a clearly labelled subsection.\n *\n * - `{memoryDir}/IDENTITY.md` — free-form reflection entries appended\n * by the extraction engine. When present it is summarised as\n * a second subsection of `peer.notes`.\n *\n * If neither file exists the `self` peer is still created with sensible\n * defaults so that the peer registry is bootstrapped for fresh installs.\n *\n * ## Path safety\n *\n * All file reads use `path.join` relative to the caller-supplied\n * `memoryDir`. The migration never follows symlinks for the source files\n * (uses `lstat` to detect + skip them). Writes go through the standard\n * `writePeer` helper which enforces all path-traversal and symlink guards.\n */\n\nimport { promises as fs, constants as fsConstants } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { readPeer, writePeer, PEERS_DIR_NAME } from \"./storage.js\";\nimport type { Peer } from \"./types.js\";\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Public types\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Options for `migrateFromIdentityAnchor`. */\nexport interface MigrateFromIdentityAnchorOptions {\n /**\n * Memory directory (same value as `config.memoryDir`). All source and\n * destination paths are resolved relative to this root.\n */\n memoryDir: string;\n\n /**\n * When `true`, compute and return the proposed peer record without\n * writing anything to disk. The `written` and `skipped` flags in the\n * result will both be `false`; the `peer` field carries the proposed\n * record.\n */\n dryRun?: boolean;\n\n /**\n * Optional override for the `self` peer's display name. Defaults to\n * `\"Self\"`.\n */\n displayName?: string;\n\n /**\n * Optional ISO-8601 timestamp to use as `createdAt`. When omitted the\n * current time is used.\n */\n createdAt?: string;\n}\n\n/** Result returned by `migrateFromIdentityAnchor`. */\nexport interface MigrateFromIdentityAnchorResult {\n /**\n * The peer record that was (or would be) written for `peers/self/`.\n * Always present regardless of `skipped` or `dryRun`.\n */\n peer: Peer;\n\n /**\n * `true` when the migration wrote `peers/self/identity.md` to disk.\n * `false` when `dryRun` is set or the file already existed (`skipped`).\n */\n written: boolean;\n\n /**\n * `true` when an existing `peers/self/identity.md` was detected and\n * the migration was a no-op. When `skipped` is `true`, `written` is\n * always `false`.\n */\n skipped: boolean;\n\n /**\n * `true` when `options.dryRun` was set. No files were written; the\n * `peer` field holds the proposed record.\n */\n dryRun: boolean;\n\n /**\n * The identity-anchor source file path that was read (if found).\n * `null` when the file did not exist or was a symlink (skipped).\n */\n identityAnchorSource: string | null;\n\n /**\n * The `IDENTITY.md` source file path that was read (if found).\n * `null` when the file did not exist or was a symlink (skipped).\n */\n identityMdSource: string | null;\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Internal helpers\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Read a legacy source file with the same security posture as the peer storage\n * module:\n *\n * 1. **Parent-directory symlink rejection** — lstat the parent directory and\n * return null if it is a symlink. This closes the case where e.g.\n * `memoryDir/identity/` is itself a symlink pointing outside `memoryDir`.\n *\n * 2. **Kernel-level O_NOFOLLOW** — open the file with `O_NOFOLLOW` so the\n * kernel atomically rejects a symlink at the final path component. This\n * eliminates the TOCTOU race between a separate lstat check and the\n * subsequent read that `safeReadLegacyFile` previously had.\n *\n * Returns `{ content, filePath }` on success, or `{ content: null, filePath: null }`\n * when the file is missing, is a symlink, the parent is a symlink, or the\n * path is not a regular file. Re-throws unexpected I/O errors (EACCES, EIO,\n * etc.) so the caller can surface real filesystem problems.\n */\nasync function safeReadLegacyFile(\n filePath: string,\n): Promise<{ content: string; filePath: string } | { content: null; filePath: null }> {\n // 1. Reject a symlinked parent directory.\n const parent = path.dirname(filePath);\n try {\n const parentStat = await fs.lstat(parent);\n if (parentStat.isSymbolicLink()) {\n return { content: null, filePath: null };\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { content: null, filePath: null };\n }\n throw err;\n }\n\n // 2. Open with O_NOFOLLOW so the kernel refuses a symlink at the target\n // path, atomically closing the lstat-then-read TOCTOU window.\n let fh: import(\"node:fs/promises\").FileHandle;\n try {\n fh = await fs.open(filePath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n // ENOENT — file does not exist; ELOOP — O_NOFOLLOW detected a symlink.\n if (code === \"ENOENT\" || code === \"ELOOP\" || code === \"ENOTDIR\") {\n return { content: null, filePath: null };\n }\n throw err;\n }\n\n try {\n // Verify it is a regular file (not a directory, FIFO, device, etc.).\n const stat = await fh.stat();\n if (!stat.isFile()) {\n return { content: null, filePath: null };\n }\n const content = await fh.readFile(\"utf8\");\n return { content, filePath };\n } finally {\n await fh.close();\n }\n}\n\n/**\n * Build the `notes` markdown body for the `self` peer from the two\n * optional legacy sources.\n *\n * The resulting notes are kept intentionally terse — the peer kernel is\n * meant to hold stable identity facts, not a full dump of every reflection\n * entry. We label the sections clearly so operators know exactly what came\n * from where.\n */\nfunction buildSelfNotes(\n anchorContent: string | null,\n identityMdContent: string | null,\n): string | undefined {\n const parts: string[] = [];\n\n if (anchorContent !== null && anchorContent.trim().length > 0) {\n parts.push(\n \"## Migrated from identity-anchor.md\\n\\n\" + anchorContent.trim(),\n );\n }\n\n if (identityMdContent !== null && identityMdContent.trim().length > 0) {\n // IDENTITY.md can be quite long. Embed it as a labelled section so the\n // operator can prune it manually after migration rather than losing data.\n parts.push(\n \"## Migrated from IDENTITY.md\\n\\n\" + identityMdContent.trim(),\n );\n }\n\n if (parts.length === 0) return undefined;\n return parts.join(\"\\n\\n\");\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Public API\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Migrate legacy identity-anchor data into `peers/self/identity.md`.\n *\n * @see {@link MigrateFromIdentityAnchorOptions} for full option documentation.\n * @see {@link MigrateFromIdentityAnchorResult} for return value semantics.\n */\nexport async function migrateFromIdentityAnchor(\n options: MigrateFromIdentityAnchorOptions,\n): Promise<MigrateFromIdentityAnchorResult> {\n const { memoryDir, dryRun = false } = options;\n const now = new Date().toISOString();\n const createdAt = options.createdAt ?? now;\n const displayName = options.displayName ?? \"Self\";\n\n // 1. Guard: if peers/self/identity.md already exists, skip.\n // We read through the standard `readPeer` helper (which enforces all\n // path-traversal guards) rather than stat-ing the file directly.\n const existing = await readPeer(memoryDir, \"self\");\n if (existing !== null) {\n return {\n peer: existing,\n written: false,\n skipped: true,\n // P2 (codex): preserve the caller-requested dryRun flag even on the\n // skip path. Previously hard-coded `false`, which contradicted the\n // MigrateFromIdentityAnchorResult contract for dry-run callers.\n dryRun,\n identityAnchorSource: null,\n identityMdSource: null,\n };\n }\n\n // 2. Read legacy sources (both optional; symlinks are silently skipped).\n const anchorPath = path.join(memoryDir, \"identity\", \"identity-anchor.md\");\n const identityMdPath = path.join(memoryDir, \"IDENTITY.md\");\n\n const [anchorResult, identityMdResult] = await Promise.all([\n safeReadLegacyFile(anchorPath),\n safeReadLegacyFile(identityMdPath),\n ]);\n\n // 3. Compose the self peer record.\n const notes = buildSelfNotes(\n anchorResult.content,\n identityMdResult.content,\n );\n\n const peer: Peer = {\n id: \"self\",\n kind: \"self\",\n displayName,\n createdAt,\n updatedAt: now,\n ...(notes !== undefined ? { notes } : {}),\n };\n\n // 4. Write (unless dry-run).\n //\n // P2 (codex): the previous readPeer-then-writePeer pattern had a\n // check-then-act TOCTOU race: two concurrent `remnic peer migrate`\n // processes could both observe \"missing\" and the later write would\n // overwrite the file, violating the non-destructive / idempotent\n // guarantee.\n //\n // Mitigation: before calling writePeer we attempt to exclusively create\n // (O_CREAT | O_EXCL | O_NOFOLLOW) the identity file path. If the open\n // succeeds we are the sole creator; close the handle immediately and let\n // writePeer fill in the real content (it mkdirs + rewrites atomically).\n // If O_EXCL fails with EEXIST, another process won the race — re-read the\n // file and return `skipped`. The window between our exclusive open and the\n // writePeer overwrite is deliberate: we've already locked out concurrent\n // migrate calls via EEXIST, and `peer set self` targeting the same file\n // path will succeed (it is a legitimate overwrite, not a migrate race).\n if (!dryRun) {\n const selfPeerDir = path.join(memoryDir, PEERS_DIR_NAME, \"self\");\n await fs.mkdir(selfPeerDir, { recursive: true });\n const identityFilePath = path.join(selfPeerDir, \"identity.md\");\n let exclusiveFh: import(\"node:fs/promises\").FileHandle | null = null;\n try {\n exclusiveFh = await fs.open(\n identityFilePath,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,\n );\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"EEXIST\") {\n // Another process created the file between our readPeer check and\n // now. Re-read and return skipped.\n const raceWinner = await readPeer(memoryDir, \"self\");\n return {\n peer: raceWinner ?? peer,\n written: false,\n skipped: true,\n dryRun,\n identityAnchorSource: null,\n identityMdSource: null,\n };\n }\n throw err;\n } finally {\n if (exclusiveFh !== null) await exclusiveFh.close();\n }\n // We hold the exclusive-create claim. Now writePeer fills in the\n // real content (it calls mkdirPeerDirAtomic + writeFileNoFollow).\n await writePeer(memoryDir, peer);\n }\n\n return {\n peer,\n written: !dryRun,\n skipped: false,\n dryRun,\n identityAnchorSource: anchorResult.filePath,\n identityMdSource: identityMdResult.filePath,\n };\n}\n","/**\n * Peer registry storage primitives — issue #679 PR 1/5.\n *\n * Pure file-I/O helpers for the per-peer kernel files:\n *\n * peers/{peer-id}/identity.md — slow, human-edited identity facts\n * peers/{peer-id}/profile.md — evolving profile (reasoner-owned)\n * peers/{peer-id}/interactions.log.md — append-only signal log\n *\n * No reasoner logic, no recall integration, no migration of existing\n * identity-anchor data — those land in PR 2/5 — 5/5.\n *\n * Path safety: `peerId` is validated against PEER_ID_PATTERN before any\n * filesystem operation. Reading a non-existent peer returns null (does not\n * throw). Reading malformed files throws — callers can catch and recover.\n */\n\nimport { promises as fs, constants as fsConstants } from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Atomic, symlink-rejecting open: returns a file handle whose\n * underlying open(2) call carried `O_NOFOLLOW`, so the kernel itself\n * refuses to follow a symlink at the target path. Closes the\n * check-then-use TOCTOU race that a separate `assertPathNotSymlink`\n * + `fs.writeFile` pattern leaves open (codex P1 round 5 on PR #723).\n */\nasync function openNoFollow(file: string, flags: number): Promise<import(\"node:fs/promises\").FileHandle> {\n return fs.open(file, flags | fsConstants.O_NOFOLLOW);\n}\n\n/** Read a file, refusing to follow symlinks at the kernel level\n * AND verifying parent-dir inode stability (codex P1 round 9). */\nasync function readFileNoFollow(file: string): Promise<string> {\n await assertParentDirInodeStable(file);\n const fh = await openNoFollow(file, fsConstants.O_RDONLY);\n try {\n return await fh.readFile(\"utf8\");\n } finally {\n await fh.close();\n }\n}\n\n/**\n * Codex P1 round 9: O_NOFOLLOW only protects the FINAL path\n * component, so a parent-directory swap mid-flight (peers/<id>\n * unlinked + replaced with a symlink between assertPeerDirNotEscaped\n * and the open) could still write outside memoryDir. Without\n * `openat` (Node has no stable JS binding for it), the best\n * pure-Node mitigation is to:\n * 1. Open the parent directory with O_DIRECTORY | O_NOFOLLOW so\n * WE hold the original-inode handle.\n * 2. fstat the parent handle and compare its (dev, inode) against\n * lstat of the parent path. If they diverge, a swap happened\n * between mkdir and now — abort.\n * 3. Then do the symlink-rejecting open of the file.\n * This narrows the race window to the few microseconds between the\n * fstat/lstat compare and the open. Fully closing the race needs\n * `openat`, which is tracked as a follow-up.\n */\nasync function assertParentDirInodeStable(filePath: string): Promise<void> {\n const parent = path.dirname(filePath);\n const dh = await fs.open(\n parent,\n fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW,\n );\n try {\n const fstatInfo = await dh.stat();\n const lstatInfo = await fs.lstat(parent);\n if (fstatInfo.ino !== lstatInfo.ino || fstatInfo.dev !== lstatInfo.dev) {\n throw new Error(\n `parent directory \"${parent}\" was swapped between checks (inode mismatch)`,\n );\n }\n if (lstatInfo.isSymbolicLink()) {\n throw new Error(`parent directory \"${parent}\" is a symlink and is rejected`);\n }\n } finally {\n await dh.close();\n }\n}\n\n/** Overwrite a file, refusing to follow symlinks at the kernel level\n * AND verifying the parent directory inode is stable across the open\n * (codex P1 round 9). */\nasync function writeFileNoFollow(file: string, data: string): Promise<void> {\n await assertParentDirInodeStable(file);\n const fh = await openNoFollow(\n file,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC,\n );\n try {\n await fh.writeFile(data, \"utf8\");\n } finally {\n await fh.close();\n }\n}\n\n/** Append to a file, refusing to follow symlinks AND verifying parent\n * inode stability (codex P1 round 9). */\nasync function appendFileNoFollow(file: string, data: string): Promise<void> {\n await assertParentDirInodeStable(file);\n const fh = await openNoFollow(\n file,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_APPEND,\n );\n try {\n await fh.writeFile(data, \"utf8\");\n } finally {\n await fh.close();\n }\n}\n\nimport {\n PEER_ID_MAX_LENGTH,\n PEER_ID_PATTERN,\n type Peer,\n type PeerInteractionLogEntry,\n type PeerKind,\n type PeerProfile,\n type PeerProfileFieldProvenance,\n} from \"./types.js\";\n\n// ──────────────────────────────────────────────────────────────────────\n// Validation\n// ──────────────────────────────────────────────────────────────────────\n\nconst ALLOWED_KINDS: ReadonlySet<PeerKind> = new Set<PeerKind>([\n \"self\",\n \"human\",\n \"agent\",\n \"integration\",\n]);\n\n/**\n * Validate a peer id. Throws `Error` with a descriptive message on failure.\n * Exported so callers can pre-check user input before constructing a Peer.\n */\nexport function assertValidPeerId(peerId: unknown): asserts peerId is string {\n if (typeof peerId !== \"string\") {\n throw new Error(\"peerId must be a string\");\n }\n if (peerId.length === 0) {\n throw new Error(\"peerId must not be empty\");\n }\n if (peerId.length > PEER_ID_MAX_LENGTH) {\n throw new Error(`peerId must be ≤ ${PEER_ID_MAX_LENGTH} characters`);\n }\n if (!PEER_ID_PATTERN.test(peerId)) {\n throw new Error(\n `peerId \"${peerId}\" is invalid — must match ${PEER_ID_PATTERN}`,\n );\n }\n // Defence-in-depth: reject consecutive dots/dashes/underscores. The\n // regex already prevents leading/trailing separators, but explicit\n // adjacency checks document intent and survive future regex refactors.\n if (/[.\\-_]{2,}/.test(peerId)) {\n throw new Error(\n `peerId \"${peerId}\" is invalid — must not contain consecutive separators`,\n );\n }\n}\n\n/**\n * Strict plain-object check. Rejects arrays, null, Maps, Sets, class\n * instances, and anything else with a non-Object/null prototype.\n * Codex P2 round 13: writePeerProfile previously treated any\n * non-null non-array object as plain; inputs like `new Map()` would\n * pass and serialize to `{}` losing all data.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return false;\n }\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction assertValidKind(kind: unknown): asserts kind is PeerKind {\n if (typeof kind !== \"string\" || !ALLOWED_KINDS.has(kind as PeerKind)) {\n throw new Error(\n `peer kind must be one of ${Array.from(ALLOWED_KINDS).join(\", \")}`,\n );\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Path helpers\n// ──────────────────────────────────────────────────────────────────────\n\n/** Root directory holding the peer registry, relative to memoryDir. */\nexport const PEERS_DIR_NAME = \"peers\";\n\nfunction peersRoot(memoryDir: string): string {\n return path.join(memoryDir, PEERS_DIR_NAME);\n}\n\nfunction peerDir(memoryDir: string, peerId: string): string {\n // Guard against path traversal on top of regex validation. After\n // assertValidPeerId, peerId cannot contain `/`, `..`, or NUL — but we\n // re-check defensively here.\n assertValidPeerId(peerId);\n const candidate = path.join(peersRoot(memoryDir), peerId);\n const root = peersRoot(memoryDir);\n // Ensure resolved path stays within peersRoot. Note: this is a\n // lexical check only — a symlinked peer directory can still escape.\n // I/O sites must additionally call `assertPeerDirNotEscaped` (below)\n // before reading or writing, which uses lstat to reject symlinks\n // and realpath to confirm physical containment (codex P1 #723).\n const relative = path.relative(root, candidate);\n if (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n throw new Error(`peerId \"${peerId}\" resolves outside peers root`);\n }\n return candidate;\n}\n\n/**\n * Reject the peers root if it is itself a symlink. Called BEFORE any\n * `fs.mkdir`, so a `peers → /tmp/outside` symlink can't get its\n * target mutated by a recursive mkdir before subsequent checks fire\n * (codex P2 + cursor M on PR #723).\n */\nasync function assertPeersRootNotSymlink(memoryDir: string): Promise<void> {\n const root = peersRoot(memoryDir);\n let rootStat: import(\"node:fs\").Stats | null = null;\n try {\n rootStat = await fs.lstat(root);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n if (rootStat && rootStat.isSymbolicLink()) {\n throw new Error(`peers root \"${root}\" is a symlink and is rejected`);\n }\n}\n\n/**\n * Codex P1 round 13: atomic mkdir of the peer directory under a\n * verified peers root. Opens the peers root with O_DIRECTORY|\n * O_NOFOLLOW first (creating it if missing under the same flags) so\n * a root-symlink swap can't redirect the subsequent mkdir to its\n * target. The dir handle is held across the mkdir, then we lstat\n * the candidate and reject if it's a symlink.\n */\nasync function mkdirPeerDirAtomic(memoryDir: string, peerId: string): Promise<void> {\n const root = peersRoot(memoryDir);\n // Ensure the root exists as a real directory, not a symlink. Use\n // mkdir + lstat: if mkdir succeeds (or EEXIST), lstat must report\n // a directory and not a symlink.\n await fs.mkdir(memoryDir, { recursive: true });\n try {\n await fs.mkdir(root);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n }\n const rootLstat = await fs.lstat(root);\n if (rootLstat.isSymbolicLink()) {\n throw new Error(`peers root \"${root}\" is a symlink and is rejected`);\n }\n if (!rootLstat.isDirectory()) {\n throw new Error(`peers root \"${root}\" exists but is not a directory`);\n }\n // Open the root with O_DIRECTORY|O_NOFOLLOW to anchor the inode.\n // Hold the handle across the peer-dir mkdir so a root swap\n // mid-operation is detected via fstat-vs-lstat compare afterward.\n const rootHandle = await fs.open(\n root,\n fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW,\n );\n try {\n const candidate = peerDir(memoryDir, peerId);\n await fs.mkdir(candidate, { recursive: true });\n // Verify root inode unchanged across the mkdir.\n const fstatRoot = await rootHandle.stat();\n const lstatRoot = await fs.lstat(root);\n if (fstatRoot.ino !== lstatRoot.ino || fstatRoot.dev !== lstatRoot.dev) {\n throw new Error(`peers root \"${root}\" was swapped during mkdir`);\n }\n // Reject symlinked peer dir.\n const peerLstat = await fs.lstat(candidate);\n if (peerLstat.isSymbolicLink()) {\n throw new Error(`peer directory \"${peerId}\" is a symlink and is rejected`);\n }\n } finally {\n await rootHandle.close();\n }\n}\n\n/**\n * Codex P1 on PR #723: `peerDir` only enforces a lexical\n * `path.relative` check, so a symlinked peer directory like\n * `peers/self → /tmp/outside` would slip through. Run this guard\n * AFTER the peer directory has been (or is known to) exist, so we\n * can lstat it and realpath-check containment. For first-time writes,\n * call `mkdirPeerDirAtomic` BEFORE this; for reads, this alone.\n */\nasync function assertPeerDirNotEscaped(memoryDir: string, peerId: string): Promise<void> {\n const candidate = peerDir(memoryDir, peerId);\n const root = peersRoot(memoryDir);\n // 1. The peers root must not be a symlink (defensive — writers\n // already checked this before mkdir, but reads must still verify).\n await assertPeersRootNotSymlink(memoryDir);\n // 2. lstat on the candidate itself. If it's a symlink, refuse.\n let candidateStat: import(\"node:fs\").Stats | null = null;\n try {\n candidateStat = await fs.lstat(candidate);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n if (candidateStat && candidateStat.isSymbolicLink()) {\n throw new Error(`peer directory \"${peerId}\" is a symlink and is rejected`);\n }\n // 3. Real-path containment. Only meaningful if the candidate exists.\n if (candidateStat) {\n const realRoot = await fs.realpath(root);\n const realCandidate = await fs.realpath(candidate);\n const rel = path.relative(realRoot, realCandidate);\n if (rel === \"\" || rel.startsWith(\"..\") || path.isAbsolute(rel)) {\n throw new Error(`peer directory \"${peerId}\" escapes the peers root`);\n }\n }\n}\n\nfunction identityPath(memoryDir: string, peerId: string): string {\n return path.join(peerDir(memoryDir, peerId), \"identity.md\");\n}\n\nfunction profilePath(memoryDir: string, peerId: string): string {\n return path.join(peerDir(memoryDir, peerId), \"profile.md\");\n}\n\nfunction interactionsPath(memoryDir: string, peerId: string): string {\n return path.join(peerDir(memoryDir, peerId), \"interactions.log.md\");\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Minimal YAML helpers (peer files only)\n// ──────────────────────────────────────────────────────────────────────\n//\n// We deliberately do not depend on the codebase's primary YAML parser\n// (`storage.ts`) because the peer schema is small and structured. We emit\n// a strict, predictable subset:\n//\n// ---\n// id: my-peer\n// kind: agent\n// displayName: \"Codex\"\n// createdAt: 2026-04-25T00:00:00.000Z\n// updatedAt: 2026-04-25T00:00:00.000Z\n// ---\n// {free-form markdown notes}\n//\n// String values are always double-quoted with `\\\\` and `\\\"` escapes. ISO\n// timestamps and the kind enum are emitted bare. This keeps round-trip\n// behaviour deterministic and easy to validate.\n\nfunction escapeYamlString(value: string): string {\n // Cursor Medium: must escape newlines / carriage returns / tabs so a\n // value like `displayName: \"first\\nsecond\"` doesn't blow up the\n // line-oriented parsePeerFrontmatter. Backslash → `\\\\`, double-quote\n // → `\\\"`, newline → `\\n`, carriage return → `\\r`, tab → `\\t`.\n return `\"${value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\")}\"`;\n}\n\nfunction unescapeYamlString(quoted: string): string {\n // Caller has already verified `quoted` starts and ends with a double quote.\n const body = quoted.slice(1, -1);\n let out = \"\";\n for (let i = 0; i < body.length; i++) {\n const ch = body[i];\n if (ch === \"\\\\\" && i + 1 < body.length) {\n const next = body[i + 1];\n if (next === \"\\\\\" || next === '\"') {\n out += next;\n i++;\n continue;\n }\n if (next === \"n\") {\n out += \"\\n\";\n i++;\n continue;\n }\n if (next === \"r\") {\n out += \"\\r\";\n i++;\n continue;\n }\n if (next === \"t\") {\n out += \"\\t\";\n i++;\n continue;\n }\n }\n out += ch;\n }\n return out;\n}\n\ninterface ParsedFrontmatter {\n fields: Record<string, string>;\n body: string;\n}\n\nfunction parsePeerFrontmatter(raw: string): ParsedFrontmatter {\n // Frontmatter must begin with a `---` line. We tolerate a leading BOM\n // and trailing newlines but otherwise require a strict, line-oriented\n // YAML subset of `key: value` pairs.\n const text = raw.replace(/^/, \"\");\n if (!text.startsWith(\"---\")) {\n throw new Error(\"peer file is missing YAML frontmatter delimiter\");\n }\n // Split on the first occurrence of `\\n---` after the leading `---`.\n const after = text.slice(3);\n const close = after.indexOf(\"\\n---\");\n if (close === -1) {\n throw new Error(\"peer file frontmatter is not terminated\");\n }\n const fmBlock = after.slice(0, close).replace(/^\\n/, \"\");\n const body = after.slice(close + 4).replace(/^\\n/, \"\");\n const fields: Record<string, string> = {};\n for (const lineRaw of fmBlock.split(\"\\n\")) {\n const line = lineRaw.trim();\n if (line === \"\" || line.startsWith(\"#\")) continue;\n const colon = line.indexOf(\":\");\n if (colon === -1) {\n throw new Error(`peer frontmatter line is malformed: ${line}`);\n }\n const key = line.slice(0, colon).trim();\n const valueRaw = line.slice(colon + 1).trim();\n if (key === \"\") {\n throw new Error(`peer frontmatter has empty key: ${line}`);\n }\n let value: string;\n if (valueRaw.startsWith('\"') && valueRaw.endsWith('\"') && valueRaw.length >= 2) {\n value = unescapeYamlString(valueRaw);\n } else {\n value = valueRaw;\n }\n fields[key] = value;\n }\n return { fields, body };\n}\n\nfunction emitPeerIdentity(peer: Peer): string {\n const lines: string[] = [\"---\"];\n lines.push(`id: ${escapeYamlString(peer.id)}`);\n lines.push(`kind: ${peer.kind}`);\n lines.push(`displayName: ${escapeYamlString(peer.displayName)}`);\n // Cursor M: emit timestamps as quoted YAML strings — bare emission\n // would let a `createdAt` value containing a newline inject extra\n // frontmatter fields when round-tripped through `parsePeerFrontmatter`.\n // (`writePeer` validates these are non-empty strings, but the type\n // doesn't constrain content.)\n lines.push(`createdAt: ${escapeYamlString(peer.createdAt)}`);\n lines.push(`updatedAt: ${escapeYamlString(peer.updatedAt)}`);\n lines.push(\"---\");\n lines.push(\"\");\n lines.push(peer.notes ?? \"\");\n // Trailing newline for POSIX friendliness.\n // CodeQL: the previous `replace(/\\n+$/, \"\\n\")` flagged as\n // polynomial-regex risk because `\\n+` can backtrack on long\n // trailing-newline runs. Strip trailing newlines with a bounded\n // loop instead — O(N) over the trailing-newline count, no regex.\n let out = lines.join(\"\\n\");\n while (out.endsWith(\"\\n\")) out = out.slice(0, -1);\n return out + \"\\n\";\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Public storage API\n// ──────────────────────────────────────────────────────────────────────\n\n/**\n * Read a peer's identity kernel.\n *\n * Returns `null` (does not throw) when the peer directory or identity\n * file does not exist. Throws on filesystem errors other than ENOENT and\n * on malformed files.\n */\nexport async function readPeer(\n memoryDir: string,\n peerId: string,\n): Promise<Peer | null> {\n assertValidPeerId(peerId);\n await assertPeerDirNotEscaped(memoryDir, peerId);\n const file = identityPath(memoryDir, peerId);\n // Codex P1 #2: even with the directory validated, a symlinked\n // identity.md inside a real peer dir would let us read arbitrary\n // out-of-scope files. Reject symlinks at the file level too.\n let raw: string;\n try {\n raw = await readFileNoFollow(file);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n const { fields, body } = parsePeerFrontmatter(raw);\n const id = fields.id ?? peerId;\n if (id !== peerId) {\n throw new Error(\n `peer identity file mismatch — expected id \"${peerId}\", file claims \"${id}\"`,\n );\n }\n const kind = fields.kind;\n assertValidKind(kind);\n const displayName = fields.displayName ?? \"\";\n const createdAt = fields.createdAt ?? \"\";\n if (createdAt === \"\") {\n throw new Error(`peer \"${peerId}\" is missing createdAt`);\n }\n // Codex P2: nullish-coalescing alone treated `updatedAt: \"\"` as a\n // valid empty timestamp, contradicting writePeer's non-empty\n // validation and the module's \"malformed files throw\" contract.\n // Treat empty string as missing and fall back to createdAt; throw\n // only when the field is malformed in some other way (caught by\n // the typeof check).\n const rawUpdatedAt = fields.updatedAt;\n const updatedAt =\n typeof rawUpdatedAt === \"string\" && rawUpdatedAt.length > 0\n ? rawUpdatedAt\n : createdAt;\n // Codex P2 + CodeQL: previously used `body.replace(/^\\s+/, \"\")`\n // which stripped ALL leading whitespace — including indentation in\n // notes. The `\\s+` patterns also flagged as polynomial-regex risk\n // (CodeQL alert #74) because they can backtrack on adversarial\n // inputs. Strip exactly one leading separator newline and one\n // trailing newline — internal AND user-authored leading\n // indentation are preserved verbatim, and the regex is bounded.\n let trimmedBody = body;\n if (trimmedBody.startsWith(\"\\r\\n\")) trimmedBody = trimmedBody.slice(2);\n else if (trimmedBody.startsWith(\"\\n\")) trimmedBody = trimmedBody.slice(1);\n if (trimmedBody.endsWith(\"\\r\\n\")) trimmedBody = trimmedBody.slice(0, -2);\n else if (trimmedBody.endsWith(\"\\n\")) trimmedBody = trimmedBody.slice(0, -1);\n return {\n id: peerId,\n kind,\n displayName,\n createdAt,\n updatedAt,\n notes: trimmedBody === \"\" ? undefined : trimmedBody,\n };\n}\n\n/**\n * Write (create or overwrite) a peer's identity kernel.\n *\n * Creates `peers/{id}/` if it does not exist. Does not touch the peer's\n * profile or interaction log. Atomic-write semantics are deferred to\n * later PRs — for the schema slice we simply write the file.\n */\nexport async function writePeer(memoryDir: string, peer: Peer): Promise<void> {\n assertValidPeerId(peer.id);\n assertValidKind(peer.kind);\n if (typeof peer.displayName !== \"string\") {\n throw new Error(\"peer.displayName must be a string\");\n }\n if (typeof peer.createdAt !== \"string\" || peer.createdAt === \"\") {\n throw new Error(\"peer.createdAt must be a non-empty ISO-8601 string\");\n }\n if (typeof peer.updatedAt !== \"string\" || peer.updatedAt === \"\") {\n throw new Error(\"peer.updatedAt must be a non-empty ISO-8601 string\");\n }\n // Codex P2 round 8: reject non-string `peer.notes`. Without this,\n // an untyped JS caller passing an object/number would silently\n // coerce to \"[object Object]\"/\"42\" via lines.push(notes ?? \"\")\n // and corrupt user-authored identity content. Notes are optional;\n // omit by passing undefined (NOT null).\n if (peer.notes !== undefined && typeof peer.notes !== \"string\") {\n throw new Error(\"peer.notes must be a string when provided\");\n }\n // Codex P1 round 13: atomic root validation. Open the peers root\n // (or its parent if peers doesn't exist yet) with O_DIRECTORY|\n // O_NOFOLLOW BEFORE the mkdir. This holds a kernel handle on the\n // original-inode root, so a swap between the symlink check and\n // the mkdir can't redirect mkdir to a symlink-target. We then\n // mkdir, lstat the result against the held handle, and only\n // proceed if they match.\n await mkdirPeerDirAtomic(memoryDir, peer.id);\n await assertPeerDirNotEscaped(memoryDir, peer.id);\n const file = identityPath(memoryDir, peer.id);\n // Codex P1 #2: reject if identity.md exists as a symlink so we\n // don't follow it on overwrite.\n await writeFileNoFollow(file, emitPeerIdentity(peer));\n}\n\n/**\n * Delete a peer's `identity.md` if present, applying the same symlink\n * and path-escape protections as the read/write paths.\n *\n * Returns `true` if a regular file was unlinked, `false` if no\n * `identity.md` existed at the time of the call. The peer directory\n * itself is left in place so any companion files (`profile.md`,\n * `interactions/`, etc.) are untouched. Idempotent: missing target\n * returns `false` rather than throwing.\n *\n * Cursor M (PR #756 review): a manual `path.join` + raw `fs.unlink`\n * bypasses `assertPeerDirNotEscaped`, the peers-root symlink check,\n * and the parent-inode-stable / lstat guards used by every other\n * peer I/O entrypoint. A symlinked `peers/<id>/` could redirect the\n * unlink to an arbitrary `identity.md` outside `memoryDir`. This\n * function consolidates the safe-delete contract so callers cannot\n * skip the guards.\n */\nexport async function deletePeer(memoryDir: string, peerId: string): Promise<boolean> {\n assertValidPeerId(peerId);\n await assertPeerDirNotEscaped(memoryDir, peerId);\n const file = identityPath(memoryDir, peerId);\n // Refuse to follow a symlink at `identity.md` itself: lstat first\n // and reject if the target is a symlink. Then verify the parent\n // directory inode is stable across the unlink (mirrors the\n // assertParentDirInodeStable + O_NOFOLLOW pattern used by\n // writeFileNoFollow). This narrows the TOCTOU window between the\n // lstat and the unlink to the same few microseconds the write path\n // accepts.\n let lstatBefore: import(\"node:fs\").Stats;\n try {\n lstatBefore = await fs.lstat(file);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return false;\n }\n throw err;\n }\n if (lstatBefore.isSymbolicLink()) {\n throw new Error(`refusing to unlink \"${file}\": target is a symlink`);\n }\n if (!lstatBefore.isFile()) {\n throw new Error(`refusing to unlink \"${file}\": target is not a regular file`);\n }\n await assertParentDirInodeStable(file);\n try {\n await fs.unlink(file);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return false;\n }\n throw err;\n }\n return true;\n}\n\n/**\n * Destructively purge the entire peer directory for a given peerId —\n * `identity.md`, `profile.md`, `interactions.log.md`, and any other\n * files written by future extensions.\n *\n * This is the DESTRUCTIVE counterpart to `deletePeer` (which only\n * removes `identity.md`). Callers must pass `confirm: \"yes\"` to prevent\n * accidental data loss — the function throws `Error(\"forgetPeer requires\n * confirm: 'yes'\")` when the flag is absent or wrong.\n *\n * Idempotent: if the peer directory does not exist the function returns\n * `{ purged: false }` rather than throwing. Safe to run twice.\n *\n * Security contract (mirrors `deletePeer`):\n * - `peerId` is validated against `PEER_ID_PATTERN`.\n * - The peers root is checked for symlink swap (assertPeersRootNotSymlink).\n * - `assertPeerDirNotEscaped` performs realpath containment: symlinked\n * peer directories are rejected and the resolved path is confirmed to\n * stay inside peersRoot (same guard used by every other I/O entry-point).\n * - A secondary `lstat` + `isSymbolicLink()` check is kept as defence-in-\n * depth before the `fs.rm` call itself.\n * - `fs.rm` with `{ recursive: true, force: true }` is used for the\n * actual removal so partially-populated directories are handled\n * atomically by the OS rather than file-by-file in userland.\n *\n * Returns:\n * `{ purged: true }` — directory existed and was removed.\n * `{ purged: false }` — directory did not exist (no-op).\n */\nexport async function forgetPeer(\n memoryDir: string,\n peerId: string,\n opts: { confirm: string },\n): Promise<{ purged: boolean }> {\n if (opts.confirm !== \"yes\") {\n throw new Error(\"forgetPeer requires confirm: 'yes'\");\n }\n assertValidPeerId(peerId);\n await assertPeersRootNotSymlink(memoryDir);\n // assertPeerDirNotEscaped performs the realpath-containment check that\n // the lexical `peerDir()` guard alone cannot provide: it lstat-checks\n // the candidate, rejects symlinks, and confirms the resolved path stays\n // inside peersRoot. Mirrors the contract of every other I/O entry-point\n // in this module (`readPeer`, `writePeer`, `deletePeer`,\n // `appendInteractionLog`, `readPeerProfile`, `writePeerProfile`).\n // The function handles ENOENT gracefully (returns without throwing), so\n // calling it here is safe for the idempotent no-op path as well.\n await assertPeerDirNotEscaped(memoryDir, peerId);\n\n const dir = peerDir(memoryDir, peerId);\n\n // lstat the candidate directory. Return early (idempotent) if it\n // doesn't exist. Reject if it resolves to a symlink.\n let dirStat: import(\"node:fs\").Stats;\n try {\n dirStat = await fs.lstat(dir);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { purged: false };\n }\n throw err;\n }\n if (dirStat.isSymbolicLink()) {\n throw new Error(\n `refusing to purge peer directory \"${peerId}\": target is a symlink`,\n );\n }\n if (!dirStat.isDirectory()) {\n throw new Error(\n `refusing to purge peer directory \"${peerId}\": target is not a directory`,\n );\n }\n\n // Perform the recursive removal. `fs.rm` with `{ recursive: true }`\n // is the correct API (replaces the deprecated `fs.rmdir`). The\n // `force: true` flag makes it a no-op if the directory was already\n // removed between our lstat and this call (race-safe idempotency).\n await fs.rm(dir, { recursive: true, force: true });\n return { purged: true };\n}\n\n/**\n * Enumerate all peers under `memoryDir/peers/`.\n *\n * Returns an empty array if the peers root does not exist. Subdirectories\n * whose name fails `PEER_ID_PATTERN` are skipped (defensive: the user\n * may have hand-edited the directory). Directories that exist but lack\n * `identity.md` are also skipped.\n */\nexport async function listPeers(memoryDir: string): Promise<Peer[]> {\n // Codex P2 round 13: atomic readdir against the root. Open the\n // peers directory with O_DIRECTORY|O_NOFOLLOW first so we hold the\n // original-inode handle, then readdir from that handle. A\n // root-symlink swap between assertPeersRootNotSymlink and a path-\n // based readdir is no longer possible — the kernel rejects the\n // open if the path resolves to a symlink, and the dirHandle.read()\n // operates on the original inode regardless of subsequent path-\n // based mutations.\n const root = peersRoot(memoryDir);\n // Cursor M round 14: Node's FileHandle has no `readdir` method\n // (only `fs.readdir(path)` is exposed), so we can't read directly\n // from the handle. Instead anchor the original-inode by opening\n // the dir with O_NOFOLLOW, fstat it, then do path-based readdir\n // and confirm the path's lstat AFTER matches the held inode. If\n // it diverges, a swap happened — abort. This is a fence around\n // the readdir, not the kernel-level guarantee a true `fdreaddir`\n // would give, but it closes the path-traversal race without\n // requiring native bindings.\n let entries: string[];\n let dh: import(\"node:fs/promises\").FileHandle | null = null;\n try {\n dh = await fs.open(\n root,\n fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW,\n );\n const fstatBefore = await dh.stat();\n entries = await fs.readdir(root);\n const lstatAfter = await fs.lstat(root);\n if (\n fstatBefore.ino !== lstatAfter.ino ||\n fstatBefore.dev !== lstatAfter.dev ||\n lstatAfter.isSymbolicLink()\n ) {\n throw new Error(`peers root \"${root}\" was swapped during readdir`);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return [];\n }\n throw err;\n } finally {\n if (dh) await dh.close();\n }\n const peers: Peer[] = [];\n // Sort for deterministic ordering — callers that need a different\n // sort order can re-sort the result.\n entries.sort();\n for (const name of entries) {\n if (!PEER_ID_PATTERN.test(name) || name.length > PEER_ID_MAX_LENGTH) {\n continue;\n }\n let stat;\n try {\n // Codex P1: use `lstat` so we don't follow symlinks. A\n // `peers/<valid-id>` symlink pointing at an arbitrary directory\n // would otherwise let listPeers (and the readPeer that\n // follows) traverse outside the peers root.\n stat = await fs.lstat(path.join(root, name));\n } catch (err) {\n // Codex P2 round 13: don't swallow non-ENOENT errors.\n // Permission-denied / EACCES / EIO need to surface so the\n // operator sees a real I/O problem rather than a silently\n // truncated peer list.\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") continue;\n throw err;\n }\n // Skip symlinks entirely — only real directories are peers.\n if (!stat.isDirectory() || stat.isSymbolicLink()) continue;\n let peer: Peer | null = null;\n try {\n peer = await readPeer(memoryDir, name);\n } catch (err) {\n // Cursor M round 14: classifying by `.code` alone treated\n // security-check failures (assertPeerDirNotEscaped /\n // assertParentDirInodeStable / \"is a symlink and is rejected\"\n // / \"inode mismatch\") as parse failures and silently skipped\n // them. Those messages are critical signals — an attacker\n // attempted to redirect a read outside the peers root. Match\n // by message prefix to detect them and propagate.\n const code = (err as NodeJS.ErrnoException).code;\n const message = err instanceof Error ? err.message : String(err);\n const isSecurityFailure =\n message.startsWith(\"peers root\") ||\n message.startsWith(\"peer directory\") ||\n message.startsWith(\"parent directory\") ||\n message.startsWith(\"path \") /* \"path \\\"...\\\" is a symlink\" */ ||\n message.includes(\"escapes the peers root\") ||\n message.includes(\"inode mismatch\") ||\n message.includes(\"is a symlink and is rejected\") ||\n message.includes(\"was swapped\");\n if (isSecurityFailure) throw err;\n // Real I/O errors (EACCES, EIO, EBUSY, etc.) propagate too.\n if (code && code !== \"ENOENT\") throw err;\n // Schema/parse failures fall through and skip the entry.\n continue;\n }\n if (peer !== null) {\n peers.push(peer);\n }\n }\n return peers;\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Interaction log (append-only)\n// ──────────────────────────────────────────────────────────────────────\n\nfunction sanitizeLogField(value: string): string {\n // Cursor Medium: every interaction-log field — not just summary —\n // must collapse newlines so a malicious or buggy `timestamp` /\n // `kind` / `sessionId` value can't break the one-line-per-entry\n // invariant the append-only log relies on. Replace CR/LF/Tab with\n // a single space; trim leading/trailing whitespace.\n return value.replace(/[\\r\\n\\t]+/g, \" \").trim();\n}\n\nfunction formatLogEntry(entry: PeerInteractionLogEntry): string {\n // One line per entry. We use a leading bullet so the file remains\n // readable as ordinary markdown. Order: timestamp, kind, optional\n // session id, summary. ALL fields are passed through `sanitizeLogField`\n // so a stray newline anywhere can't shatter the append-only invariant\n // (cursor Medium on PR #723).\n //\n // Cursor Low on PR #736: the previous bare `session=<id>` token was\n // ambiguous with summaries that literally start with `session=`\n // (e.g. summary `\"session=foo bar\"` round-tripped to\n // `{sessionId: \"foo\", summary: \"bar\"}`). New entries wrap the\n // session marker in square brackets — `[session=<id>]` — which a\n // sanitized summary can never start with (sanitizeLogField never\n // emits `[` as the first character of a summary that originally\n // started with `session=`, and bracketed metadata is unambiguously\n // distinct from `session=` text). Old entries written by previous\n // code remain parseable through the legacy fallback in\n // `parseLogLine`.\n const ts = sanitizeLogField(entry.timestamp);\n const kind = sanitizeLogField(entry.kind);\n const summary = sanitizeLogField(entry.summary);\n const session = entry.sessionId\n ? ` [session=${sanitizeLogField(entry.sessionId)}]`\n : \"\";\n return `- [${ts}] (${kind})${session} ${summary}`;\n}\n\n/**\n * Append one entry to a peer's interaction log.\n *\n * Creates `peers/{id}/` and `interactions.log.md` if needed. The file is\n * append-only by contract — this helper never rewrites prior entries.\n * Returns the absolute path of the log file (useful for tests).\n */\nexport async function appendInteractionLog(\n memoryDir: string,\n peerId: string,\n entry: PeerInteractionLogEntry,\n): Promise<string> {\n assertValidPeerId(peerId);\n // Codex P2 round 10: trim() before the empty check matches what\n // sanitizeLogField does at format time. A whitespace-only\n // `timestamp` or `kind` would previously pass validation and then\n // be normalized to \"\" later, producing an entry like `- [] ()`\n // that breaks downstream parsers.\n if (typeof entry.timestamp !== \"string\" || entry.timestamp.trim() === \"\") {\n throw new Error(\"interaction entry must have a non-whitespace timestamp\");\n }\n if (typeof entry.kind !== \"string\" || entry.kind.trim() === \"\") {\n throw new Error(\"interaction entry must have a non-whitespace kind\");\n }\n if (typeof entry.summary !== \"string\") {\n throw new Error(\"interaction entry must have a string summary\");\n }\n // Codex P2 round 6/8/9: optional sessionId must be a non-empty\n // string OR strictly `undefined` (omitted). null and empty string\n // are both rejected explicitly: the previous behavior silently\n // dropped them during formatting, which reinterprets invalid input\n // instead of failing fast like other validators in this module.\n // Codex P2 round 12: trim() before the empty check matches what\n // sanitizeLogField does at format time. Whitespace-only sessionId\n // would otherwise pass validation and produce a `session=` token\n // with an empty value that breaks downstream log parsers.\n if (entry.sessionId !== undefined) {\n if (typeof entry.sessionId !== \"string\" || entry.sessionId.trim() === \"\") {\n throw new Error(\"interaction entry sessionId must be a non-whitespace string when provided\");\n }\n }\n await mkdirPeerDirAtomic(memoryDir, peerId);\n await assertPeerDirNotEscaped(memoryDir, peerId);\n const file = interactionsPath(memoryDir, peerId);\n const line = formatLogEntry(entry) + \"\\n\";\n // `appendFile` creates the file if it does not exist. POSIX guarantees\n // writes < PIPE_BUF are atomic; entries on this path are well under\n // that bound. Ordering across concurrent writers is the caller's\n // responsibility for now — the reasoner runs serially in PR 2/5.\n await appendFileNoFollow(file, line);\n return file;\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Profile read/write (schema scaffold; reasoner ships in PR 2/5)\n// ──────────────────────────────────────────────────────────────────────\n\ninterface ProfileFile {\n updatedAt: string;\n fields: Record<string, string>;\n provenance: Record<string, PeerProfileFieldProvenance[]>;\n}\n\nfunction emitPeerProfile(profile: PeerProfile): string {\n // Profiles use a JSON-in-fenced-code-block payload inside a markdown\n // file so they remain human-readable. The frontmatter holds the\n // updatedAt stamp; the body holds the full structured payload.\n const payload: ProfileFile = {\n updatedAt: profile.updatedAt,\n fields: { ...profile.fields },\n provenance: Object.fromEntries(\n Object.entries(profile.provenance).map(([k, v]) => [k, [...v]]),\n ),\n };\n const json = JSON.stringify(payload, null, 2);\n return [\n \"---\",\n `peerId: ${escapeYamlString(profile.peerId)}`,\n `updatedAt: ${escapeYamlString(profile.updatedAt)}`,\n \"---\",\n \"\",\n \"<!-- peer profile — managed by the async reasoner. Manual edits will be overwritten. -->\",\n \"\",\n \"```json\",\n json,\n \"```\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction parsePeerProfile(raw: string, peerId: string): PeerProfile {\n const { fields: fm, body } = parsePeerFrontmatter(raw);\n if (fm.peerId !== undefined && fm.peerId !== peerId) {\n throw new Error(\n `peer profile mismatch — expected \"${peerId}\", file claims \"${fm.peerId}\"`,\n );\n }\n const fenceMatch = body.match(/```json\\s*\\n([\\s\\S]*?)\\n```/);\n if (!fenceMatch) {\n throw new Error(`peer profile for \"${peerId}\" is missing JSON payload`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(fenceMatch[1]);\n } catch (err) {\n throw new Error(\n `peer profile for \"${peerId}\" has invalid JSON: ${(err as Error).message}`,\n );\n }\n if (typeof parsed !== \"object\" || parsed === null) {\n throw new Error(`peer profile for \"${peerId}\" is not an object`);\n }\n const payload = parsed as Partial<ProfileFile>;\n // Codex P2: only accept string updatedAt values. A malformed payload\n // like `{ \"updatedAt\": 123 }` would previously short-circuit through\n // `payload.updatedAt ?? fm.updatedAt ?? \"\"` and produce a `PeerProfile`\n // whose updatedAt is a number — corrupting any downstream code that\n // assumes the contract.\n const payloadUpdatedAt = typeof payload.updatedAt === \"string\" ? payload.updatedAt : undefined;\n const updatedAt = payloadUpdatedAt ?? fm.updatedAt ?? \"\";\n if (typeof updatedAt !== \"string\" || updatedAt === \"\") {\n throw new Error(`peer profile for \"${peerId}\" is missing updatedAt`);\n }\n // Codex P2 round 10: a malformed `fields: \"wat\"` or\n // `provenance: 42` previously coerced to {} and silently dropped\n // the section. That contradicts the \"malformed files throw\"\n // contract — the file IS malformed, not just empty. Reject loudly.\n // `undefined` is still tolerated (an older profile file might omit\n // the section entirely).\n let fieldsObj: object;\n if (payload.fields === undefined) {\n fieldsObj = {};\n } else if (\n typeof payload.fields === \"object\" &&\n payload.fields !== null &&\n !Array.isArray(payload.fields)\n ) {\n fieldsObj = payload.fields;\n } else {\n throw new Error(`peer profile for \"${peerId}\" has malformed fields section`);\n }\n let provenanceObj: object;\n if (payload.provenance === undefined) {\n provenanceObj = {};\n } else if (\n typeof payload.provenance === \"object\" &&\n payload.provenance !== null &&\n !Array.isArray(payload.provenance)\n ) {\n provenanceObj = payload.provenance;\n } else {\n throw new Error(`peer profile for \"${peerId}\" has malformed provenance section`);\n }\n // Coerce values defensively. We never trust the on-disk shape.\n // Codex P1: skip prototype-pollution keys explicitly. We don't use\n // null-prototype objects in the returned shape because callers\n // (tests, downstream consumers) expect plain objects with normal\n // semantics — the assertion `assert.deepEqual` differentiates by\n // prototype. The skip-list is the load-bearing defense; iteration\n // via Object.entries() of attacker-controlled JSON objects is safe\n // as long as we never assign through dangerous keys.\n const DANGEROUS_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n const fields: Record<string, string> = {};\n for (const [k, v] of Object.entries(fieldsObj)) {\n if (DANGEROUS_KEYS.has(k)) continue;\n if (typeof v === \"string\") fields[k] = v;\n }\n const provenance: Record<string, PeerProfileFieldProvenance[]> = {};\n for (const [k, v] of Object.entries(provenanceObj)) {\n if (DANGEROUS_KEYS.has(k)) continue;\n if (!Array.isArray(v)) continue;\n const list: PeerProfileFieldProvenance[] = [];\n for (const item of v) {\n if (\n typeof item !== \"object\" ||\n item === null ||\n Array.isArray(item)\n ) {\n continue;\n }\n const r = item as unknown as Record<string, unknown>;\n // Codex P2 round 9: empty observedAt/signal strings should be\n // treated as malformed, not valid. Drop the entry.\n if (typeof r.observedAt !== \"string\" || r.observedAt === \"\") continue;\n if (typeof r.signal !== \"string\" || r.signal === \"\") continue;\n // Codex P2 round 6: previously the optional fields were never\n // type-checked, so a hand-edited `{sourceSessionId: 123}`\n // survived and corrupted the PeerProfileFieldProvenance contract.\n // Build a clean record with only string-typed optional fields.\n const clean: PeerProfileFieldProvenance = {\n observedAt: r.observedAt,\n signal: r.signal,\n ...(typeof r.sourceSessionId === \"string\" && r.sourceSessionId.length > 0\n ? { sourceSessionId: r.sourceSessionId }\n : {}),\n ...(typeof r.note === \"string\" && r.note.length > 0\n ? { note: r.note }\n : {}),\n };\n list.push(clean);\n }\n provenance[k] = list;\n }\n return { peerId, updatedAt, fields, provenance };\n}\n\n/**\n * Read a peer's profile. Returns null if the profile file does not exist.\n *\n * The PR-1 surface only ships the structured read/write so the reasoner\n * (PR 2/5) and recall integration (PR 3/5) have a stable target. We do\n * not yet expose any field-update helpers.\n */\nexport async function readPeerProfile(\n memoryDir: string,\n peerId: string,\n): Promise<PeerProfile | null> {\n assertValidPeerId(peerId);\n await assertPeerDirNotEscaped(memoryDir, peerId);\n const file = profilePath(memoryDir, peerId);\n let raw: string;\n try {\n raw = await readFileNoFollow(file);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n return parsePeerProfile(raw, peerId);\n}\n\n/**\n * Write (create or overwrite) a peer's profile.\n */\nexport async function writePeerProfile(\n memoryDir: string,\n profile: PeerProfile,\n): Promise<void> {\n assertValidPeerId(profile.peerId);\n if (typeof profile.updatedAt !== \"string\" || profile.updatedAt === \"\") {\n throw new Error(\"profile.updatedAt must be a non-empty ISO-8601 string\");\n }\n // Codex P2 round 6: validate the nested payload shape on write so\n // round-trip semantics are preserved. Without this, untyped JS\n // callers can persist non-string field values or malformed\n // provenance entries — readPeerProfile silently scrubs them on\n // the way back, so data is lost without an error. Fail fast at\n // the boundary instead.\n if (!isPlainObject(profile.fields)) {\n throw new Error(\"profile.fields must be a plain object\");\n }\n // Codex P2 round 11: parsePeerProfile drops \"__proto__\" /\n // \"constructor\" / \"prototype\" keys on read. Without symmetric\n // rejection on write, those keys silently disappear during\n // round-trip — failing the contract that what writes succeeds\n // also reads back. Reject at the boundary so JS callers learn\n // immediately that those keys aren't allowed.\n const RESERVED_KEYS: ReadonlySet<string> = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n for (const [key, value] of Object.entries(profile.fields)) {\n if (RESERVED_KEYS.has(key)) {\n throw new Error(`profile.fields key \"${key}\" is reserved and cannot be persisted`);\n }\n if (typeof value !== \"string\") {\n throw new Error(`profile.fields[\"${key}\"] must be a string`);\n }\n }\n if (!isPlainObject(profile.provenance)) {\n throw new Error(\"profile.provenance must be a plain object\");\n }\n for (const [key, list] of Object.entries(profile.provenance)) {\n if (RESERVED_KEYS.has(key)) {\n throw new Error(`profile.provenance key \"${key}\" is reserved and cannot be persisted`);\n }\n if (!Array.isArray(list)) {\n throw new Error(`profile.provenance[\"${key}\"] must be an array`);\n }\n for (let i = 0; i < list.length; i++) {\n const item = list[i] as unknown;\n if (typeof item !== \"object\" || item === null || Array.isArray(item)) {\n throw new Error(`profile.provenance[\"${key}\"][${i}] must be an object`);\n }\n const r = item as Record<string, unknown>;\n if (typeof r.observedAt !== \"string\" || r.observedAt === \"\") {\n throw new Error(`profile.provenance[\"${key}\"][${i}].observedAt must be a non-empty string`);\n }\n if (typeof r.signal !== \"string\" || r.signal === \"\") {\n throw new Error(`profile.provenance[\"${key}\"][${i}].signal must be a non-empty string`);\n }\n // Cursor M round 9: empty optional strings would round-trip lose\n // (parsePeerProfile drops them on read). Reject at the boundary\n // for consistency with other validators in this module.\n if (r.sourceSessionId !== undefined) {\n if (typeof r.sourceSessionId !== \"string\" || r.sourceSessionId === \"\") {\n throw new Error(`profile.provenance[\"${key}\"][${i}].sourceSessionId must be a non-empty string when provided`);\n }\n }\n if (r.note !== undefined) {\n if (typeof r.note !== \"string\" || r.note === \"\") {\n throw new Error(`profile.provenance[\"${key}\"][${i}].note must be a non-empty string when provided`);\n }\n }\n }\n }\n await mkdirPeerDirAtomic(memoryDir, profile.peerId);\n await assertPeerDirNotEscaped(memoryDir, profile.peerId);\n const file = profilePath(memoryDir, profile.peerId);\n await writeFileNoFollow(file, emitPeerProfile(profile));\n}\n\n/**\n * Read the raw interaction log for a peer.\n *\n * Returns the empty string if the log does not yet exist. Callers parse\n * the log themselves — this PR does not ship structured log parsing.\n * Exposed primarily so tests can verify monotonic append semantics.\n */\nexport async function readInteractionLogRaw(\n memoryDir: string,\n peerId: string,\n): Promise<string> {\n assertValidPeerId(peerId);\n await assertPeerDirNotEscaped(memoryDir, peerId);\n const file = interactionsPath(memoryDir, peerId);\n try {\n return await readFileNoFollow(file);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return \"\";\n }\n throw err;\n }\n}\n\n/**\n * Inverse of `formatLogEntry`. Used by `readPeerInteractionLog` to\n * convert the on-disk one-line bullet form back into a structured\n * `PeerInteractionLogEntry`. Returns `null` for malformed lines so\n * callers can keep the rest of the log even when a single entry was\n * hand-edited or partially written.\n *\n * Formats accepted (must match `formatLogEntry`):\n *\n * - [TS] (KIND) [session=SID] SUMMARY // canonical (PR #736)\n * - [TS] (KIND) SUMMARY // session optional\n *\n * The unbracketed `session=SID` form (which would have been written\n * by an earlier draft of #679 PR 2/5) is intentionally NOT parsed —\n * the cursor #736 finding showed it was indistinguishable from a\n * summary that legitimately starts with `session=`. Since #679 PR\n * 2/5 is the first shipped writer for `sessionId` AND ships the\n * bracketed canonical form simultaneously, there is no real legacy\n * data on disk to support. A summary like `session=foo bar` thus\n * round-trips verbatim into `summary` rather than being mis-claimed\n * as a session id.\n *\n * Whitespace inside SUMMARY is preserved verbatim. The parser is\n * deliberately strict — anything that doesn't start with `- [` is\n * rejected and dropped (the file is also markdown-friendly, so blank\n * lines and stray prose are simply ignored).\n */\nfunction parseLogLine(line: string): PeerInteractionLogEntry | null {\n if (!line.startsWith(\"- [\")) return null;\n const tsClose = line.indexOf(\"]\", 3);\n if (tsClose === -1) return null;\n const timestamp = line.slice(3, tsClose).trim();\n if (timestamp === \"\") return null;\n // Skip optional whitespace between `]` and `(`.\n let cursor = tsClose + 1;\n while (cursor < line.length && line[cursor] === \" \") cursor += 1;\n if (line[cursor] !== \"(\") return null;\n const kindOpen = cursor;\n const kindClose = line.indexOf(\")\", kindOpen + 1);\n if (kindClose === -1) return null;\n const kind = line.slice(kindOpen + 1, kindClose).trim();\n if (kind === \"\") return null;\n cursor = kindClose + 1;\n let sessionId: string | undefined;\n // Optional session id token. We tolerate any leading whitespace\n // count; `formatLogEntry` always emits exactly one space, but\n // operator-edited logs may diverge.\n while (cursor < line.length && line[cursor] === \" \") cursor += 1;\n // Canonical form (PR #736): `[session=<id>]`. Unambiguous because\n // a sanitized summary cannot start with `[session=` followed by a\n // closing bracket and a space — sanitization preserves user content\n // verbatim except for newlines/CR/tab, so a summary that literally\n // begins `[session=foo]` would imply the OPERATOR wrote a real\n // session marker, which is acceptable as session attribution.\n if (line.startsWith(\"[session=\", cursor)) {\n const close = line.indexOf(\"]\", cursor + \"[session=\".length);\n // A `[session=...]` with no closing bracket on the same line is\n // malformed metadata; treat the whole tail as summary instead of\n // misclaiming a session id.\n if (close > -1) {\n const sid = line.slice(cursor + \"[session=\".length, close).trim();\n if (sid.length > 0) sessionId = sid;\n cursor = close + 1;\n }\n }\n // NOTE: the legacy unbracketed `session=<id>` form is intentionally\n // NOT parsed. The cursor #736 finding is fundamentally a format\n // ambiguity: `- [TS] (KIND) session=foo bar` is indistinguishable\n // from a real session token vs. a summary that begins with the\n // literal text `session=foo bar`. Since #679 PR 2/5 is the first\n // PR that writes `sessionId` to the log AND ships the bracketed\n // canonical form simultaneously, there is no production legacy\n // data to support. Old `session=`-style summaries — both real\n // operator notes and hypothetical legacy entries — round-trip\n // verbatim into `summary` rather than being silently mis-claimed\n // as a session id.\n // Remaining tail is the summary (possibly empty if the log was\n // hand-edited; we still accept \"\" because `formatLogEntry` itself\n // accepts an empty summary string).\n while (cursor < line.length && line[cursor] === \" \") cursor += 1;\n const summary = line.slice(cursor);\n return sessionId === undefined\n ? { timestamp, kind, summary }\n : { timestamp, kind, sessionId, summary };\n}\n\n/**\n * Read the structured interaction log for a peer.\n *\n * Returns an empty array when the log file does not exist. Each line\n * is parsed via `parseLogLine`; malformed lines are silently skipped\n * so the reasoner can still derive profile fields from a partially\n * corrupt log rather than aborting the whole pass.\n *\n * `options.limit` (when > 0) restricts the result to the most recent\n * N entries. `options.afterTimestamp` (ISO-8601) filters out entries\n * with `timestamp < afterTimestamp` (string compare is safe for\n * canonical ISO-8601 form). Both filters are applied in order:\n * timestamp first, then limit.\n *\n * Order: oldest → newest, matching the append-only on-disk order so\n * downstream consumers can reason about temporal evolution without\n * re-sorting.\n */\nexport async function readPeerInteractionLog(\n memoryDir: string,\n peerId: string,\n options: { limit?: number; afterTimestamp?: string } = {},\n): Promise<PeerInteractionLogEntry[]> {\n const raw = await readInteractionLogRaw(memoryDir, peerId);\n if (raw === \"\") return [];\n const entries: PeerInteractionLogEntry[] = [];\n // Split on bare newlines; logs are written with a trailing `\\n` per\n // entry by `appendInteractionLog`, so the final element after split\n // is typically the empty string. Both `\\r\\n` and `\\n` are tolerated.\n for (const lineRaw of raw.split(/\\r?\\n/)) {\n const line = lineRaw.trimEnd();\n if (line === \"\") continue;\n const parsed = parseLogLine(line);\n if (parsed === null) continue;\n entries.push(parsed);\n }\n let filtered = entries;\n if (typeof options.afterTimestamp === \"string\" && options.afterTimestamp.length > 0) {\n const cutoff = options.afterTimestamp;\n filtered = filtered.filter((e) => e.timestamp > cutoff);\n }\n // Gotcha #27: guard slice(-n) against n === 0 — `slice(-0)` returns\n // the entire array. Treat 0 and negatives as \"no limit applied\".\n if (typeof options.limit === \"number\" && options.limit > 0) {\n if (filtered.length > options.limit) {\n filtered = filtered.slice(filtered.length - options.limit);\n }\n }\n return filtered;\n}\n","/**\n * Peer registry types — issue #679 PR 1/5.\n *\n * Generalizes the singular identity-anchor model to a multi-peer registry.\n * Every party Remnic interacts with — humans, agents, integrations, and\n * \"self\" — is represented as a `Peer` with an evolving cognitive profile.\n *\n * This module defines pure types only. Storage primitives live in\n * `./storage.ts`. Reasoner integration, recall injection, CLI/HTTP/MCP\n * surfaces, and migration of existing identity-anchor data ship in later\n * PRs (2/5 — 5/5).\n */\n\n/**\n * Kind of peer.\n *\n * - `self` — the current Remnic operator (replaces singular identity-anchor).\n * - `human` — another human collaborator distinct from self.\n * - `agent` — another AI agent (Claude Code, Codex, Hermes, etc.).\n * - `integration` — non-conversational integration (cron, webhook, importer).\n */\nexport type PeerKind = \"self\" | \"human\" | \"agent\" | \"integration\";\n\n/**\n * Stable, slow-changing facts about a peer.\n *\n * `id` matches `^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$`, 1-64 chars,\n * with no leading/trailing/consecutive dots or dashes. Stored on disk under\n * `peers/{id}/identity.md` as YAML frontmatter + markdown body.\n */\nexport interface Peer {\n /** Stable, opaque identifier. See PEER_ID_PATTERN. */\n readonly id: string;\n /** Kind of peer. Drives default profile schema and recall posture. */\n readonly kind: PeerKind;\n /** Human-readable display name. Distinct from `id`; mutable. */\n readonly displayName: string;\n /** ISO-8601 timestamp of first registration. */\n readonly createdAt: string;\n /** ISO-8601 timestamp of most recent update to identity. */\n readonly updatedAt: string;\n /** Optional free-form markdown body for the identity kernel. */\n readonly notes?: string;\n}\n\n/**\n * Evolving cognitive profile for a peer.\n *\n * Updated by the async profile reasoner (PR 2/5) from observable session\n * signals. Every field carries provenance back to its originating session\n * and signal. This PR only defines the shape — population is deferred.\n */\nexport interface PeerProfile {\n /** Peer this profile belongs to. */\n readonly peerId: string;\n /** ISO-8601 timestamp of most recent profile mutation. */\n readonly updatedAt: string;\n /**\n * Arbitrary key/value profile fields. Values are markdown strings.\n * Keys are stable section identifiers (e.g. `communication_style`,\n * `recurring_concerns`). The reasoner is responsible for choosing\n * keys; this PR does not constrain them beyond requiring strings.\n */\n readonly fields: Record<string, string>;\n /**\n * Per-field provenance. Maps field key → list of provenance entries.\n * A field may have multiple sources (the reasoner accumulates evidence\n * across sessions before promoting a field).\n */\n readonly provenance: Record<string, ReadonlyArray<PeerProfileFieldProvenance>>;\n}\n\n/**\n * Provenance for a single profile-field mutation.\n *\n * Reasoner output (PR 2/5) attaches one of these every time it touches a\n * field, so the user can audit exactly why a profile claim exists.\n */\nexport interface PeerProfileFieldProvenance {\n /** ISO-8601 timestamp the field was set/updated by this signal. */\n readonly observedAt: string;\n /** Originating session id (or other source identifier). */\n readonly sourceSessionId?: string;\n /** Short label for the signal type (e.g. \"explicit_preference\"). */\n readonly signal: string;\n /** Optional free-form note explaining the inference. */\n readonly note?: string;\n}\n\n/**\n * One row of the append-only interaction log for a peer.\n *\n * Stored on disk under `peers/{id}/interactions.log.md` as a sequence of\n * markdown bullet entries with a leading ISO-8601 timestamp. Append-only\n * by contract — the reasoner reads this log to derive profile updates.\n */\nexport interface PeerInteractionLogEntry {\n /** ISO-8601 timestamp the interaction occurred. */\n readonly timestamp: string;\n /** Originating session id, if any. */\n readonly sessionId?: string;\n /** Short kind label (e.g. \"message\", \"tool_call\", \"preference_set\"). */\n readonly kind: string;\n /** Free-form markdown summary of the interaction. */\n readonly summary: string;\n}\n\n/**\n * Regex enforced on `Peer.id`. Exported so callers can mirror validation\n * before constructing a `Peer`.\n *\n * Rules:\n * - 1-64 characters total\n * - First and last character must be `[A-Za-z0-9]`\n * - Interior may contain `.`, `_`, `-` in addition to alphanumerics\n * - No leading or trailing dot/dash/underscore\n * - No consecutive separators (`..`, `--`, `__`, `.-`, etc.)\n *\n * Cursor Medium: previously the regex allowed `a..b` even though the\n * docs claimed otherwise — a separate JS-side check enforced the rule\n * but the standalone PATTERN was wrong for any external consumer\n * relying on it. Tighten the regex itself so PEER_ID_PATTERN is the\n * single source of truth: an alphanumeric, optionally followed by\n * groups of (one separator + one-or-more alphanumerics), with the\n * final group ending on an alphanumeric. Negative lookahead-free so\n * it works in any JS engine.\n */\n// CodeQL alert #75: the previous form `^[A-Za-z0-9](?:[._-]?[A-Za-z0-9]+)*$`\n// was ambiguous on alphanumeric runs because `[._-]?` could match\n// empty, letting each iteration consume 1+ alphanumerics in many\n// overlapping ways — exponential backtracking on adversarial inputs.\n// Tighten so the optional group REQUIRES a separator before each\n// subsequent alphanumeric run; no overlapping match paths, linear\n// time on any input.\nexport const PEER_ID_PATTERN = /^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/;\n\n/** Maximum length for `Peer.id`. */\nexport const PEER_ID_MAX_LENGTH = 64;\n"],"mappings":"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","names":["fs","fsConstants","path","path","fs","fsConstants"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|