@remnic/core 1.1.7 → 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-B9sziJdP.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-LKJA5MR2.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-AKV3HOGZ.js → chunk-3VRIIII5.js} +150 -2
- 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-ZOMA7J3J.js → chunk-6OAQEOGV.js} +2 -2
- package/dist/{chunk-FSWYMUWI.js → chunk-6Z6UH6TK.js} +38 -12
- package/dist/chunk-6Z6UH6TK.js.map +1 -0
- package/dist/{chunk-EONJ7GK3.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-M3QQ5DRA.js → chunk-A6PGANSE.js} +3 -3
- package/dist/{chunk-KUIEFH2S.js → chunk-BIHCWSWA.js} +3 -3
- package/dist/{chunk-PT2EZWOH.js → chunk-CTYRIJ5E.js} +3 -3
- package/dist/{chunk-COAGZQT7.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-VZNQB6NL.js → chunk-FPWUENQH.js} +39 -35
- 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-RXGR3YLU.js → chunk-G3G3LY22.js} +2 -2
- package/dist/{chunk-ZBZVNWQO.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-D7WYTVUQ.js → chunk-ICULSMDG.js} +2 -2
- package/dist/{chunk-5NS6NN5A.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-YELFQNQH.js → chunk-KMWZXT5T.js} +2 -2
- package/dist/{chunk-YKGRACQP.js → chunk-M3DK45UM.js} +5 -5
- package/dist/{chunk-DLYTYJ43.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-7RAW2T4P.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-MYMOXFMR.js → chunk-PHQH2VUO.js} +4 -4
- package/dist/{chunk-OZAFME7S.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-AIT53NLG.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-KQB4C4OE.js → chunk-XL3UCAZA.js} +22 -22
- package/dist/{chunk-ASIQZXYO.js → chunk-XMVFHBHT.js} +2 -2
- package/dist/{chunk-6B23Z44B.js → chunk-XN4D6Z7X.js} +3 -3
- package/dist/{chunk-S5SQDIF5.js → chunk-Y3VT6ZCP.js} +4 -4
- package/dist/{cli-olNPi1uN.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-D5d5vvyS.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-2JKWFKJV.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-7MI7KE35.js → memory-governance-AAQPBZEP.js} +7 -7
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/{memory-projection-store-BQt5VUQ8.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-D2lHhFWI.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-5W-r5SKc.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-tDODR2je.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-C-USTTAx.d.ts → types-Bmp9ssU2.d.ts} +4 -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-7RAW2T4P.js.map +0 -1
- package/dist/chunk-AKV3HOGZ.js.map +0 -1
- package/dist/chunk-COAGZQT7.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-FSWYMUWI.js.map +0 -1
- package/dist/chunk-GZCUW5IC.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-R2XRID2N.js.map +0 -1
- package/dist/chunk-TUFG6VXY.js.map +0 -1
- package/dist/chunk-VZNQB6NL.js.map +0 -1
- package/dist/chunk-ZBZVNWQO.js.map +0 -1
- /package/dist/{chunk-LKJA5MR2.js.map → chunk-2MVUXO4H.js.map} +0 -0
- /package/dist/{chunk-ZOMA7J3J.js.map → chunk-6OAQEOGV.js.map} +0 -0
- /package/dist/{chunk-EONJ7GK3.js.map → chunk-7SFAENUZ.js.map} +0 -0
- /package/dist/{chunk-M3QQ5DRA.js.map → chunk-A6PGANSE.js.map} +0 -0
- /package/dist/{chunk-KUIEFH2S.js.map → chunk-BIHCWSWA.js.map} +0 -0
- /package/dist/{chunk-PT2EZWOH.js.map → chunk-CTYRIJ5E.js.map} +0 -0
- /package/dist/{chunk-RXGR3YLU.js.map → chunk-G3G3LY22.js.map} +0 -0
- /package/dist/{chunk-3FPTCC3Z.js.map → chunk-GVPWB7EY.js.map} +0 -0
- /package/dist/{chunk-D7WYTVUQ.js.map → chunk-ICULSMDG.js.map} +0 -0
- /package/dist/{chunk-5NS6NN5A.js.map → chunk-J3P6WSFZ.js.map} +0 -0
- /package/dist/{chunk-YELFQNQH.js.map → chunk-KMWZXT5T.js.map} +0 -0
- /package/dist/{chunk-YKGRACQP.js.map → chunk-M3DK45UM.js.map} +0 -0
- /package/dist/{chunk-DLYTYJ43.js.map → chunk-MJLUHRSF.js.map} +0 -0
- /package/dist/{chunk-MYMOXFMR.js.map → chunk-PHQH2VUO.js.map} +0 -0
- /package/dist/{chunk-OZAFME7S.js.map → chunk-QPLYTPYL.js.map} +0 -0
- /package/dist/{chunk-3LCWFNVS.js.map → chunk-SKE7JYKA.js.map} +0 -0
- /package/dist/{chunk-AIT53NLG.js.map → chunk-U4SZXGEO.js.map} +0 -0
- /package/dist/{chunk-KQB4C4OE.js.map → chunk-XL3UCAZA.js.map} +0 -0
- /package/dist/{chunk-ASIQZXYO.js.map → chunk-XMVFHBHT.js.map} +0 -0
- /package/dist/{chunk-6B23Z44B.js.map → chunk-XN4D6Z7X.js.map} +0 -0
- /package/dist/{chunk-S5SQDIF5.js.map → chunk-Y3VT6ZCP.js.map} +0 -0
- /package/dist/{engine-2JKWFKJV.js.map → engine-EDFFOWDD.js.map} +0 -0
- /package/dist/{memory-governance-7MI7KE35.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/local-llm.ts"],"sourcesContent":["import { log } from \"./logger.js\";\nimport type { PluginConfig } from \"./types.js\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport type { ModelRegistry } from \"./model-registry.js\";\nimport { launchProcessSync } from \"./runtime/child-process.js\";\nimport { mergeEnv, readEnvVar } from \"./runtime/env.js\";\n\n/** Trim trailing slash characters without backtracking regex. */\nfunction trimTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s[end - 1] === \"/\") end--;\n return s.substring(0, end);\n}\n\n/**\n * Local LLM client for OpenAI-compatible endpoints (LM Studio, Ollama, MLX, etc.)\n *\n * Based on openclaw-tactician's provider detection patterns for consistency.\n * Provides privacy-preserving, cost-effective LLM operations with\n * graceful fallback to cloud providers when local LLM is unavailable.\n */\nexport type LocalLlmType = \"lmstudio\" | \"ollama\" | \"mlx\" | \"vllm\" | \"generic\";\n\n/**\n * Backends known to honor `chat_template_kwargs: { enable_thinking: false }`\n * on OpenAI-compatible `/v1/chat/completions`. LM Studio and vLLM both\n * forward this field to the jinja chat template, where thinking-capable\n * models (Qwen 3.5, Gemma 4, DeepSeek) suppress reasoning tokens.\n *\n * Strict OpenAI-compatible backends (standard OpenAI, Azure OpenAI, some\n * proxies) reject unknown request fields with 400 — which trips the\n * `localLlm400*` cooldown path. `LocalLlmClient` therefore only injects\n * the kwarg when the detected backend is in this set; unknown / `generic`\n * / `ollama` / `mlx` fail open (no injection, no 400 risk). Issue #548.\n */\nconst THINKING_COMPATIBLE_BACKENDS: ReadonlySet<LocalLlmType> = new Set([\n \"lmstudio\",\n \"vllm\",\n]);\n\ninterface LocalServerConfig {\n type: LocalLlmType;\n defaultPort: number;\n healthEndpoint: string;\n modelsEndpoint: string;\n detectFn: (response: unknown) => boolean;\n}\n\nconst LOCAL_SERVERS: LocalServerConfig[] = [\n {\n type: \"ollama\",\n defaultPort: 11434,\n healthEndpoint: \"/\",\n modelsEndpoint: \"/api/tags\",\n detectFn: (resp) => typeof resp === \"string\" && resp.includes(\"Ollama\"),\n },\n {\n type: \"mlx\",\n defaultPort: 8080,\n healthEndpoint: \"/v1/models\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) =>\n typeof resp === \"object\" &&\n resp !== null &&\n \"data\" in resp &&\n Array.isArray((resp as { data: unknown[] }).data),\n },\n {\n type: \"lmstudio\",\n defaultPort: 1234,\n healthEndpoint: \"/v1/models\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) =>\n typeof resp === \"object\" &&\n resp !== null &&\n \"data\" in resp &&\n Array.isArray((resp as { data: unknown[] }).data),\n },\n {\n type: \"vllm\",\n defaultPort: 8000,\n healthEndpoint: \"/health\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) => resp === \"\" || (typeof resp === \"object\" && resp !== null),\n },\n];\n\nexport interface LocalModelInfo {\n id: string;\n contextWindow?: number;\n maxTokens?: number;\n}\n\nexport type LocalLlmRequestPriority = \"recall-critical\" | \"background\";\n\ninterface LocalLlmChatCompletionOptions {\n temperature?: number;\n maxTokens?: number;\n responseFormat?: { type: string };\n timeoutMs?: number;\n operation?: string;\n priority?: LocalLlmRequestPriority;\n}\n\ninterface LocalLlmQueuedRequest {\n messages: Array<{ role: string; content: string }>;\n options: LocalLlmChatCompletionOptions;\n priority: LocalLlmRequestPriority;\n enqueuedAtMs: number;\n resolve: (value: LocalLlmChatCompletionResult | null) => void;\n}\n\ninterface LocalLlmChatCompletionResult {\n content: string;\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number };\n}\n\nconst LOCAL_LLM_GLOBAL_BACKEND_STATE = \"__openclawEngramLocalLlmBackendState\";\n\ntype LocalLlmBackendState = {\n untilMs: number;\n reason: string;\n};\nexport class LocalLlmClient {\n private config: PluginConfig;\n private isAvailable: boolean | null = null;\n private lastHealthCheck: number = 0;\n private detectedType: LocalLlmType | null = null;\n private cachedModelInfo: LocalModelInfo | null = null;\n private cachedLmsContext: number | null = null;\n private lastLmsCheck: number = 0;\n private consecutive400s: number = 0;\n private cooldownUntilMs: number = 0;\n private modelRegistry?: ModelRegistry;\n private _disableThinking: boolean = false;\n private readonly requestQueues: Record<LocalLlmRequestPriority, LocalLlmQueuedRequest[]> = {\n \"recall-critical\": [],\n background: [],\n };\n private readonly queueProcessing = new Set<LocalLlmRequestPriority>();\n private queueDrainScheduled: boolean = false;\n private static readonly HEALTH_CHECK_INTERVAL_MS = 60000; // 1 minute\n private static readonly LMS_CACHE_INTERVAL_MS = 30000; // 30 seconds\n\n constructor(config: PluginConfig, modelRegistry?: ModelRegistry) {\n this.config = config;\n this.modelRegistry = modelRegistry;\n }\n\n /**\n * Request thinking/reasoning suppression on the next chat completion.\n *\n * When `true`, the client will inject\n * `chat_template_kwargs: { enable_thinking: false }` into the request\n * body — **but only when the detected backend is known to support it**\n * (LM Studio, vLLM; see `THINKING_COMPATIBLE_BACKENDS`). Strict\n * OpenAI-compat backends reject unknown fields with 400; on those the\n * client fails open (thinking runs normally). This is the safe\n * default for Remnic extraction / consolidation: measurable latency\n * win on thinking-capable backends, zero risk on others. Issue #548.\n */\n set disableThinking(value: boolean) {\n this._disableThinking = value;\n }\n\n private resolveHomeDir(): string {\n return this.config.localLlmHomeDir || readEnvVar(\"HOME\") || os.homedir();\n }\n\n private buildRequestHeaders(base: Record<string, string> = {}): Record<string, string> {\n const headers: Record<string, string> = {\n ...base,\n ...(this.config.localLlmHeaders ?? {}),\n };\n if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {\n headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;\n }\n return headers;\n }\n\n private isAbortError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const maybe = err as { name?: string; message?: string };\n return (\n maybe.name === \"AbortError\" ||\n maybe.message === \"This operation was aborted\" ||\n maybe.message === \"The operation was aborted\"\n );\n }\n\n /**\n * Set the ModelRegistry for caching detected capabilities\n */\n setModelRegistry(registry: ModelRegistry): void {\n this.modelRegistry = registry;\n }\n\n /**\n * Get the detected server type (null if not detected)\n */\n getDetectedType(): LocalLlmType | null {\n return this.detectedType;\n }\n\n private getBackendKey(): string {\n return trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n ).replace(/\\/v1$/, \"\");\n }\n\n private getGlobalBackendState(): Map<string, LocalLlmBackendState> {\n const globalAny = globalThis as typeof globalThis & {\n [LOCAL_LLM_GLOBAL_BACKEND_STATE]?: Map<string, LocalLlmBackendState>;\n };\n if (!globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE]) {\n globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE] = new Map();\n }\n return globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE];\n }\n\n private getTrippedBackendState(now: number): LocalLlmBackendState | null {\n const state = this.getGlobalBackendState().get(this.getBackendKey()) ?? null;\n if (!state) return null;\n if (state.untilMs <= now) {\n this.getGlobalBackendState().delete(this.getBackendKey());\n this.lastHealthCheck = 0;\n return null;\n }\n return state;\n }\n\n private markBackendUnavailable(reason: string, durationMs: number): void {\n const normalizedReason = this.normalizeBackendTripReason(reason);\n if (durationMs > 0) {\n const untilMs = Date.now() + durationMs;\n this.getGlobalBackendState().set(this.getBackendKey(), { untilMs, reason: normalizedReason });\n } else {\n this.getGlobalBackendState().delete(this.getBackendKey());\n }\n this.isAvailable = false;\n this.lastHealthCheck = 0;\n log.warn(\n `local LLM backend unavailable for ${durationMs}ms: model=${this.config.localLlmModel} reason=${normalizedReason}`,\n );\n }\n\n private extractNonRecoverableBackendReason(reason: string): string | null {\n const match = reason.match(\n /Failed to load model|Library not loaded|different Team IDs|code signature|llm_engine_mlx_amphibian/i,\n );\n return match?.[0] ?? null;\n }\n\n private extractNonRecoverableBackendReasonFromErrorText(errorText: string): string | null {\n const directReason = this.extractNonRecoverableBackendReason(errorText);\n if (directReason) return directReason;\n try {\n const parsed = JSON.parse(errorText) as { error?: { message?: string } };\n return this.extractNonRecoverableBackendReason(parsed?.error?.message ?? \"\");\n } catch {\n return null;\n }\n }\n\n private normalizeBackendTripReason(reason: string): string {\n const cleaned = reason.replace(/\\s+/g, \" \").replace(/^[-:–—\\s]+/, \"\").trim();\n if (!cleaned) return \"unknown local backend failure\";\n return cleaned.length > 160 ? `${cleaned.slice(0, 157)}...` : cleaned;\n }\n\n /**\n * Fetch with timeout for health checks\n */\n private async fetchWithTimeout(\n url: string,\n timeoutMs: number = 2000,\n headers?: Record<string, string>,\n ): Promise<{ ok: boolean; data: unknown; status: number | null }> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n headers: this.buildRequestHeaders({ Accept: \"application/json\", ...(headers ?? {}) }),\n });\n clearTimeout(timeout);\n\n if (!response.ok) {\n return { ok: false, data: null, status: response.status };\n }\n\n const contentType = response.headers.get(\"content-type\");\n if (contentType?.includes(\"application/json\")) {\n return { ok: true, data: await response.json(), status: response.status };\n } else {\n return { ok: true, data: await response.text(), status: response.status };\n }\n } catch (err) {\n clearTimeout(timeout);\n return { ok: false, data: null, status: null };\n }\n }\n\n /**\n * Check if local LLM is available\n * Uses 127.0.0.1 instead of localhost to avoid DNS issues (consistent with tactician)\n */\n async checkAvailability(): Promise<boolean> {\n // Cache health check results for 1 minute\n const now = Date.now();\n const trippedState = this.getTrippedBackendState(now);\n if (trippedState) {\n this.isAvailable = false;\n this.lastHealthCheck = 0;\n log.info(\n `local LLM availability: backend circuit open for ${Math.max(0, trippedState.untilMs - now)}ms (${trippedState.reason})`,\n );\n return false;\n }\n if (this.isAvailable !== null && now - this.lastHealthCheck < LocalLlmClient.HEALTH_CHECK_INTERVAL_MS) {\n return this.isAvailable;\n }\n\n // Normalize URL - replace localhost with 127.0.0.1, remove trailing slashes\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n let sawUnauthorizedProbe = false;\n\n // Try to detect which server type is running\n for (const serverConfig of LOCAL_SERVERS) {\n const healthUrl = `${baseUrl}${serverConfig.healthEndpoint}`;\n log.debug(`checking ${serverConfig.type} at ${healthUrl}`);\n\n const result = await this.fetchWithTimeout(healthUrl);\n if (result.ok && serverConfig.detectFn(result.data)) {\n this.isAvailable = true;\n this.detectedType = serverConfig.type;\n this.lastHealthCheck = now;\n log.info(`detected ${serverConfig.type} at ${baseUrl}`);\n return true;\n }\n if (result.status === 401 || result.status === 403) {\n sawUnauthorizedProbe = true;\n }\n }\n\n // Generic check if specific detection failed\n try {\n const modelsUrl = `${baseUrl}/v1/models`;\n const result = await this.fetchWithTimeout(modelsUrl);\n if (result.ok) {\n this.isAvailable = true;\n this.detectedType = \"generic\";\n this.lastHealthCheck = now;\n log.info(`detected generic OpenAI-compatible server at ${baseUrl}`);\n return true;\n }\n if (result.status === 401 || result.status === 403) {\n sawUnauthorizedProbe = true;\n }\n } catch {\n // Fall through to unavailable\n }\n\n this.isAvailable = false;\n this.detectedType = null;\n this.lastHealthCheck = now;\n if (sawUnauthorizedProbe) {\n log.warn(\n `local LLM availability probe was unauthorized at ${baseUrl}; verify localLlmApiKey and localLlmAuthHeader settings`,\n );\n }\n log.debug(\"local LLM not available at\", baseUrl);\n return false;\n }\n\n /**\n * Try to get context window from LM Studio settings.json as fallback.\n * This reads the defaultContextLength setting which is what LM Studio uses\n * when loading models without explicit context configuration.\n */\n private getContextFromLmStudioSettings(): number | null {\n try {\n const homeDir = this.resolveHomeDir();\n const settingsPath = `${homeDir}/.cache/lm-studio/settings.json`;\n\n if (!existsSync(settingsPath)) {\n log.debug(`LM Studio settings: file not found at ${settingsPath}`);\n return null;\n }\n\n const content = readFileSync(settingsPath, \"utf-8\");\n const settings = JSON.parse(content) as {\n defaultContextLength?: {\n type?: string;\n value?: number;\n };\n };\n\n if (settings.defaultContextLength?.value) {\n const contextWindow = settings.defaultContextLength.value;\n log.debug(`LM Studio settings: found default context length: ${contextWindow}`);\n return contextWindow;\n }\n\n return null;\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n log.debug(`LM Studio settings: failed to read - ${errorMsg}`);\n return null;\n }\n }\n\n /**\n * Try to get context window from LMS CLI (LM Studio specific).\n * Uses --json flag for reliable parsing.\n * Returns null if LMS CLI is not available or model not found.\n */\n private getContextFromLmsCli(modelId: string): number | null {\n try {\n // Check if lms CLI exists in common locations.\n // HOME may be absent in launchd environments, so prefer the resolved helper.\n const homeDir = this.resolveHomeDir();\n const lmsPaths = [\n this.config.localLmsCliPath || \"\",\n `${homeDir}/.cache/lm-studio/bin/lms`,\n \"/usr/local/bin/lms\",\n \"/opt/homebrew/bin/lms\",\n ];\n\n const lmsPath = lmsPaths.find((p) => p.length > 0 && existsSync(p));\n if (!lmsPath) {\n log.debug(`LMS CLI: not found in standard locations (checked: ${lmsPaths.join(\", \")})`);\n return null;\n }\n\n // Run lms ps --json to get loaded models with context\n // Use spawnSync with shell and explicit PATH to ensure lms can find its dependencies\n log.debug(`LMS CLI: running: ${lmsPath} ps --json`);\n const existingPath = readEnvVar(\"PATH\") || \"\";\n const result = launchProcessSync(lmsPath, [\"ps\", \"--json\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n shell: false, // Don't use shell for JSON output - more reliable\n env: mergeEnv({\n PATH: `${this.config.localLmsBinDir || `${homeDir}/.cache/lm-studio/bin`}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${existingPath}`,\n HOME: homeDir,\n }),\n });\n\n if (result.error) {\n log.debug(`LMS CLI: spawn error - ${result.error.message}`);\n return null;\n }\n\n if (result.stderr && result.stderr.trim()) {\n log.debug(`LMS CLI: stderr - ${result.stderr.slice(0, 200)}`);\n }\n\n const output = result.stdout || \"\";\n if (!output.trim()) {\n log.debug(\"LMS CLI: empty output - LM Studio may not be running or no models loaded\");\n return null;\n }\n\n // Parse JSON output\n let models: Array<{\n identifier?: string;\n modelKey?: string;\n contextLength?: number;\n maxContextLength?: number;\n }>;\n\n try {\n models = JSON.parse(output) as typeof models;\n } catch (parseErr) {\n log.debug(`LMS CLI: JSON parse error - ${parseErr}`);\n return null;\n }\n\n if (!Array.isArray(models) || models.length === 0) {\n log.debug(\"LMS CLI: no models loaded\");\n return null;\n }\n\n // Find the model matching our configured model ID\n const model = models.find((m) =>\n m.identifier === modelId ||\n m.modelKey === modelId ||\n (m.identifier?.includes(modelId.replace(/@\\d+bit$/, \"\")))\n );\n\n if (!model) {\n log.debug(`LMS CLI: model \"${modelId}\" not found in loaded models: ${models.map(m => m.identifier).join(\", \")}`);\n return null;\n }\n\n // Use contextLength (actual configured) or fall back to maxContextLength (model max)\n const contextWindow = model.contextLength || model.maxContextLength;\n\n if (contextWindow) {\n log.info(`LMS CLI detected context window: ${contextWindow} for ${modelId} (max: ${model.maxContextLength})`);\n return contextWindow;\n }\n\n return null;\n } catch (err) {\n // LMS CLI not available or failed\n const errorMsg = err instanceof Error ? err.message : String(err);\n log.debug(`LMS CLI: failed - ${errorMsg}`);\n return null;\n }\n }\n\n /**\n * Get full model info from LMS CLI including context length and max context length.\n * Returns null if LMS CLI is unavailable or model not found.\n */\n private getLmsModelInfo(modelId: string): { contextLength: number; maxContextLength: number; identifier: string } | null {\n try {\n const result = launchProcessSync(\"lms\", [\"ps\", \"--json\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n shell: false,\n });\n\n if (result.error) {\n return null;\n }\n\n const output = result.stdout || \"\";\n if (!output.trim()) {\n return null;\n }\n\n let models: Array<{\n identifier?: string;\n modelKey?: string;\n contextLength?: number;\n maxContextLength?: number;\n }>;\n\n try {\n models = JSON.parse(output) as typeof models;\n } catch {\n return null;\n }\n\n if (!Array.isArray(models) || models.length === 0) {\n return null;\n }\n\n const model = models.find((m) =>\n m.identifier === modelId ||\n m.modelKey === modelId ||\n (m.identifier?.includes(modelId.replace(/@\\d+bit$/, \"\")))\n );\n\n if (!model || !model.contextLength) {\n return null;\n }\n\n return {\n contextLength: model.contextLength,\n maxContextLength: model.maxContextLength || model.contextLength,\n identifier: model.identifier || modelId,\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Get context window for the configured model, using cache if available.\n * This method caches the result to avoid repeated LMS CLI calls.\n * Order: ModelRegistry (persistent) -> memory cache -> LMS CLI -> settings.json\n */\n getCachedContextWindow(modelId: string): number | null {\n const now = Date.now();\n\n // 1. Check ModelRegistry for persisted context window\n if (this.modelRegistry) {\n const caps = this.modelRegistry.getCapabilities(modelId);\n if (caps.source === \"lmstudio\" && caps.contextWindow) {\n log.debug(`ModelRegistry: using persisted LM Studio context: ${caps.contextWindow}`);\n // Also update memory cache\n this.cachedLmsContext = caps.contextWindow;\n this.lastLmsCheck = now;\n return caps.contextWindow;\n }\n }\n\n // 2. Return in-memory cached value if still valid\n if (this.cachedLmsContext && now - this.lastLmsCheck < LocalLlmClient.LMS_CACHE_INTERVAL_MS) {\n log.debug(`LMS CLI: returning in-memory cached context: ${this.cachedLmsContext}`);\n return this.cachedLmsContext;\n }\n\n // 3. Try LMS CLI (authoritative source)\n const lmsInfo = this.getLmsModelInfo(modelId);\n if (lmsInfo?.contextLength) {\n this.cachedLmsContext = lmsInfo.contextLength;\n this.lastLmsCheck = now;\n // Calculate appropriate output tokens based on context size\n // Use 12.5% of context window, capped at 16K (generous but safe)\n const calculatedOutputTokens = Math.min(Math.floor(lmsInfo.contextLength / 8), 16384);\n const outputTokens = Math.max(calculatedOutputTokens, 4096); // Minimum 4K\n // Persist to ModelRegistry with detected capabilities\n if (this.modelRegistry) {\n this.modelRegistry.setCapabilities(modelId, {\n maxPositionEmbeddings: lmsInfo.maxContextLength || lmsInfo.contextLength,\n contextWindow: lmsInfo.contextLength,\n supportsExtendedContext: (lmsInfo.maxContextLength || lmsInfo.contextLength) > 65536,\n typicalOutputTokens: outputTokens,\n source: \"lmstudio\",\n });\n log.info(`LMS CLI: Stored capabilities for ${modelId}: ${lmsInfo.contextLength} context, ${outputTokens} output tokens`);\n }\n return lmsInfo.contextLength;\n }\n\n // Legacy: Try LMS CLI context only (fallback)\n const legacyContext = this.getContextFromLmsCli(modelId);\n if (legacyContext) {\n this.cachedLmsContext = legacyContext;\n this.lastLmsCheck = now;\n // Persist to ModelRegistry with calculated output tokens\n if (this.modelRegistry) {\n const calculatedOutputTokens = Math.min(Math.floor(legacyContext / 8), 16384);\n const outputTokens = Math.max(calculatedOutputTokens, 4096);\n this.modelRegistry.setCapabilities(modelId, {\n maxPositionEmbeddings: legacyContext,\n contextWindow: legacyContext,\n supportsExtendedContext: false,\n typicalOutputTokens: outputTokens,\n source: \"lmstudio\",\n });\n }\n return legacyContext;\n }\n\n // 4. Fall back to LM Studio settings.json\n const settingsContext = this.getContextFromLmStudioSettings();\n if (settingsContext) {\n log.info(`LM Studio settings: using default context: ${settingsContext}`);\n this.cachedLmsContext = settingsContext;\n this.lastLmsCheck = now;\n return settingsContext;\n }\n\n return null;\n }\n\n /**\n * Clear the LMS context cache. Call this when the model changes.\n */\n clearContextCache(): void {\n this.cachedLmsContext = null;\n this.lastLmsCheck = 0;\n log.debug(\"LMS CLI: context cache cleared\");\n }\n\n private remainingCooldownMs(now: number = Date.now()): number {\n return Math.max(0, this.cooldownUntilMs - now);\n }\n\n private scheduleQueueDrain(): void {\n if (this.queueDrainScheduled) return;\n this.queueDrainScheduled = true;\n\n queueMicrotask(() => {\n this.queueDrainScheduled = false;\n this.startAvailableQueuedRequests();\n });\n }\n\n private hasQueuedRequests(): boolean {\n return (\n this.requestQueues[\"recall-critical\"].length > 0 ||\n this.requestQueues.background.length > 0\n );\n }\n\n private dequeueQueuedRequest(priority: LocalLlmRequestPriority): LocalLlmQueuedRequest | null {\n const next = this.requestQueues[priority].shift();\n return next ?? null;\n }\n\n private failOpenQueuedRequestsForCooldown(): number {\n let dropped = 0;\n for (const priority of [\"recall-critical\", \"background\"] as const) {\n while (this.requestQueues[priority].length > 0) {\n const queued = this.requestQueues[priority].shift();\n queued?.resolve(null);\n dropped += 1;\n }\n }\n return dropped;\n }\n\n private startAvailableQueuedRequests(): void {\n if (!this.queueProcessing.has(\"recall-critical\")) {\n const nextCritical = this.dequeueQueuedRequest(\"recall-critical\");\n if (nextCritical) {\n this.queueProcessing.add(\"recall-critical\");\n void this.runQueuedRequest(nextCritical);\n }\n }\n\n if (!this.queueProcessing.has(\"background\")) {\n const nextBackground = this.dequeueQueuedRequest(\"background\");\n if (nextBackground) {\n this.queueProcessing.add(\"background\");\n void this.runQueuedRequest(nextBackground);\n }\n }\n }\n\n private async runQueuedRequest(next: LocalLlmQueuedRequest): Promise<void> {\n try {\n const remainingCooldownMs = this.remainingCooldownMs();\n if (remainingCooldownMs > 0) {\n const additionalDropped = this.failOpenQueuedRequestsForCooldown();\n log.warn(\n `local LLM: cooldown active (${remainingCooldownMs}ms remaining), dropping ${additionalDropped + 1} queued request(s) fail-open`,\n );\n next.resolve(null);\n return;\n }\n\n let result: LocalLlmChatCompletionResult | null = null;\n try {\n result = await this.runChatCompletionRequest(next.messages, next.options, {\n priority: next.priority,\n enqueuedAtMs: next.enqueuedAtMs,\n });\n } catch (err) {\n log.warn(`local LLM queue drain failed open: ${err instanceof Error ? err.message : String(err)}`);\n }\n next.resolve(result);\n } finally {\n this.queueProcessing.delete(next.priority);\n if (this.hasQueuedRequests()) {\n this.scheduleQueueDrain();\n }\n }\n }\n\n private async runChatCompletionRequest(\n messages: Array<{ role: string; content: string }>,\n options: LocalLlmChatCompletionOptions,\n queueMeta?: { priority: LocalLlmRequestPriority; enqueuedAtMs: number },\n ): Promise<LocalLlmChatCompletionResult | null> {\n log.debug(\n `local LLM chatCompletion: localLlmEnabled=${this.config.localLlmEnabled}, model=${this.config.localLlmModel}`,\n );\n\n const operation = options.operation ?? \"unspecified\";\n const startedAtMs = Date.now();\n if (queueMeta) {\n log.debug(\n `local LLM queue start: priority=${queueMeta.priority} waitMs=${startedAtMs - queueMeta.enqueuedAtMs} op=${operation}`,\n );\n }\n\n try {\n const isAvailable = await this.checkAvailability();\n if (!isAvailable) {\n log.debug(\n `local LLM: checkAvailability returned false for ${this.config.localLlmUrl}`,\n );\n return null;\n }\n\n const promptChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);\n const requestBody: Record<string, unknown> = {\n model: this.config.localLlmModel,\n messages,\n temperature: options.temperature ?? 0.7,\n // Use max_tokens consistent with cloud models\n max_tokens: options.maxTokens ?? 4096,\n };\n\n // Skip response_format for local LLMs - they don't support json_object type\n // The prompts already instruct the model to output JSON\n // Only send if it's json_schema type which some local LLMs support\n if (options.responseFormat?.type === \"json_schema\") {\n requestBody.response_format = options.responseFormat;\n }\n\n // Suppress thinking/reasoning for thinking-capable models\n // (Qwen 3.5, Gemma 4, DeepSeek). These models default to\n // thinking-on via their chat template; sending\n // `chat_template_kwargs: { enable_thinking: false }` tells the\n // template to skip reasoning tokens.\n //\n // Gate the injection on detected backend support (issue #548,\n // Codex P1 on PR #550): `chat_template_kwargs` is an LM Studio /\n // vLLM / llama.cpp extension, not part of standard OpenAI chat\n // completions. Strict OpenAI-compatible backends reject\n // unknown fields with 400, which trips the 400-cooldown path and\n // can effectively disable local extraction. Fail open when the\n // backend hasn't been positively identified as thinking-capable.\n if (\n this._disableThinking &&\n this.detectedType !== null &&\n THINKING_COMPATIBLE_BACKENDS.has(this.detectedType)\n ) {\n requestBody.chat_template_kwargs = { enable_thinking: false };\n }\n\n // Normalize URL (use 127.0.0.1 instead of localhost)\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n const chatUrl = baseUrl.endsWith(\"/v1\")\n ? `${baseUrl}/chat/completions`\n : `${baseUrl}/v1/chat/completions`;\n\n const requestBodyJson = JSON.stringify(requestBody);\n log.debug(\n `local LLM: sending request to ${chatUrl} with model ${this.config.localLlmModel}`,\n );\n // Avoid logging request bodies by default (can contain sensitive user content).\n log.debug(`local LLM: request body length=${requestBodyJson.length}`);\n\n // Write request body to file for debugging\n if (this.config.debug) {\n try {\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(\"/tmp/engram-last-request.json\", requestBodyJson);\n } catch {\n /* ignore */\n }\n }\n\n const effectiveTimeoutMs =\n typeof options.timeoutMs === \"number\"\n ? Math.min(this.config.localLlmTimeoutMs, options.timeoutMs)\n : this.config.localLlmTimeoutMs;\n const maxAttempts = 1 + Math.max(0, this.config.localLlmRetry5xxCount);\n let response: Response | null = null;\n let lastAbortError: Error | null = null;\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n const attemptAbort = new AbortController();\n const attemptTimeout = setTimeout(() => attemptAbort.abort(), effectiveTimeoutMs);\n try {\n response = await fetch(chatUrl, {\n method: \"POST\",\n headers: this.buildRequestHeaders({\n \"Content-Type\": \"application/json\",\n }),\n body: JSON.stringify(requestBody),\n signal: attemptAbort.signal,\n });\n } catch (err) {\n if (!this.isAbortError(err)) throw err;\n lastAbortError = err instanceof Error ? err : new Error(String(err));\n if (attempt < maxAttempts) {\n const backoffMs = this.config.localLlmRetryBackoffMs * attempt;\n log.warn(\n `local LLM request aborted: op=${operation} attempt=${attempt}/${maxAttempts} timeoutMs=${effectiveTimeoutMs} model=${this.config.localLlmModel}; retrying after ${backoffMs}ms`,\n );\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n continue;\n }\n break;\n } finally {\n clearTimeout(attemptTimeout);\n }\n\n if (response.ok) break;\n if (response.status >= 500 && attempt < maxAttempts) {\n try {\n const errorText = await response.clone().text();\n const nonRecoverableReason =\n this.extractNonRecoverableBackendReasonFromErrorText(errorText);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n this.consecutive400s = 0;\n return null;\n }\n } catch (e) {\n log.debug(`local LLM failed to inspect retryable error body: ${e}`);\n }\n }\n if (response.status < 500 || attempt >= maxAttempts) break;\n\n const backoffMs = this.config.localLlmRetryBackoffMs * attempt;\n log.warn(\n `local LLM request got ${response.status}; retrying (attempt ${attempt + 1}/${maxAttempts}) after ${backoffMs}ms`,\n );\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n }\n log.debug(\n `local LLM: received response, status=${response?.status}, ok=${response?.ok}`,\n );\n\n if (!response) {\n if (lastAbortError) {\n log.warn(\n `local LLM request aborted after ${maxAttempts} attempt(s): op=${operation} timeoutMs=${effectiveTimeoutMs} model=${this.config.localLlmModel} promptChars=${promptChars} durationMs=${Date.now() - startedAtMs}`,\n );\n } else {\n log.warn(\n `local LLM request failed: no response object (op=${operation} model=${this.config.localLlmModel} durationMs=${Date.now() - startedAtMs})`,\n );\n }\n return null;\n }\n\n if (!response.ok) {\n let reason = \"\";\n let errorText = \"\";\n try {\n errorText = await response.text();\n // Try to extract a stable error message without logging content.\n try {\n const parsed = JSON.parse(errorText) as { error?: { message?: string } };\n reason = parsed?.error?.message ? ` — ${parsed.error.message}` : \"\";\n } catch {\n // Keep a short preview in debug only.\n log.debug(`local LLM error body: ${errorText.slice(0, 500)}`);\n }\n } catch (e) {\n log.debug(`local LLM failed to read error body: ${e}`);\n }\n log.warn(\n `local LLM request failed: ${response.status} ${response.statusText}${reason} ` +\n `(op=${operation}, model=${this.config.localLlmModel}, url=${chatUrl}, promptChars=${promptChars}, maxTokens=${requestBody.max_tokens as number})`,\n );\n const nonRecoverableReason =\n this.extractNonRecoverableBackendReason(reason) ??\n this.extractNonRecoverableBackendReasonFromErrorText(errorText);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n this.consecutive400s = 0;\n return null;\n }\n if (response.status === 400) {\n this.consecutive400s += 1;\n if (this.consecutive400s >= this.config.localLlm400TripThreshold) {\n this.cooldownUntilMs = Date.now() + this.config.localLlm400CooldownMs;\n log.warn(\n `local LLM: entering cooldown for ${this.config.localLlm400CooldownMs}ms ` +\n `after ${this.consecutive400s} consecutive 400 responses`,\n );\n this.consecutive400s = 0;\n }\n } else {\n this.consecutive400s = 0;\n }\n return null;\n }\n this.consecutive400s = 0;\n\n const data = (await response.json()) as {\n choices?: Array<{\n message?: { content?: string; reasoning_content?: string };\n }>;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n };\n };\n\n log.debug(\n `local LLM response: choices=${data.choices?.length}, usage=${JSON.stringify(data.usage)}`,\n );\n\n // Thinking models (e.g. Qwen 3.5) may put their response in\n // `reasoning_content` and leave `content` empty. Fall back to\n // reasoning_content so engram still gets a usable result.\n const msg = data.choices?.[0]?.message;\n const content = msg?.content || msg?.reasoning_content || \"\";\n if (!content) {\n log.warn(`local LLM returned empty content. choices=${JSON.stringify(data.choices)?.slice(0, 200)}`);\n return null;\n }\n\n // Estimate tokens if not provided by local LLM\n const usage = data.usage\n ? {\n promptTokens: data.usage.prompt_tokens ?? 0,\n completionTokens: data.usage.completion_tokens ?? 0,\n totalTokens: data.usage.total_tokens ?? 0,\n }\n : this.estimateTokens(messages, content);\n\n const durationMs = Date.now() - startedAtMs;\n if (this.config.slowLogEnabled && durationMs >= this.config.slowLogThresholdMs) {\n const promptChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);\n const op = options.operation ? ` op=${options.operation}` : \"\";\n log.warn(\n `SLOW local LLM:${op} durationMs=${durationMs} model=${this.config.localLlmModel} url=${chatUrl} promptChars=${promptChars} outputTokens=${usage.completionTokens} totalTokens=${usage.totalTokens}`,\n );\n }\n\n log.debug(\"local LLM: request succeeded, tokens:\", usage.totalTokens);\n return { content, usage };\n } catch (err) {\n const errMsg = err instanceof Error ? err.message : String(err);\n const durationMs = Date.now() - startedAtMs;\n if (this.isAbortError(err)) {\n log.warn(\n `local LLM request aborted: op=${operation} timeoutMs=${options.timeoutMs ?? this.config.localLlmTimeoutMs} model=${this.config.localLlmModel} durationMs=${durationMs} error=${errMsg}`,\n );\n return null;\n }\n log.warn(`local LLM request error: op=${operation} error=${errMsg}`);\n this.isAvailable = false; // Mark as unavailable on non-abort errors\n const nonRecoverableReason = this.extractNonRecoverableBackendReason(errMsg);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n }\n return null;\n } finally {\n if (queueMeta) {\n const finishedAtMs = Date.now();\n const waitMs = startedAtMs - queueMeta.enqueuedAtMs;\n log.debug(\n `local LLM queue finish: priority=${queueMeta.priority} waitMs=${waitMs} runMs=${finishedAtMs - startedAtMs} totalMs=${finishedAtMs - queueMeta.enqueuedAtMs} op=${operation}`,\n );\n }\n }\n }\n\n /**\n * Query the local LLM server for loaded model information.\n * Returns null if unavailable or if the model is not found.\n */\n async getLoadedModelInfo(): Promise<LocalModelInfo | null> {\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n\n // Handle URL construction - localLlmUrl may already include /v1\n const modelsUrl = baseUrl.endsWith(\"/v1\")\n ? `${baseUrl}/models`\n : `${baseUrl}/v1/models`;\n log.debug(`Fetching model info from ${modelsUrl}`);\n\n try {\n const result = await this.fetchWithTimeout(modelsUrl, 3000);\n if (!result.ok) {\n if (result.status === 401 || result.status === 403) {\n log.warn(\n `Local LLM: unauthorized while fetching models from ${modelsUrl}; verify localLlmApiKey and localLlmAuthHeader settings`,\n );\n }\n log.warn(`Local LLM: Failed to fetch models from ${modelsUrl} - server returned error`);\n return null;\n }\n if (!result.data) {\n log.warn(`Local LLM: No data returned from ${modelsUrl}`);\n return null;\n }\n\n const data = result.data as {\n data?: Array<{\n id?: string;\n object?: string;\n owned_by?: string;\n // LM Studio specific fields\n max_context_length?: number;\n max_tokens?: number;\n // Ollama specific\n name?: string;\n details?: {\n parameter_size?: string;\n family?: string;\n };\n }>;\n };\n\n if (!Array.isArray(data.data) || data.data.length === 0) {\n log.warn(\"Local LLM returned no models\");\n return null;\n }\n\n // Verbose model listings are noisy on every gateway restart. Keep it debug-only.\n const modelIds = data.data.map((m) => m.id).filter(Boolean);\n log.debug(\n `Local LLM: Found ${modelIds.length} model(s). First 10: ${modelIds.slice(0, 10).join(\", \")}`,\n );\n\n // Find the model matching our configured model ID\n const configuredModel = this.config.localLlmModel;\n let model = data.data.find((m) => m.id === configuredModel);\n\n // If not found by exact match, try partial match (handle suffixes like @4bit)\n if (!model) {\n model = data.data.find((m) =>\n configuredModel.includes(m.id || \"\") ||\n (m.id || \"\").includes(configuredModel.replace(/@\\d+bit$/, \"\"))\n );\n }\n\n // If still not found, use the first loaded model and warn\n if (!model) {\n model = data.data[0];\n const availablePreview = data.data\n .map((m) => m.id)\n .filter(Boolean)\n .slice(0, 10)\n .join(\", \");\n log.warn(\n `Configured model \"${configuredModel}\" not found in local LLM. ` +\n `Using \"${model.id}\" instead. Available (first 10): ${availablePreview}`\n );\n }\n\n // Extract context window - try multiple field names\n let contextWindow = model.max_context_length || model.max_tokens;\n\n // If API doesn't report context window, try LMS CLI (LM Studio specific)\n if (!contextWindow) {\n log.info(\"Local LLM: API did not report context window, trying LMS CLI...\");\n const lmsContext = this.getCachedContextWindow(model.id || \"\");\n if (lmsContext) {\n contextWindow = lmsContext;\n }\n }\n\n this.cachedModelInfo = {\n id: model.id || \"unknown\",\n contextWindow: contextWindow,\n maxTokens: model.max_tokens,\n };\n\n log.info(\n `Local LLM model detected: ${this.cachedModelInfo.id}, ` +\n `context window: ${contextWindow?.toLocaleString() || \"unknown (may use default)\"}`\n );\n\n return this.cachedModelInfo;\n } catch (err) {\n log.warn(`Failed to fetch model info: ${err}`);\n return null;\n }\n }\n\n /**\n * Check if the configured model is available and get its actual context window.\n * Warns if there's a mismatch between expected and actual context.\n */\n async validateModelConfig(expectedContextWindow?: number): Promise<{\n available: boolean;\n actualContextWindow?: number;\n warnings: string[];\n }> {\n const warnings: string[] = [];\n\n const modelInfo = await this.getLoadedModelInfo();\n if (!modelInfo) {\n return { available: false, warnings: [\"Could not query local LLM for model info\"] };\n }\n\n // If we have expected context and the server reports one, check for mismatch\n if (expectedContextWindow && modelInfo.contextWindow) {\n if (modelInfo.contextWindow < expectedContextWindow) {\n warnings.push(\n `Context window mismatch: Model ${modelInfo.id} supports ${modelInfo.contextWindow.toLocaleString()} tokens, ` +\n `but engram is configured for ${expectedContextWindow.toLocaleString()}. ` +\n `Set localLlmMaxContext: ${modelInfo.contextWindow} in config to avoid errors.`\n );\n }\n }\n\n // Warn if server doesn't report context window (common with some local LLM setups)\n if (!modelInfo.contextWindow) {\n warnings.push(\n `Local LLM server did not report context window for ${modelInfo.id}. ` +\n `If you get \"context length exceeded\" errors, set localLlmMaxContext in config.`\n );\n }\n\n return {\n available: true,\n actualContextWindow: modelInfo.contextWindow,\n warnings,\n };\n }\n\n /**\n * Make a chat completion request to local LLM\n */\n async chatCompletion(\n messages: Array<{ role: string; content: string }>,\n options: LocalLlmChatCompletionOptions = {},\n ): Promise<LocalLlmChatCompletionResult | null> {\n if (!this.config.localLlmEnabled) {\n log.debug(\"local LLM: disabled, returning null\");\n return null;\n }\n\n const remainingMs = this.remainingCooldownMs();\n if (remainingMs > 0) {\n log.debug(`local LLM: cooldown active (${remainingMs}ms remaining), skipping request`);\n return null;\n }\n if (options.priority) {\n const priority = options.priority;\n return await new Promise<LocalLlmChatCompletionResult | null>((resolve) => {\n this.requestQueues[priority].push({\n messages,\n options,\n priority,\n enqueuedAtMs: Date.now(),\n resolve,\n });\n this.scheduleQueueDrain();\n });\n }\n\n return await this.runChatCompletionRequest(messages, options);\n }\n\n /**\n * Estimate tokens when local LLM doesn't return usage stats\n * Rough estimate: 1 token ≈ 4 characters\n */\n private estimateTokens(\n messages: Array<{ role: string; content: string }>,\n response: string\n ): { promptTokens: number; completionTokens: number; totalTokens: number } {\n const promptChars = messages.reduce((sum, m) => sum + m.content.length, 0);\n const promptTokens = Math.ceil(promptChars / 4);\n const completionTokens = Math.ceil(response.length / 4);\n\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n };\n }\n\n /**\n * Try local LLM first, fallback to cloud provider if configured\n */\n async withFallback<T>(\n localOperation: () => Promise<T | null>,\n fallbackOperation: () => Promise<T>,\n operationName: string\n ): Promise<T> {\n // Try local LLM first if enabled\n if (this.config.localLlmEnabled) {\n const localResult = await localOperation();\n if (localResult !== null) {\n log.debug(`${operationName}: used local LLM`);\n return localResult;\n }\n\n // Local failed or unavailable\n if (this.config.localLlmFallback) {\n log.info(`${operationName}: local LLM unavailable, falling back to cloud`);\n } else {\n throw new Error(`${operationName}: local LLM unavailable and fallback disabled`);\n }\n }\n\n // Use fallback (cloud provider)\n return fallbackOperation();\n }\n}\n"],"mappings":"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","names":["backoffMs","promptChars"]}
|
|
@@ -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"]}
|