{"uuid": "d7bfab33-81a9-4115-a654-d16bc2e424b2", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2024-27980", "type": "seen", "source": "https://gist.github.com/ukind/2e6e8aa37861a40d5f3ad455cef9918b", "content": "# Remnic + QMD Windows Setup (LLM Instructions)\n\n**Audience:** LLM agent configuring Remnic on Windows from zero. Execute steps in order. Verify each step before proceeding. Stop and report on any verification failure.\n\n**Goal end state:**\n\n```\nSearch backend: available cli=true daemon=true session=true\n  cliPath=C:\\nvm4w\\nodejs\\qmd.cmd\n  cliPathSource=configured\n  cliVersion=qmd 2.5.3\n```\n\nPlus LLM extraction working (no 401, facts auto-persisted):\n\n```\nremnic: persisted: 2 facts, 0 entities, 2 questions, 2 profile updates\n```\n\n---\n\n## Prerequisites (verify before step 1)\n\n```bash\nnode --version        # expect: v18+ (v24.x recommended)\nnpm --version         # expect: 10+\nwhere node            # note install root, e.g. C:\\nvm4w\\nodejs\\node.exe\n```\n\nIf Node missing \u2192 install via nvm-windows first. Do **not** proceed without Node.\n\n**Required minimum versions** (older versions have the Windows spawn bug):\n\n- `@remnic/cli` \u2265 **9.3.644** (verified through **9.7.15** as of 2026-07-31)\n- `@tobilu/qmd` = **2.5.3** (must match Remnic's `QMD_SUPPORTED_VERSION` \u2014 currently `\"2.5.3\"`)\n\n**\u26a0 Check for conflicting package managers before step 1.** If `bun`, `nub` (`@nubjs/nub`), `pnpm`, or `yarn` are installed globally alongside npm, `npm install -g @tobilu/qmd` can fail with `spawn C:\\WINDOWS\\system32\\cmd.exe ENOENT` during the `better-sqlite3` postinstall build. This is the #1 cause of a failed QMD install on a machine that already has Node. Detect and remove:\n\n```bash\n# detect\nwhere bun; where nub; where pnpm; where yarn\nnpm list -g --depth=0 2&gt;&amp;1 | grep -iE \"bun|nub|pnpm|yarn\"\n\n# remove (example: bun + nub found)\nnpm uninstall -g bun @nubjs/nub nub\nrm -rf ~/.bun          # bun native install dir\n# pnpm: npm uninstall -g pnpm; rm -rf ~/AppData/Local/pnpm\n# yarn (classic):  npm uninstall -g yarn\n# yarn (berry via corepack): corepack disable yarn\n```\n\nAlso remove any dead `~/.bun/bin` (or equivalent) entries from PATH \u2014 they become harmless once the binary is gone, but a clean PATH avoids confusion. After removal, re-run step 1.\n\n---\n\n## Step 1 \u2014 Install QMD\n\n```bash\nnpm install -g @tobilu/qmd\n```\n\n**Verify:**\n\n```bash\nqmd --version\n# expect: qmd 2.5.3\nwhere qmd\n# expect two lines: \\qmd  and  \\qmd.cmd\n```\n\n**Capture** the `.cmd` path. Use it as `` for the rest of this doc.\nExample: `C:\\nvm4w\\nodejs\\qmd.cmd`\n\nIf `qmd --version` prints a version \u2260 2.5.3 \u2192 install exact version:\n\n```bash\nnpm install -g @tobilu/qmd@2.5.3\n```\n\n---\n\n## Step 2 \u2014 Install Remnic\n\n```bash\nnpm install -g @remnic/cli@latest\n```\n\n**Verify:**\n\n```bash\nnpm list -g @remnic/cli\n# expect: @remnic/cli@9.3.644 or newer\n```\n\nIf installed version &lt; 9.3.644 \u2192 **stop**. The Windows `child_process.spawn` bug is unfixed below this version (see GitHub issue #1476). Force latest:\n\n```bash\nnpm install -g @remnic/cli@latest\n```\n\n---\n\n## Step 3 \u2014 Create Remnic config\n\nThere are **two** candidate paths. Write to BOTH to be safe \u2014 they serve different readers:\n\n1. `%USERPROFILE%\\.config\\remnic\\config.json` \u2014 the documented modern path (forward-compat).\n2. `%USERPROFILE%\\.pi\\agent\\remnic.config.json` \u2014 **the path newer Remnic (\u2265 9.3.7xx) actually resolves at runtime when the Pi connector is installed.** `remnic config` prints this as the first line. If you only write path 1, the server loads an empty/placeholder config \u2192 `cliPathSource=auto-path` and `no gateway default AI`.\n\n&gt; `.config/engram/config.json` is the legacy pre-rename path; older Remnic still falls back to it. **Verify which path your install reads with `remnic config` (first line is the resolved path) and ensure the file there has the full schema below.** When in doubt, write the same content to all three locations.\n\n&gt; \u26a0\u26a0 **CRITICAL \u2014 `resolveConfigPath()` checks `process.cwd()/remnic.config.json` FIRST.** Config resolution order is: `--config` flag \u2192 `REMNIC_CONFIG_PATH`/`ENGRAM_CONFIG_PATH` env \u2192 `process.cwd()/remnic.config.json` \u2192 `process.cwd()/engram.config.json` \u2192 `~/.config/remnic/config.json` \u2192 `~/.config/engram/config.json`. **This means the daemon's config depends on the LAUNCH working directory.** A stray `remnic.config.json` left in `%USERPROFILE%` (the user home root) gets picked FIRST whenever the daemon is launched from home (login tasks, manual `daemon start` from `~`, some MCP hosts) \u2014 silently overriding the canonical config. This is the #1 cause of split-brain: two configs with different `memoryDir` / `model` / preset \u2192 two memory stores, broken extraction, recall reading the wrong tree.\n&gt;\n&gt; **Hard rule: after writing the canonical config, DELETE any `remnic.config.json` / `engram.config.json` from `%USERPROFILE%` itself (the home root, NOT `.config\\`).** Verify:\n&gt;\n&gt; ```bash\n&gt; # MUST print 'absent' \u2014 a home-root remnic.config.json is always a bug\n&gt; test -e \"$USERPROFILE/remnic.config.json\" &amp;&amp; echo \"PRESENT (delete it!)\" || echo absent\n&gt; # all candidate configs must be byte-identical\n&gt; md5sum \"$USERPROFILE/.config/remnic/config.json\" \"$USERPROFILE/.config/engram/config.json\" \"$USERPROFILE/.pi/agent/remnic.config.json\"\n&gt; ```\n&gt;\n&gt; Only the three paths under `.config\\` and `.pi\\agent\\` should exist. The home-root file is never legitimate.\n\nCreate parent dirs if missing:\n\n```bash\nmkdir -p \"$USERPROFILE/.config/remnic\"\nmkdir -p \"$USERPROFILE/.pi/agent\"   # for path 2 above\n```\n\n**Generate a random auth token first** (the server REQUIRES this \u2014 without it, all requests get 401):\n\n```bash\n# Generate a 64-char hex token. Use this as  below.\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n```\n\nWrite file (substitute `` from step 1, `` from above, and `` from your gateway \u2014 use double backslashes in JSON):\n\n```json\n{\n  \"qmdPath\": \"C:\\\\nvm4w\\\\nodejs\\\\qmd.cmd\",\n  \"remnic\": {\n    \"qmdPath\": \"C:\\\\nvm4w\\\\nodejs\\\\qmd.cmd\",\n    \"openaiApiKey\": \"\",\n    \"openaiBaseUrl\": \"https://your-gateway.example.com/v1\",\n    \"model\": \"\",\n    \"memoryDir\": \"C:\\\\Users\\\\\\\\.pi\\\\agent\\\\.remnic\\\\memory\",\n    \"memoryOsPreset\": \"balanced\"\n  },\n  \"server\": {\n    \"host\": \"127.0.0.1\",\n    \"port\": 4318,\n    \"authToken\": \"\"\n  },\n  \"engram\": { \"qmdPath\": \"C:\\\\nvm4w\\\\nodejs\\\\qmd.cmd\" }\n}\n```\n\nAll three top-level keys (`qmdPath`, `remnic`, `engram`) required for forward-compat with the `engram \u2192 remnic` rename. Forward slashes (`C:/nvm4w/nodejs/qmd.cmd`) also work \u2014 use whichever your file-write tool handles safely.\n\n**Critical fields explained:**\n\n| Field | Purpose | Why it matters |\n| --- | --- | --- |\n| `server.authToken` | Master auth token for the HTTP server | **REQUIRED.** Without it, server log prints `No auth token set \u2014 server will reject all requests` and ALL requests return 401. Generate via `node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"` |\n| `server.host` / `server.port` | Server bind address | `127.0.0.1:4318` is the default. Change only if port conflicts |\n| `openaiApiKey` | API key for your LLM gateway | Extraction engine uses this for fact/entity extraction |\n| `openaiBaseUrl` | Gateway URL (OpenAI-compatible) | **Must match the gateway that issued the key.** If omitted, Remnic sends the key to `api.openai.com` \u2192 401 |\n| `model` | Model ID the extraction LLM requests | **REQUIRED on most gateways.** Remnic hardcodes `DEFAULT_REASONING_MODEL = \"gpt-5.5\"` and uses it whenever `remnic.model` is empty/unset. The chunk filename is **version-specific** (it changes every release \u2014 was `chunk-2HAG7JOI.js:1067` in 9.3.x, is `chunk-INELTPRG.js:1187` in 9.7.15). Find it in your install with: `grep -rn \"DEFAULT_REASONING_MODEL =\" \"$(npm root -g)/@remnic/cli/node_modules/@remnic/core/dist/\"`. If your gateway does not serve `gpt-5.5`, extraction fails (`M302 Model \"gpt-5.5\" is not available` / `404 model \"gpt-5.5\" not found`) \u2192 `fallback_no_parsed_output` \u2192 **zero new memories extracted**. Set this to a model your gateway actually serves (e.g. Manifest `auto`, Ollama-cloud `minimax-m3`, OpenRouter `gpt-4o-mini`). Verify available IDs via `curl -s $OPENAI_BASE_URL/models -H \"Authorization: Bearer $OPENAI_API_KEY\"` |\n| `memoryDir` | Where extracted memories are written | **QMD `index.yml` (step 4) MUST point at this exact path.** Mismatch = QMD indexes empty dir, recall falls back to slow file scan |\n| `memoryOsPreset` | OS-specific path handling | `balanced` is the safe default |\n| `gatewayConfig` (optional, see Issue G) | Model config for the fallback LLM client (scheduled functions: hourly summaries, some Dreams phases) | **REQUIRED to enable scheduled/background functions.** Without it, the hourly summarizer is silently disabled (`no gateway default AI and local LLM disabled \u2014 hourly summarization disabled`). Must use `provider/model` format (e.g., `\"openai/minimax-m3\"`). Requires a matching provider config in `gatewayConfig.models.providers` to override the built-in `openai` base URL. See Issue G for full schema. |\n\n&gt; \u26a0 **`gatewayConfig` vs `model`:** These are separate fields for separate code paths. `model` feeds the **direct client** (extraction). `gatewayConfig` feeds the **fallback client** (scheduled functions). Both are needed for full functionality. Do NOT set `modelSource: \"gateway\"` \u2014 that routes extraction through the fallback client and breaks it.\n\n**Verify:**\n\n```bash\nremnic config\n# First line shows the resolved path. Accept ANY of:\n#   Config: %USERPROFILE%\\.config\\remnic\\config.json      (documented modern)\n#   Config: %USERPROFILE%\\.pi\\agent\\remnic.config.json    (newer Remnic \u2265 9.3.7xx with Pi connector)\n#   Config: %USERPROFILE%\\.config\\engram\\config.json      (legacy pre-rename)\n# JSON body must contain qmdPath AND remnic.model. If the body shows\n# placeholder ${OPENAI_API_KEY} / ${REMNIC_AUTH_TOKEN} and no qmdPath/model,\n# you are reading the Pi EXTENSION template, not the server config \u2014\n# overwrite it with the full schema above.\n```\n\nIf the resolved path is any of the three above, ensure that exact file has the full schema (qmdPath, remnic.model, remnic.openaiApiKey, remnic.openaiBaseUrl, server.authToken). The Pi extension file at `%USERPROFILE%\\.pi\\agent\\extensions\\remnic\\remnic.config.json` is a DIFFERENT file (connector-facing: recall/observe settings + its own `remnic_p\u2026` token) \u2014 do not confuse the two.\n\n---\n\n## Step 4 \u2014 Create QMD collections config\n\nPath: `%USERPROFILE%\\.config\\qmd\\index.yml`\n\nCreate parent dir if missing:\n\n```bash\nmkdir -p \"$USERPROFILE/.config/qmd\"\n```\n\n**Determine collection name first.** Remnic derives it from workspace identity. Default Pi agent collection is `openclaw-engram` (the legacy name was kept after the rename to preserve embedding indexes). The exact name will appear in the daemon log after step 5 if you guess wrong \u2014 adjust this file and re-run `qmd update` if so.\n\n&gt; \u26a0 **The `openclaw-engram` path MUST match `remnic.memoryDir` from step 3.** The Remnic server writes extracted memories to the configured `memoryDir`, NOT to the default `~/.remnic/memory`. If `index.yml` points at a different directory, QMD indexes an empty tree and recall silently degrades to file-scan fallback. This is the #1 cause of \"recall works but QMD never fires.\"\n\nWrite file:\n\n```yaml\ncollections:\n  pi-memory:\n    path: C:\\Users\\\\.pi\\agent\\memory\n    pattern: \"**/*.md\"\n  openclaw-engram:\n    # MUST match remnic.memoryDir from step 3 config.\n    # Default Pi agent: .pi\\agent\\.remnic\\memory\n    path: C:\\Users\\\\.pi\\agent\\.remnic\\memory\n    pattern: \"**/*.md\"\nmodels:\n  embed: hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf\n  generate: hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf\n  rerank: hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf\n```\n\nSubstitute every `` placeholder above with your actual Windows username (`echo $USERNAME` or `%USERNAME%`). OpenClaw integration is **not** part of this Windows setup \u2014 its workspace collection is intentionally omitted. Skip `remnic openclaw install` and ignore the doctor warning about the missing `openclaw.json`.\n\n**Verify:**\n\n```bash\nqmd doctor 2&gt;&amp;1 | head -20\n# expect: collections discovered, no YAML parse errors\n```\n\n---\n\n## Step 5 \u2014 Install Pi connector (REQUIRED for Pi agent integration)\n\nIf using Remnic with the Pi Coding Agent, install the connector. This creates the Pi extension at `%USERPROFILE%\\.pi\\agent\\extensions\\remnic\\` and generates a **separate connector-scoped token** (different from `server.authToken` \u2014 both are accepted by the server).\n\n```bash\nremnic connectors install pi\n```\n\n**Verify:**\n\n```bash\n# Extension files exist\ntest -f \"$USERPROFILE/.pi/agent/extensions/remnic/remnic.config.json\" &amp;&amp; echo \"OK pi extension\" || echo \"FAIL pi extension\"\n\n# Connector shows as installed (\u2713)\nremnic connectors list 2&gt;&amp;1 | grep \"pi\"\n# expect: \u2713 pi  Pi Coding Agent ...\n```\n\nThe extension's `remnic.config.json` contains its own `authToken` and `remnicDaemonUrl`. Do **not** manually edit this file unless the daemon URL or port changes.\n\n&gt; \u26a0 **Token relationship:** The server accepts BOTH the master `server.authToken` (from step 3 config) AND any connector token generated by `remnic token generate` / `remnic connectors install`. They are different tokens by design \u2014 the master token guards the HTTP API, connector tokens are scoped per-connector. Do not try to make them match.\n\n---\n\n## Step 6 \u2014 Set LLM gateway env vars (REQUIRED for extraction)\n\nIf using a non-OpenAI gateway (Manifest, OpenRouter, Groq, Ollama, etc.), the OpenAI SDK reads `OPENAI_API_KEY` from the environment. Without `OPENAI_BASE_URL` set, the SDK sends that key to `api.openai.com` \u2192 **401 Incorrect API key**. This breaks all LLM extraction (fact/entity/profile extraction, hourly summaries).\n\nSet both env vars to match your `config.json` gateway from step 3:\n\n```bash\nsetx OPENAI_API_KEY \"\"\nsetx OPENAI_BASE_URL \"https://your-gateway.example.com/v1\"\n```\n\n&gt; \u26a0 **`setx` only affects NEW processes spawned after it runs.** The current shell, any running daemon, and any running MCP host will NOT see the new env vars until they are restarted. A full Windows reboot is the most reliable way to ensure all processes inherit the new env.\n&gt;\n&gt; **Avoiding a reboot for immediate testing:** `remnic daemon start` spawns a detached process that inherits the *spawning shell's* environment at fork time \u2014 NOT the registry values `setx` just wrote. So `setx` + immediate `remnic daemon start` in the same shell still yields `Fatal: Environment variable OPENAI_API_KEY is not set`. Export the vars into the current shell FIRST, then start the daemon:\n&gt;\n&gt; ```bash\n&gt; # write env script from config (avoids hand-copying secrets)\n&gt; node -e \"const c=require(require('path').join(process.env.USERPROFILE,'.pi','agent','remnic.config.json'));process.stdout.write('export OPENAI_API_KEY=\\\"'+c.remnic.openaiApiKey+'\\\"\\nexport OPENAI_BASE_URL=\\\"'+c.remnic.openaiBaseUrl+'\\\"\\n')\" &gt; /tmp/r.sh &amp;&amp; source /tmp/r.sh\n&gt; remnic daemon stop; remnic daemon start\n&gt; ```\n&gt;\n&gt; The `setx` (registry) values still take effect for future reboots / Task Scheduler; the `source` just bridges the current session.\n\n**Verify (in a NEW shell opened after `setx`):**\n\n```bash\nenv | grep OPENAI\n# expect both OPENAI_API_KEY and OPENAI_BASE_URL present\n```\n\n**Gateway compatibility notes:**\n\n| Gateway type | Known issues | Fix |\n| --- | --- | --- |\n| Manifest (`app.manifest.build`) | Only serves model `auto` for most agents; Remnic defaults to `gpt-5.5` \u2192 `M302 Model \"gpt-5.5\" is not available`. Also forces SSE streaming by default; routes to reasoning model that returns `reasoning_content` not `content` | **Set `remnic.model: \"auto\"`** in config (step 3). Disable streaming on gateway dashboard. Remnic's default `extractionMaxOutputTokens: 16384` is sufficient for reasoning overhead |\n| Ollama-cloud (`ollama.com/v1`) | Reasoning models (`minimax-m3`, `glm-5`, etc.) with `thinkingFormat: zai` | Set `remnic.model` to a served ID (e.g. `minimax-m3`). Verified working: extraction returns parseable JSON in `content`, `persisted: N facts` |\n| OpenRouter / Groq / Together | Standard OpenAI-compatible | Usually works out of the box |\n| Ollama / LM Studio (local) | Set `openaiBaseUrl` to `http://localhost:11434/v1` (Ollama) or `http://localhost:1234/v1` (LM Studio) | Use dummy key like `ollama` |\n\n---\n\n## Step 7 \u2014 Pull QMD models (REQUIRED before first daemon start)\n\nThe daemon's startup QMD-sync and `qmd embed` both need the 3 GGUF models (~2.3 GB: embedding + query-expansion + reranker). They are **not** bundled \u2014 pull them BEFORE starting the daemon, otherwise the first daemon start races the download and logs `QMD update \u2026 timed out after 90000ms` (the #1 fresh-install stumble).\n\n```bash\nqmd pull\n# expect: downloads embed/generate/rerank GGUFs into ~/.cache/qmd/models/\n```\n\n**Verify:**\n\n```bash\nqmd doctor 2&gt;&amp;1 | grep -iE \"model cache|missing\"\n# expect: no \"missing\" lines (all 3 present)\n```\n\n`qmd pull` is idempotent \u2014 re-running only fetches missing/changed models. Skip on reinstall if `~/.cache/qmd/models/` already has all three.\n\n---\n\n## Step 8 \u2014 Start Remnic daemon\n\nWindows has **no service install** \u2014 `remnic daemon install` returns `Unsupported platform: win32`. You must `remnic daemon start` manually after every reboot (or wire it into Task Scheduler \u2014 see Persistence section).\n\n```bash\nremnic daemon stop          # safe if not running\nremnic daemon start\n```\n\n**Verify (wait 3 seconds first):**\n\n```bash\nsleep 3\nnetstat -ano | findstr \":4318\" | findstr LISTENING\n# expect one LISTENING line \u2014 proves the daemon is actually up\ngrep -i \"Search backend\" \"$USERPROFILE/.remnic/server.log\" | tail -1\n```\n\n&gt; \u26a0 **`remnic daemon status` now works on Windows as of 9.7.15.** Previously (\u22649.3.x) it reported `Running: no` and never wrote the PID file after detach \u2014 that bug is fixed; `server.pid` is now written and `daemon status` reports the correct PID. `netstat -ano | findstr :4318` remains a reliable cross-check (and the source of truth on older versions).\n\n**Expected success line:**\n\n```\nremnic: Search backend: available cli=true daemon=true session=true\n  cliPath=C:\\nvm4w\\nodejs\\qmd.cmd\n  cliPathSource=configured\n  cliVersion=qmd 2.5.3\n```\n\nA harmless `cliProbeError=spawn /opt/homebrew/bin/qmd ENOENT` may appear in the first few startup lines (the probe tries a hardcoded macOS path before the configured path wins). Ignore it once the success line above shows `cli=true`.\n\n**If failure** \u2192 see Troubleshooting matrix below before continuing.\n\n---\n\n## Step 9 \u2014 Index existing memory\n\n```bash\nqmd update\n```\n\n**Expected output:** lists each collection, shows `Indexed: N new` counts.\n\nIf a collection shows `0 new, 0 updated, 0 unchanged, 0 removed` and the directory contains `.md` files \u2192 check the path in `index.yml` matches the actual directory.\n\n**Then embed:**\n\n```bash\nqmd embed\n```\n\n**Expected:**\n\n```\n\u2713 Done! Embedded N chunks from M documents in Xs\n```\n\n&gt; \u26a0 **Embedding uses the GPU (node-llama-cpp).** On a PC without an NVIDIA GPU + drivers, `qmd embed` falls back to CPU (far slower) or errors with `ggml-cuda \u2026 CUDA error`. Force CPU if needed: `QMD_FORCE_CPU=1 qmd embed`. The first embed of a large store is the slowest run \u2014 subsequent embeds are incremental.\n\n---\n\n## Step 10 \u2014 Restart MCP host\n\nIf Remnic is consumed via MCP (Pi agent, Claude Desktop, Cursor, Codex), **restart the host application** after the steps above. The Pi connector talks to the **standalone daemon at `127.0.0.1:4318`** (see its `remnicDaemonUrl` in the extension config) \u2014 it does NOT spawn its own daemon, so in-place env/config changes won't reach an already-connected host until it reconnects. (Other MCP hosts may spawn their own embedded daemon over stdio \u2014 same restart principle applies.)\n\n**Verify via MCP tool (in host context):**\n\n```\nremnic_console_state\n```\n\nExpected fragment:\n\n```json\n\"qmdProbe\": {\n  \"available\": true,\n  \"daemonMode\": true,\n  \"debug\": \"cli=true daemon=true session=true cliPath=...qmd.cmd...\"\n}\n```\n\n---\n\n## Extraction pipeline (how it works + how to verify)\n\nRemnic's core loop: each `observe` (conversation turn) is queued \u2192 an LLM extracts structured memories \u2192 they're persisted to the memory store and QMD-indexed. Understanding the stages prevents the two most common confusions: *\"I called observe but nothing persisted\"* and *\"I fixed the model and suddenly a burst of extractions ran.\"*\n\n### It's asynchronous \u2014 don't trust the in-turn response\n\n`observe` / `remnic_observe` returns immediately with `\"extractionQueued\": true`. The LLM call + persistence happen **seconds later** on the extraction queue. The response only confirms the turn was *accepted*, not that memories were extracted.\n\n**Verify extraction health from the log, never from the response:**\n\n```bash\n# positive signal \u2014 facts were extracted and written\ngrep -aE \"persisted: [0-9]+ facts\" \"$USERPROFILE/.remnic/server.log\" | tail -5\n# expect e.g.: remnic: persisted: 3 facts, 1 entities, 2 questions, 0 profile updates\n\n# negative signal \u2014 all extraction stages failed\ngrep -aE \"extraction fallback returned no parsed output|model \\\"gpt-5.5\\\" not found\" \"$USERPROFILE/.remnic/server.log\" | tail -5\n```\n\nA `persisted: 0 facts, \u2026 1 questions` line is still a **successful** run (the LLM decided nothing was worth keeping) \u2014 only `extraction fallback returned no parsed output` / model-404s indicate failure.\n\n### The 3-stage fallback chain\n\nExtraction tries LLM backends in order; the first to return parseable JSON wins:\n\n1. **Direct client** (`remnic.model`, bare ID) \u2014 primary path, OpenAI SDK against `openaiBaseUrl`.\n2. **Gateway AI** (fallback client, `gatewayConfig\u2026provider/model`) \u2014 only if the direct client errors (404 / timeout / parse-fail). Needs `gatewayConfig` (Issue G).\n3. **Gateway default AI** \u2014 last resort; absent `gatewayConfig` \u2192 `fallback LLM: no models configured in gateway`.\n\nLog signature per stage: `extraction: direct client failed, falling back to gateway AI` \u2192 `fallback LLM: no models configured in gateway` \u2192 `extraction fallback returned no parsed output`. **Stage 1 alone is sufficient for extraction**; stages 2\u20133 also serve scheduled functions (Issue G). Do NOT set `modelSource: \"gateway\"` \u2014 it forces extraction onto the fallback client and breaks it.\n\n### Backlog self-heal (why a burst appears after a fix)\n\nA successful extraction marks the conversation **fingerprint** processed; a *failed* extraction (all stages) does **NOT** mark it. So every subsequent `observe` re-queues any unprocessed fingerprints. Consequence: when you fix a broken model/gateway, the daemon replays the accumulated backlog \u2014 a burst of `persisted:` lines. This is correct, not a bug. It is bounded by the circuit breaker + backoff below.\n\n### Circuit breaker + retry\n\nWhen the gateway is failing, extraction does not hammer it:\n\n- **Circuit breaker**: after **5** consecutive failures (`extractionBreakerFailureThreshold`) extraction pauses **5 min** (`extractionBreakerCooldownMs`); a **401/auth** failure pauses **30 min** (`extractionBreakerAuthCooldownMs`). A paused daemon logs nothing new \u2014 it looks idle, not erroring. Don't confuse silence with health; check `persisted:` and the error grep.\n- **Retry/backoff** (`extractionRetryEnabled`, **on** by default): schedule `[60s, \u2026]`, `extractionRetryMaxBackoffMs` = 6 h, jitter 0.2.\n\n### Gating thresholds (why short chats extract nothing)\n\nConversations below these floors skip extraction entirely (no LLM call, no error logged):\n\n| Tunable | Default | Effect |\n| --- | --- | --- |\n| `extractionMinChars` | **40** | Turns shorter than this are skipped |\n| `extractionMinUserTurns` | **1** | Need \u22651 real user turn |\n| `extractionMaxTurnChars` | **4000** | Per-turn truncation |\n| `extractionMaxFactsPerRun` | **12** | Cap facts per run |\n| `extractionMaxEntitiesPerRun` | 6 \u00b7 `\u2026Questions` 3 \u00b7 `\u2026ProfileUpdates` 4 | Per-type caps |\n| `extractionDedupeEnabled` | **on** (5-min `extractionDedupeWindowMs`) | Suppresses near-duplicate facts within window |\n| `extractionMaxOutputTokens` | **16384** | LLM response budget (covers reasoning overhead) |\n\nMechanical/telemetry transcripts are also pre-filtered (`extractionTelemetryPrefilterEnabled`) \u2014 action logs with no durable cues skip extraction.\n\n### Faithfulness gate / judge (optional, off by default)\n\n`extractionJudgeEnabled` runs a **second** LLM pass scoring extracted facts for faithfulness before persisting (issue #1576 / #1581). Off by default. Enabling requires `extractionJudgeModel` (bare ID, same gateway) and roughly doubles LLM cost. `extractionJudgeShadow: true` logs scores without gating (safe way to evaluate). Leave off unless you need quality gating.\n\n### What gets extracted\n\nEach run can emit: **facts** (categories: `fact`, `decision`, `preference`, `entity`, `relationship`, `principle`, `commitment`, `moment`, `skill`, `rule`, `procedure`, `reasoning_trace`), **entities**, **profileUpdates** (behavioral profile), **questions** (open curiosities), and **relationships** (entity-graph edges).\n\n---\n\n## Troubleshooting matrix\n\nEach row: log signature \u2192 root cause \u2192 fix.\n\n| Log line / symptom | Root cause | Fix |\n| --- | --- | --- |\n| **Split-brain: `remnic config` shows canonical path BUT daemon writes to wrong `memoryDir` / uses `gpt-5.5` / preset differs** | A stray `remnic.config.json` (or `engram.config.json`) exists in `%USERPROFILE%` **home root**. `resolveConfigPath()` checks `process.cwd()/remnic.config.json` FIRST, so a daemon launched from home loads that file instead of the canonical `~/.config/remnic/config.json`. Two configs \u2192 two memory stores \u2192 extraction/recall disagree. | `test -e \"$USERPROFILE/remnic.config.json\" &amp;&amp; rm \"$USERPROFILE/remnic.config.json\"`. Then `md5sum` the three legitimate configs to confirm identical. Restart daemon from the same cwd each time (or rely on `~/.config/remnic/config.json` fallthrough). See Step 3 warning + Issue A. |\n| **Extraction 404 `NotFoundError: 404 model \"gpt-5.5\" not found`** (Ollama-cloud / OpenRouter / any non-OpenAI gateway) | `remnic.model` unset in the config the daemon ACTUALLY loaded (often because a home-root rogue config won the cwd race). Falls back to hardcoded `DEFAULT_REASONING_MODEL = \"gpt-5.5\"`. | Set `remnic.model` to a served ID (e.g. `minimax-m3`) in the file `remnic config` resolves to \u2014 AND ensure no home-root config shadows it. Verify the model is served: `curl -s $OPENAI_BASE_URL/models -H \"Authorization: Bearer $OPENAI_API_KEY\"`. Restart daemon. Grep log for `persisted: N facts` to confirm. |\n| **Two memory stores: recall returns paths under `.pi\\agent\\.remnic\\memory` but new extractions vanish / land in `~/.remnic\\memory`** | Divergent configs with different `memoryDir`. The daemon writes to ITS config's `memoryDir`; QMD `index.yml` indexes the other \u2192 newly written memories are never recalled. | Pick ONE `memoryDir` (recommend `.pi\\agent\\.remnic\\memory`, the project-scoped store), set it identically in all 3 configs, align `index.yml` `openclaw-engram.path` to match, merge any unique files from the abandoned store, then `qmd update &amp;&amp; qmd embed`. See Step 3 + Issue A. |\n| `runExtraction: failed to load meta for retry bookkeeping (non-fatal) SyntaxError: Unexpected token ' ', \"            \"... is not valid JSON` at `_StorageManager.loadMeta` | `state/meta.json` in the active `memoryDir` is corrupted \u2014 typically all-NUL bytes (136K of `\\0`) from an aborted/crashed write. `JSON.parse` throws on every extraction. | Quarantine the bad file: `mv /state/meta.json /state/meta.json.nul-corrupt.bak`. The server recreates a fresh valid `meta.json` on the next write. Non-fatal but pollutes the log and breaks extraction-retry bookkeeping \u2014 fix it. |\n| `cliProbeError=spawn /opt/homebrew/bin/qmd ENOENT` AND `cli=false` | Remnic &lt; 9.3.644 (pre cross-spawn fix) | `npm install -g @remnic/cli@latest`, restart daemon + MCP host |\n| `cliProbeError=spawn ... EINVAL` | Windows blocking `.cmd` spawn (Node \u226518 CVE-2024-27980 enforcement); Remnic version too old | Same as above |\n| `cliPathSource=auto-path` but everything works | Config file not present or not loaded | Optional \u2014 works via PATH. To silence the fallback, complete step 3 |\n| `Search backend: available cli=true` BUT `QMD collection \"\" not found` | Collection missing from `index.yml` | Add collection name from log to step 4 file, run `qmd update` |\n| `Search collection missing for Remnic memory store` | Memory dir empty or never indexed | Run `qmd update &amp;&amp; qmd embed` |\n| `QMD update failed for collection X: ... aborted` on fresh install | Models not yet downloaded (Step 7 `qmd pull` skipped or incomplete) | Run `qmd pull` (Step 7), then `qmd update &amp;&amp; qmd embed`. Avoided entirely if Step 7 is done first |\n| Daemon shows old version after `npm install -g` | Daemon was running during upgrade | `remnic daemon stop &amp;&amp; remnic daemon start`, then restart MCP host |\n| Server returns 401 for ALL requests (`{\"error\":\"unauthorized\"}`) | `server.authToken` missing from config.json | Add `\"server\": { \"authToken\": \"\" }` to config. Generate hex via `node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"`. Restart daemon |\n| Server log: `No auth token set \u2014 server will reject all requests` | Same as above \u2014 no `server.authToken` and no `REMNIC_AUTH_TOKEN` env var | Same fix |\n| Pi agent not calling recall/observe (no memory extraction from conversations) | Pi connector not installed, or installed with stale token | Run `remnic connectors install pi --force`. Verify `remnic connectors list` shows `\u2713 pi` |\n| `Incorrect API key provided: mnfst_...` in extraction logs | `OPENAI_BASE_URL` env var not set \u2014 SDK sends gateway key to real `api.openai.com` | `setx OPENAI_BASE_URL \"https://your-gateway/v1\"` then **reboot Windows** (daemon inherits env at spawn time) |\n| Extraction returns empty (`direct client returned no result`) | Gateway forces SSE streaming, or reasoning model fills `reasoning_content` not `content` | Disable streaming on gateway side. Ensure `extractionMaxOutputTokens` \u2265 4096 (default 16384 is fine) |\n| `extraction fallback returned no parsed output` | Same as above \u2014 LLM responded but content field was empty | Same fix. Verify with `curl` test (see Step 5 gateway notes) |\n| QMD recall uses `fallbackUsed: true` / `source: \"recent_scan\"` | QMD `index.yml` path \u2260 server's configured `memoryDir` | Align `index.yml` `openclaw-engram.path` to match `remnic.memoryDir` from config. Re-run `qmd update &amp;&amp; qmd embed` |\n| `ggml-cuda.cu:98: CUDA error` during QMD reranking | Intermittent GPU crash in node-llama-cpp reranker (RTX cards) | Rerun usually works. Persistent? Set `QMD_FORCE_CPU=1` env var, or use `qmd query --no-rerank` |\n| `qmd doctor` shows `model cache: invalid ... not valid GGUF` | Corrupted `.etag` sidecar from older QMD version | `qmd pull --refresh`, then delete orphan `*.etag` files in `~/.cache/qmd/models/` |\n| `remnic daemon status` says `Running: no` but `netstat` shows :4318 LISTENING | **Fixed in 9.7.15.** On \u22649.3.x the server detached without writing `.remnic/server.pid`, so `daemon status` lied. | Upgrade to \u22659.7.15 (PID file + status now correct). On old versions: use `netstat -ano \\| findstr :4318`; do not run `daemon start` again (spawns a 2nd process) |\n| `remnic daemon install` \u2192 `Unsupported platform: win32` | No Windows service support yet | Use Task Scheduler (see Persistence section below) |\n| Doctor warns `OpenClaw config file ... (not found)` | OpenClaw not used in this setup | Ignore. Do **not** run `remnic openclaw install` |\n| `cliProbeError=spawn /opt/homebrew/bin/qmd ENOENT` in startup log AND `cli=true` later | Cosmetic \u2014 macOS path is probed first before configured path resolves | Ignore. Only matters if `cli=false` persists |\n| `npm install -g @tobilu/qmd` fails with `spawn C:\\WINDOWS\\system32\\cmd.exe ENOENT` during `better-sqlite3` postinstall | Conflicting global package manager (bun / `@nubjs/nub` / pnpm / yarn) is intercepting build scripts | Detect via `where bun; where nub; where pnpm; where yarn`. Remove with `npm uninstall -g bun @nubjs/nub nub` (and equivalents for pnpm/yarn) and `rm -rf ~/.bun`. Re-run step 1 |\n| `extractWithDirectClient: failed to parse JSON from response (first 200 chars: [\ud83e\udd9a Manifest M302] Model \"gpt-5.5\" is not available for this agent...)` then `extraction fallback returned no parsed output` | Remnic hardcodes `DEFAULT_REASONING_MODEL = \"gpt-5.5\"` (chunk filename is version-specific \u2014 grep for it in `@remnic/core/dist/`) and your gateway does not serve that model | Add `remnic.model: \"\"` to server config. Verify what's served via `curl -s $OPENAI_BASE_URL/models -H \"Authorization: Bearer $OPENAI_API_KEY\"`. Restart daemon. See step 3 `model` field |\n| `QMD warmup: complete` followed by `QMD update failed for collection X: qmd update timed out after 90000ms` on first daemon start | QMD models not yet downloaded when the daemon's startup sync runs \u2014 **prevented by Step 7 (`qmd pull` before daemon start)** | If Step 7 was skipped: run `qmd pull`, then `qmd update &amp;&amp; qmd embed`, then restart the daemon |\n| `qmd pull` reaches 99% then crashes with `ENOENT: rename .ipull -&gt; .gguf` (Node `triggerUncaughtException`) | QMD temp-file race on the final rename step | Re-run `qmd pull` \u2014 other models are cached, only the failed one re-downloads. Second run completes the rename successfully |\n| `no gateway default AI and local LLM disabled \u2014 hourly summarization disabled` | `gatewayConfig.agents.defaults.model.primary` not set AND no local LLM AND `modelSource` is not `\"gateway\"`. The fallback client has no model to use for scheduled/background functions. | Add `gatewayConfig` with `provider/model` format (e.g., `\"openai/minimax-m3\"`) to the `remnic` config block. See Issue G. Do NOT set `modelSource: \"gateway\"` \u2014 that breaks extraction. |\n| `fallback LLM: invalid model format: minimax-m3` | Fallback client's `parseModelString` requires `provider/model` format (e.g., `openai/minimax-m3`). A bare model ID like `minimax-m3` has no `/` separator \u2192 rejected. | Change `gatewayConfig.agents.defaults.model.primary` to `\"provider/\"` and add a matching provider config in `gatewayConfig.models.providers`. |\n| `fallback LLM: no models configured in gateway` | Two causes: (1) model string format invalid (see above), or (2) provider config missing from `gatewayConfig.models.providers` so `parseModelString` can't resolve provider \u2192 empty model chain. | Fix model string format AND add provider override with `baseUrl`, `api`, `apiKey`, `models: []`. See Issue G. |\n| `fallback LLM: timed out after 2500ms` | Fallback client has a hard 2500ms timeout. Cloud reasoning models (minimax-m3, etc.) typically need 3-5s to respond. | No config override found as of 9.7.15. This affects scheduled functions (hourly summaries) but NOT extraction (uses direct client). Accept as known limitation or switch to a faster model. |\n| Daemon PID unchanged after Pi host \"restart\" (same PID on :4318) | `remnic daemon start` spawns a DETACHED process that survives Pi host restarts. The old daemon holds stale config in memory. | Kill the process explicitly: `Stop-Process -Id  -Force` from an elevated PowerShell, then `remnic daemon start`. Use `netstat -ano | findstr :4318` to confirm PID changed. |\n| `modelSource: \"gateway\"` set \u2192 extraction fails with `fallback LLM: no models configured in gateway` | `modelSource: \"gateway\"` routes extraction through the fallback client. If `gatewayConfig` is incomplete (wrong model format or missing provider), extraction breaks. | Remove `modelSource: \"gateway\"` from config (leave unset). Extraction uses the direct client with bare `remnic.model`. Add `gatewayConfig` separately for scheduled functions. |\n| `cliPathSource=auto-path` in daemon log | Daemon loaded a config WITHOUT `qmdPath` (likely the Pi extension template or old diverged config). Falls back to PATH lookup. | Check which path `remnic config` resolves to, ensure THAT file has `qmdPath` and full schema. Sync all 3 config files. See Issue A. |\n| `remnic daemon status` shows a PID but `netstat` shows :4318 LISTENING under a DIFFERENT PID | An orphan/second daemon holds the port while `daemon status` reads a stale `server.pid` | Kill the port-holder (`Stop-Process -Id  -Force`) then `remnic daemon start`. Use `netstat -ano \\| findstr :4318` for the real PID |\n\n---\n\n## Decision points (LLM should ask if ambiguous)\n\n| Question | When to ask | Default if no answer |\n| --- | --- | --- |\n| Which Node install root? | Multiple Node installs detected via `where node` | Use first result from `where qmd` |\n| Which collection name? | Daemon log shows name \u2260 `openclaw-engram` | Use whatever the log emits literally |\n| User wants `setx QMD_PATH` env var persistence? | User asks for fix to persist across reboots | Skip \u2014 config file in step 3 is sufficient |\n| Auto-start daemon on boot? | User wants persistence across reboots | Register Task Scheduler entry (see Persistence section). No native service path exists on Windows |\n| Which LLM gateway? | `openaiApiKey` in config is not a real OpenAI key (e.g. `mnfst_...` prefix) | Ask user for gateway URL. Set both `openaiBaseUrl` in config AND `OPENAI_BASE_URL` env var |\n| Gateway uses reasoning models? | Extraction returns empty content but non-empty `reasoning_content` | Ensure streaming is off on gateway side. Default `extractionMaxOutputTokens: 16384` handles reasoning overhead |\n\n---\n\n## Reference: file/path inventory\n\nAfter successful setup, these files exist:\n\n```\n%USERPROFILE%\\.config\\remnic\\config.json     # Remnic config (documented modern \u2014 \u2265 9.3.x)\n%USERPROFILE%\\.config\\engram\\config.json     # Legacy config path (pre-rename fallback)\n%USERPROFILE%\\.pi\\agent\\remnic.config.json   # Server config the newer Remnic actually reads at runtime (when Pi connector is installed). Mirror the schema here.\n%USERPROFILE%\\.pi\\agent\\extensions\\remnic\\remnic.config.json  # Pi EXTENSION config (connector-facing: recall/observe + remnic_p\u2026 token). Different file; do not overwrite with the server schema.\n%USERPROFILE%\\.config\\qmd\\index.yml          # QMD collections\n%USERPROFILE%\\.remnic\\server.log             # Daemon log\n%USERPROFILE%\\.remnic\\server.pid             # PID file \u2014 written correctly in \u22659.7.15 (was NOT written \u22649.3.x)\n%USERPROFILE%\\.remnic\\memory\\                # DEFAULT store (used ONLY if memoryDir is unset \u2014 RETIRE/avoid; see Issue A). Two coexisting stores = split-brain.\n%USERPROFILE%\\.engram\\engram.db              # SQLite store (legacy dir name retained)\n%USERPROFILE%\\.pi\\agent\\memory\\              # Pi agent memory\n%USERPROFILE%\\.pi\\agent\\.remnic\\memory\\      # CANONICAL Remnic memory store (set this as memoryDir in ALL configs + QMD index.yml)\n\\qmd                            # POSIX wrapper (don't use as qmdPath on Windows)\n\\qmd.cmd                        # Windows wrapper (USE THIS as qmdPath)\n\\node_modules\\@tobilu\\qmd\\      # QMD package\n\\node_modules\\@remnic\\cli\\      # Remnic package\n```\n\n`` = output of `npm root -g`, typically `C:\\nvm4w\\nodejs\\node_modules` or `%APPDATA%\\npm\\node_modules`.\n\n---\n\n## One-shot validation script\n\nRun this after all steps. Every line must pass:\n\n```bash\n# 1. QMD reachable\nqmd --version | grep -q \"qmd 2.5.3\" &amp;&amp; echo \"OK qmd\" || echo \"FAIL qmd\"\n# 1a. QMD models pulled (REQUIRED before embed/query \u2014 Step 7); need &gt;=3 GGUFs cached\ntest \"$(ls \"$USERPROFILE/.cache/qmd/models/\"*.gguf 2&gt;/dev/null | wc -l)\" -ge 3 &amp;&amp; echo \"OK qmd models\" || echo \"FAIL qmd models (run: qmd pull)\"\n\n# 2. Remnic version (&gt;= 9.3.644 required; verified through 9.7.15)\n# NOTE: a naive grep regex breaks across minor/patch boundaries (e.g. it misses 9.7.15). Use a real numeric compare:\nV=$(npm list -g @remnic/cli 2&gt;/dev/null | grep -oE \"@remnic/cli@[0-9]+\\.[0-9]+\\.[0-9]+\" | head -1 | cut -d@ -f3)\nnode -e \"const m='9.3.644'.split('.').map(Number),v=(process.argv[1]||'0.0.0').split('.').map(Number);const ok=v[0]&gt;m[0]||(v[0]===m[0]&amp;&amp;(v[1]&gt;m[1]||(v[1]===m[1]&amp;&amp;v[2]&gt;=m[2])));console.log(v.some(isNaN)||!ok?'FAIL remnic '+process.argv[1]+' (need &gt;= 9.3.644)':'OK remnic '+process.argv[1])\" \"$V\"\n\n# 0. Extraction model set (REQUIRED \u2014 Remnic defaults to gpt-5.5 which most gateways don't serve)\n# Resolve the live server config the same way `remnic config` does\nRESOLVED_CFG=$(remnic config 2&gt;/dev/null | head -1 | sed 's/^Config: //')\nif [ -n \"$RESOLVED_CFG\" ] &amp;&amp; [ -f \"$RESOLVED_CFG\" ]; then\n  node -e \"const c=require(process.argv[1]);const m=c?.remnic?.model;if(typeof m==='string'&amp;&amp;m.length&gt;0){console.log('OK extraction model = '+m)}else{console.log('FAIL extraction model: add remnic.model to '+process.argv[1]);process.exit(1)}\" \"$RESOLVED_CFG\" || true\nelse\n  echo \"FAIL extraction model: cannot resolve server config (run: remnic config)\"\nfi\n\n# 0a. gatewayConfig set (REQUIRED for scheduled/background functions)\n# Without gatewayConfig, hourly summarizer is silently disabled\nif [ -n \"$RESOLVED_CFG\" ] &amp;&amp; [ -f \"$RESOLVED_CFG\" ]; then\n  node -e \"const c=require(process.argv[1]);const g=c?.remnic?.gatewayConfig?.agents?.defaults?.model?.primary;if(typeof g==='string'&amp;&amp;g.length&gt;0){console.log('OK gateway model = '+g)}else{console.log('FAIL gatewayConfig: add gatewayConfig.agents.defaults.model.primary to '+process.argv[1]);process.exit(1)}\" \"$RESOLVED_CFG\" || true\nelse\n  echo \"FAIL gatewayConfig: cannot resolve server config\"\nfi\n\n# 3. Config files (accept any of the 3 paths)\n( test -f \"$USERPROFILE/.config/remnic/config.json\" || test -f \"$USERPROFILE/.config/engram/config.json\" || test -f \"$USERPROFILE/.pi/agent/remnic.config.json\" ) &amp;&amp; echo \"OK remnic config\" || echo \"FAIL remnic config\"\n# 3a. NO home-root config (cwd pick-first trap \u2014 silent split-brain if present)\ntest -e \"$USERPROFILE/remnic.config.json\" &amp;&amp; echo \"FAIL home-root config present (delete it)\" || echo \"OK no home-root config\"\ntest -e \"$USERPROFILE/engram.config.json\" &amp;&amp; echo \"FAIL home-root engram config present (delete it)\" || true\n# 3b. all legitimate configs byte-identical (no divergence)\nA=$(md5sum \"$USERPROFILE/.config/remnic/config.json\" 2&gt;/dev/null | cut -d' ' -f1)\nB=$(md5sum \"$USERPROFILE/.config/engram/config.json\" 2&gt;/dev/null | cut -d' ' -f1)\nC=$(md5sum \"$USERPROFILE/.pi/agent/remnic.config.json\" 2&gt;/dev/null | cut -d' ' -f1)\n[ -n \"$A\" ] &amp;&amp; [ \"$A\" = \"$B\" ] &amp;&amp; [ \"$A\" = \"$C\" ] &amp;&amp; echo \"OK configs identical\" || echo \"FAIL configs diverge (sync all 3)\"\ntest -f \"$USERPROFILE/.config/qmd/index.yml\" &amp;&amp; echo \"OK qmd config\" || echo \"FAIL qmd config\"\n\n# 4. Server authToken present in config (REQUIRED \u2014 without it ALL requests get 401)\n# Check ALL three candidate paths, not just one\ngrep -q \"authToken\" \"$USERPROFILE/.config/remnic/config.json\" 2&gt;/dev/null \\\n  || grep -q \"authToken\" \"$USERPROFILE/.config/engram/config.json\" 2&gt;/dev/null \\\n  || grep -q \"authToken\" \"$USERPROFILE/.pi/agent/remnic.config.json\" 2&gt;/dev/null \\\n  &amp;&amp; echo \"OK auth token\" || echo \"FAIL auth token (add server.authToken to config)\"\n\n# 5. Pi connector installed (REQUIRED for Pi agent recall/observe)\ntest -f \"$USERPROFILE/.pi/agent/extensions/remnic/remnic.config.json\" &amp;&amp; echo \"OK pi connector\" || echo \"FAIL pi connector (run: remnic connectors install pi)\"\n\n# 6. LLM gateway env vars present (REQUIRED for extraction)\ntest -n \"$OPENAI_API_KEY\" &amp;&amp; echo \"OK api key\" || echo \"FAIL api key env\"\ntest -n \"$OPENAI_BASE_URL\" &amp;&amp; echo \"OK base url\" || echo \"FAIL base url env (setx then reboot)\"\n\n# 7. Daemon actually listening (`netstat` is authoritative; `remnic daemon status` also works in \u22659.7.15)\nnetstat -ano | findstr \":4318\" | findstr -q LISTENING &amp;&amp; echo \"OK daemon port\" || echo \"FAIL daemon port\"\n\n# 8. Backend probe in log\ngrep -q \"Search backend: available cli=true daemon=true session=true\" \"$USERPROFILE/.remnic/server.log\" &amp;&amp; echo \"OK backend\" || echo \"FAIL backend\"\n\n# 9. Collection indexed\nqmd update 2&gt;&amp;1 | grep -q \"All collections updated\" &amp;&amp; echo \"OK collections\" || echo \"FAIL collections\"\n\n# 10. End-to-end memory roundtrip\nremnic query \"test\" --json 2&gt;&amp;1 | grep -q '\"results\"' &amp;&amp; echo \"OK query\" || echo \"FAIL query\"\n\n# 11. LLM extraction working (no 401, facts auto-persisted)\ngrep -q \"persisted:.*facts\" \"$USERPROFILE/.remnic/server.log\" &amp;&amp; echo \"OK extraction\" || echo \"FAIL extraction (check OPENAI_BASE_URL + remnic.model + gateway streaming)\"\n\n# 12. Hourly summarizer enabled (no \"disabled\" warning in startup log)\n# Check the most recent startup for the hourly summarizer warning\ngrep -nE \"hourly summariz\" \"$USERPROFILE/.remnic/server.log\" | tail -1 | grep -q \"disabled\" &amp;&amp; echo \"FAIL hourly summarizer (add gatewayConfig, see Issue G)\" || echo \"OK hourly summarizer enabled\"\n\n# 13. Dreams pipeline running (lightSleep + deepSleep have runCount &gt; 0)\n# This requires MCP; skip if not available. LightSleep/deepSleep run on cadence, give it time.\n```\n\nEvery numbered line above must print `OK ...`. Any `FAIL` \u2192 see Troubleshooting matrix.\n\n---\n\n## MCP Tool Validation Results (Tested 2026-07-18; re-verified 2026-07-31 on @remnic/cli 9.7.15 / qmd 2.5.3 / Node v24.14.0)\n\nAll Remnic MCP tools were exercised against a live Windows daemon. Below: tools that work, tools that don't, and exact fixes.\n\n### Overall health\n\n- **Core recall/store/observe**: 100% functional\n- **QMD search**: functional (`memory_search` returns results, `recall` returns 0 because namespace is empty of matching memories)\n- **Work layer (tasks/projects/boards)**: 100% functional\n- **Config-gated features**: ~15 tools disabled by default \u2014 not bugs, need config overrides or preset switch\n- **Real bugs found**: 2 originally \u2014 config divergence (Issue A) **RESOLVED 2026-07-31** (root cause = cwd-resolution + home-root rogue config; fix documented in Issue A); MCP host field injection (Issue B) status unchanged as of 9.7.15\n\n### What works (no issues)\n\n| Category | Tools |\n|----------|-------|\n| Core recall | `recall`, `recall_explain`, `recall_tier_explain`, `recall_xray`, `memory_search`, `memory_get`, `memory_timeline` |\n| Memory write | `memory_store`, `observe`, `suggestion_submit`, `memory_promote`, `memory_outcome` |\n| Debug/introspection | `console_state`, `memory_last_recall`, `memory_intent_debug`, `memory_qmd_debug`, `memory_graph_explain`, `profiling_report`, `dreams_status` |\n| Work layer | `work_task` (all CRUD), `work_project` (all CRUD), `work_board` (export) |\n| Lifecycle maintenance | `memory_governance_run`, `procedure_mining_run`, `contradiction_scan_run`, `memory_summarize_hourly` |\n| Dreams pipeline | `dreams_run` (lightSleep/REM/deepSleep, dry-run) |\n| Peer registry | `peer_list`, `peer_set`, `peer_get`, `peer_delete` |\n| Query/entity | `entity_get`, `review_list`, `review_queue_list`, `capsule_list` |\n| Continuity (read) | `continuity_incident_list` (reports disabled cleanly) |\n| Identity | `memory_identity`, `identity_anchor_get` |\n| Correction | `memory_correct_plan`, `memory_correct_apply` |\n\n### Issues found and fixes\n\n#### Issue A \u2014 Config file divergence (CRITICAL)\n\n**Symptom:** `remnic config` resolves to `%USERPROFILE%\\.pi\\agent\\remnic.config.json`, but `.config\\remnic\\config.json` exists with **different** gateway credentials and no `model` field. LLM-dependent features (extraction, briefing follow-ups, correction planner) fail with `[\ud83e\udd9a Manifest M302] Model \"gpt-5.5\" is not available`. Graph recall stays disabled even after changing `memoryOsPreset` to `research-max`.\n\n**Root cause:** Three config files exist, but the **CLI and Pi embedded daemon resolve DIFFERENT paths**:\n\n- CLI command `remnic config` \u2192 `.pi\\agent\\remnic.config.json`\n- Pi embedded daemon (MCP host) \u2192 `.config\\remnic\\config.json`\n\nWhen they have different values, CLI tools see one config and MCP tools see another. The Pi daemon was loading `.config\\remnic\\config.json` (old Manifest gateway, `balanced` preset, no `model`), while CLI showed `.pi\\agent\\remnic.config.json` (updated Ollama gateway, `research-max` preset).\n\n**Deeper mechanism (verified 2026-07-31):** `resolveConfigPath()` checks `process.cwd()/remnic.config.json` **FIRST**, before any `~/.config/...` path. So whichever process launches the daemon determines the winner via its working directory. The most insidious case is a **fourth** file \u2014 a stray `remnic.config.json` sitting in `%USERPROFILE%` (the home root) \u2014 which wins whenever the daemon is launched from home (login Task Scheduler entry, manual `daemon start` from `~`, some MCP hosts). That rogue file typically has no `model` (\u2192 `gpt-5.5` 404), a different `memoryDir` (\u2192 a second, near-empty memory store at `~/.remnic/memory`), and `${OPENAI_API_KEY}`/`${REMNIC_AUTH_TOKEN}` placeholders. Symptom triplet: extraction errors with `model \"gpt-5.5\"`, recall returns paths under a different memory tree, and `remnic status` shows 401 (rogue config's empty token).\n\n**Fix:**\n\n1. **Delete any home-root config** (the cwd-pick-first trap \u2014 this is the step most guides miss):\n\n   ```bash\n   # MUST be absent. A home-root remnic.config.json is always a bug.\n   test -e \"$USERPROFILE/remnic.config.json\" &amp;&amp; mv \"$USERPROFILE/remnic.config.json\" \"$USERPROFILE/remnic.config.json.rogue-bak\" || echo \"no home-root config (good)\"\n   test -e \"$USERPROFILE/engram.config.json\" &amp;&amp; mv \"$USERPROFILE/engram.config.json\" \"$USERPROFILE/engram.config.json.rogue-bak\" || true\n   ```\n\n2. Sync ALL THREE legitimate config files to identical content:\n   - `%USERPROFILE%\\.config\\remnic\\config.json`\n   - `%USERPROFILE%\\.config\\engram\\config.json` (legacy fallback)\n   - `%USERPROFILE%\\.pi\\agent\\remnic.config.json`\n\n3. **Reconcile `memoryDir` if a divergent store was created.** If the rogue config wrote to `~/.remnic/memory` (the daemon default) while the canonical config uses `.pi\\agent\\.remnic\\memory`, merge any unique memory files (facts/checkpoints/corrections) from the abandoned store into the canonical one, point all configs + QMD `index.yml` at the canonical `memoryDir`, then archive the abandoned store and run `qmd update &amp;&amp; qmd embed`. Verify the abandoned store gets ZERO new writes after restart.\n\n4. After ANY config change, restart **both** the standalone daemon AND the Pi CLI.\n\n```bash\n# Sync all three (run from the directory where your master config lives)\ncp \"%USERPROFILE%\\.pi\\agent\\remnic.config.json\" \"%USERPROFILE%\\.config\\remnic\\config.json\"\ncp \"%USERPROFILE%\\.pi\\agent\\remnic.config.json\" \"%USERPROFILE%\\.config\\engram\\config.json\"\n\n# Verify\nremnic config | head -1\n# \u2192 Config: C:\\Users\\\\.pi\\agent\\remnic.config.json\n\n# Restart standalone daemon\nremnic daemon stop\nremnic daemon start\n\n# Then restart Pi CLI for MCP-visible changes to take effect\n```\n\n**Verify which config the Pi daemon actually loaded:**\n\n```bash\n# Check if qmdPath is configured (not auto-path)\ngrep -i \"cliPathSource\" \"%USERPROFILE%\\.remnic\\server.log\" | tail -1\n# expect: cliPathSource=configured  (not auto-path)\n\n# Check graph recall state via MCP\nremnic_recall_explain\n# expect: graphDecision.graphRecallEnabled=true, multiGraphMemoryEnabled=true\n```\n\n#### Issue B \u2014 MCP host injects `sessionKey` / `cwd` into tool calls (BUG)\n\n**Symptom:** Two tools fail with validation error:\n\n```\nrequest validation failed: (root): Unrecognized key(s) in object: 'sessionKey', 'cwd'\n```\n\n**Affected tools:**\n\n- `remnic_action_confidence`\n- `remnic_capsule_export`\n\n**Root cause:** The Pi MCP host injects `sessionKey` and `cwd` metadata fields into every tool call. These tools' JSON schemas have `additionalProperties: false`, rejecting the injected fields.\n\n**Fix:** This is a Remnic server-side schema bug. Report to maintainers. Workaround: use these tools via CLI (`remnic capsule export`, direct HTTP) instead of MCP.\n\n#### Issue C \u2014 Graph recall disabled by preset\n\n**Symptom:** `remnic_recall_explain` shows `graphRecallEnabled: false`, `multiGraphMemoryEnabled: false`, and graph section says `\"reason\": \"graph recall disabled by config\"`.\n\n**Root cause:** `memoryOsPreset: \"balanced\"` sets both flags to `false`. Only `research-max` preset enables them.\n\n**Fix:** Switch preset in the config file the daemon reads:\n\n```json\n{\n  \"remnic\": {\n    \"memoryOsPreset\": \"research-max\",\n    ...\n  }\n}\n```\n\nThen **restart the Pi CLI** (MCP host) \u2014 the host caches config at startup. Daemon restart alone is insufficient for MCP-visible changes.\n\n**Presets compared:**\n\n| Feature | `balanced` | `research-max` |\n|---------|-----------|----------------|\n| `graphRecallEnabled` | false | **true** |\n| `multiGraphMemoryEnabled` | false | **true** |\n| `graphAssistInFullModeEnabled` | false | **true** |\n| `proactiveExtractionEnabled` | false | **true** |\n| `contextCompressionActionsEnabled` | false | **true** |\n| `compressionGuidelineLearningEnabled` | false | **true** |\n| `lcmEnabled` | false | **true** |\n| `explicitCueRecallEnabled` | false | **true** |\n| `maxProactiveQuestionsPerExtraction` | 2 | **4** |\n| `maxCompressionTokensPerHour` | 1500 | **3000** |\n| `behaviorLoopAutoTuneEnabled` | false | **true** |\n\n**Caveat:** `research-max` enables everything \u2014 more LLM calls, more disk I/O, more background processing. For production use, prefer individual overrides instead:\n\n```json\n{\n  \"remnic\": {\n    \"memoryOsPreset\": \"balanced\",\n    \"graphRecallEnabled\": true,\n    \"multiGraphMemoryEnabled\": true,\n    \"feedbackEnabled\": true,\n    \"contextCompressionActionsEnabled\": true\n  }\n}\n```\n\nUser config values override preset values due to `{ ...preset, ...baseCfg }` merge in `parseConfig`.\n\n#### Issue D \u2014 LLM-dependent features degraded (gateway-side)\n\n**Symptom:** `remnic_briefing` generates sections but follow-ups fail with:\n\n```\nLLM follow-ups failed: Unexpected token '\ud83e\udd9a', \"[\ud83e\udd9a Manifest M302]\" is not valid JSON\n```\n\nAlso: `remnic_memory_correct_plan` warns:\n\n```\nLLM unavailable: correction classify+draft: local LLM unavailable\n```\n\n**Root cause (original finding \u2014 RESOLVED 2026-07-21, reconfirmed 2026-07-31):** Earlier testing concluded `openaiBaseUrl: \"https://ollama.com/v1\"` was the Ollama **website**, not the API endpoint.\n\n**Correction (verified 2026-07-21):** `https://ollama.com/v1` IS a valid OpenAI-compatible API endpoint. Live tests confirmed:\n\n- `GET /v1/models` \u2192 HTTP 200, returns JSON model list (including `minimax-m3`)\n- `POST /v1/chat/completions` with `minimax-m3` \u2192 HTTP 200, returns valid JSON with `content` field populated\n\nThe original failure was caused by **config divergence** (Issue A), NOT the URL being wrong. The daemon was loading a config WITHOUT `remnic.model`, so extraction fell back to hardcoded `gpt-5.5` \u2192 `not_found_error`. The HTTP response was HTML only because the gateway returned an error page for the missing model, not because the URL was a marketing page.\n\n**Fix:** If using local Ollama (no `ollama.com` cloud account):\n\n```json\n\"openaiBaseUrl\": \"http://localhost:11434/v1\",\n\"openaiApiKey\": \"ollama\"\n```\n\nIf using **Ollama cloud** (`ollama.com/v1`) or any other cloud gateway (Manifest, OpenRouter, etc.), ensure `openaiBaseUrl` matches the gateway's actual API endpoint AND that `remnic.model` is set to a model the gateway serves (see Issue G below).\n\n#### Issue E \u2014 Features disabled by config (expected, not bugs)\n\nThese tools return `\"enabled\": false` because they are gated by config flags. They are not broken \u2014 they need explicit opt-in:\n\n| Tool | Config flag to enable |\n|------|----------------------|\n| `memory_feedback` | `feedbackEnabled: true` |\n| `memory_action_apply` | `contextCompressionActionsEnabled: true` |\n| `continuity_incident_open` / `continuity_loop_add_or_update` | `identityContinuityEnabled: true` |\n| `identity_anchor_update` | `identityContinuityEnabled: true` |\n| `shared_context_write_output` / `shared_feedback_record` / `shared_priorities_append` / `shared_context_cross_signals_run` / `shared_context_curate_daily` | `sharedContextEnabled: true` |\n| `compounding_weekly_synthesize` / `compounding_promote_candidate` | `compoundingEnabled: true` |\n| `compression_guidelines_optimize` / `compression_guidelines_activate` | `compressionGuidelineLearningEnabled: true` |\n| `conversation_index_update` / `lcm_search` / `lcm_compaction_flush` / `lcm_compaction_record` | `lcmEnabled: true` |\n| `wearables_sync` | `wearables.enabled: true` |\n| `graph_edge_decay_run` | `graphEdgeDecayEnabled: true` |\n| `pattern_reinforcement_run` | `patternReinforcementEnabled: true` |\n| `live_connectors_run` | Per-connector config in `connectors:` block |\n\nAll of the above are enabled by default in `research-max` preset. Individual overrides also work.\n\n#### Issue F \u2014 `work_task` id type must be string\n\n**Symptom:** Passing `id: 1` (number) to `remnic_work_task` update fails with:\n\n```\n- id: must be string\n```\n\n**Fix:** Pass `id` as a string: `\"task-1784348023747-...\"` not `1`. The task IDs are auto-generated strings returned by `work_task` create.\n\n#### Issue G \u2014 Scheduled/background functions silently disabled without `gatewayConfig` (CRITICAL)\n\n**Symptom:** Server log at startup:\n\n```\nremnic: no gateway default AI and local LLM disabled \u2014 hourly summarization disabled\n```\n\nExtraction works (facts persisted via direct client), but scheduled/background functions that rely on the **fallback LLM client** are silently disabled:\n\n- **Hourly summarizer** \u2014 explicitly disabled by the check\n- **Dreams REM phase** \u2014 0 runs (may need the fallback client)\n- Other background LLM tasks that route through `FallbackLlmClient`\n\nDreams `lightSleep` and `deepSleep` phases still run (they process memory files locally, not via LLM).\n\n**Root cause:** Remnic has TWO separate LLM code paths:\n\n| Path | Used by | Config field | Model format |\n|------|---------|-------------|-------------|\n| **Direct client** | Extraction (fact/entity/profile extraction) | `remnic.model` (string) | Bare model ID, e.g. `minimax-m3` |\n| **Fallback client** (`FallbackLlmClient`) | Hourly summaries, some Dreams phases, calibration, causal consolidation | `gatewayConfig.agents.defaults.model.primary` | `provider/model` format, e.g. `openai/minimax-m3` |\n\nThe `HourlySummarizer` constructor (`@remnic/core/src/summarizer.ts:73`) checks:\n\n```typescript\nif (!gatewayConfig?.agents?.defaults?.model?.primary &amp;&amp; !resolveLocalLlmCapabilities(config).localLlm &amp;&amp; config.modelSource !== \"gateway\") {\n  log.warn(\"no gateway default AI and local LLM disabled \u2014 hourly summarization disabled\");\n}\n```\n\nWhen ALL THREE conditions are true (no `gatewayConfig` model, no local LLM, `modelSource \u2260 \"gateway\"`), the summarizer is disabled. Setting `remnic.model` alone does NOT satisfy this check \u2014 it only feeds the direct client.\n\n**\u26a0 Do NOT set `modelSource: \"gateway\"` as a workaround.** While it satisfies the third condition (silencing the warning), it also routes **extraction** through the fallback client instead of the direct client. The fallback client's `parseModelString` requires `provider/model` format \u2014 a bare `minimax-m3` \u2192 `fallback LLM: invalid model format` \u2192 extraction breaks. You get the summarizer enabled but extraction broken. The correct fix is `gatewayConfig` (below).\n\n**Fix:** Add `gatewayConfig` to the `remnic` config block. The model string MUST be in `provider/model` format. The provider config overrides the built-in `openai` defaults (which point at `api.openai.com/v1`):\n\n```json\n{\n  \"remnic\": {\n    \"model\": \"minimax-m3\",\n    \"gatewayConfig\": {\n      \"agents\": {\n        \"defaults\": {\n          \"model\": {\n            \"primary\": \"openai/minimax-m3\"\n          }\n        }\n      },\n      \"models\": {\n        \"providers\": {\n          \"openai\": {\n            \"baseUrl\": \"https://your-gateway.example.com/v1\",\n            \"api\": \"openai-completions\",\n            \"apiKey\": \"same-key-as-openaiApiKey\",\n            \"models\": []\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nKey details:\n\n- `gatewayConfig.agents.defaults.model.primary` must be `\"openai/\"` (with `/` separator). The `openai` prefix maps to the provider config.\n- `gatewayConfig.models.providers.openai.baseUrl` overrides the built-in default (`api.openai.com/v1`). Point at your actual gateway.\n- `gatewayConfig.models.providers.openai.api` must be `\"openai-completions\"` for OpenAI-compatible chat completions. Do NOT use `\"openai-responses\"` (the built-in default) unless your gateway serves the Responses API.\n- `gatewayConfig.models.providers.openai.apiKey` can be the same value as `remnic.openaiApiKey`. The fallback client resolves it via `resolveProviderApiKey` which checks provider config first.\n- Do NOT set `remnic.modelSource` \u2014 leave it unset. Extraction uses the direct client (with bare `remnic.model`), scheduled functions use the fallback client (with `provider/model` format from `gatewayConfig`).\n\n**Verify after fix + restart:**\n\n```bash\n# Startup log should show \"hourly summarizer initialized\" WITHOUT the disabled warning\ngrep -E \"hourly summarizer\" \"$USERPROFILE/.remnic/server.log\" | tail -2\n# expect: hourly summarizer initialized   (NOT \"hourly summarization disabled\")\n\n# Fallback client should recognize the model (no \"invalid model format\")\ngrep -E \"invalid model format|no models configured\" \"$USERPROFILE/.remnic/server.log\" | tail -2\n# expect: no matches (or old entries only)\n\n# Dreams status should show activity\nremnic_dreams_status\n# expect: lightSleep and deepSleep runCount &gt; 0\n```\n\n**Known limitation \u2014 fallback client timeout:** After fixing the model format, the fallback client may still fail with `fallback LLM: timed out after 2500ms`. The fallback client has a hard 2500ms timeout which is too short for reasoning models on cloud gateways (direct client gets responses in 3-5s). This affects hourly summary generation but does NOT affect extraction (which uses the direct client). This is a known issue \u2014 no config override found as of 9.7.15.\n\n### Config file after testing (verified working)\n\nThis is the minimal config that resolves Issues A through G (Ollama-cloud example \u2014 `minimax-m3` verified working 2026-07-31; substitute your gateway's served model ID \u2014 Manifest uses `auto`):\n\n```json\n{\n  \"qmdPath\": \"C:\\\\nvm4w\\\\nodejs\\\\qmd.cmd\",\n  \"remnic\": {\n    \"qmdPath\": \"C:\\\\nvm4w\\\\nodejs\\\\qmd.cmd\",\n    \"openaiApiKey\": \"\",\n    \"openaiBaseUrl\": \"https://your-gateway.example.com/v1\",\n    \"model\": \"minimax-m3\",\n    \"memoryDir\": \"C:\\\\Users\\\\\\\\.pi\\\\agent\\\\.remnic\\\\memory\",\n    \"memoryOsPreset\": \"research-max\",\n    \"feedbackEnabled\": true,\n    \"identityContinuityEnabled\": true,\n    \"sharedContextEnabled\": true,\n    \"compoundingEnabled\": true,\n    \"compressionGuidelineLearningEnabled\": true,\n    \"lcmEnabled\": true,\n    \"graphEdgeDecayEnabled\": true,\n    \"patternReinforcementEnabled\": true,\n    \"wearables\": { \"enabled\": true },\n    \"gatewayConfig\": {\n      \"agents\": {\n        \"defaults\": {\n          \"model\": {\n            \"primary\": \"openai/minimax-m3\"\n          }\n        }\n      },\n      \"models\": {\n        \"providers\": {\n          \"openai\": {\n            \"baseUrl\": \"https://your-gateway.example.com/v1\",\n            \"api\": \"openai-completions\",\n            \"apiKey\": \"\",\n            \"models\": []\n          }\n        }\n      }\n    }\n  },\n  \"server\": {\n    \"host\": \"127.0.0.1\",\n    \"port\": 4318,\n    \"authToken\": \"&lt;64-char-hex&gt;\"\n  },\n  \"engram\": { \"qmdPath\": \"C:\\\\nvm4w\\\\nodejs\\\\qmd.cmd\" }\n}\n```\n\n&gt; \u26a0 **After any config change, restart BOTH the daemon AND the Pi CLI** for MCP-visible changes to take effect. Daemon restart alone only affects CLI commands, not the MCP tool surface.\n\n---\n\n## Background context (skip if executing only)\n\n- The Windows blocker is GitHub issue [joshuaswarren/remnic#1476](https://github.com/joshuaswarren/remnic/issues/1476), fixed in PR #1486 via `cross-spawn` in 9.3.644.\n- Pre-fix, `child_process.spawn` on Windows could not exec `.cmd` (EINVAL), POSIX shell wrappers (ENOENT), or bare JS files (ENOENT).\n- Post-fix, all three launcher forms work, but `.cmd` is the canonical choice on Windows.\n- Remnic auto-probe still falls back to `/opt/homebrew/bin/qmd` if PATH lookup fails \u2014 this is harmless once the configured path takes priority via step 3.\n- The `engram \u2192 remnic` rename moved the config dir from `.config/engram/` to `.config/remnic/` but kept the SQLite store at `%USERPROFILE%\\.engram\\engram.db` and the daemon log at `%USERPROFILE%\\.remnic\\server.log`. The mismatch is intentional \u2014 do not try to consolidate them.\n- **Windows status bugs, version-split:** On \u22649.3.x the daemon detached without writing `.remnic/server.pid`, so `remnic daemon status` reported `Running: no` even while listening \u2014 on those versions use `netstat`. **Fixed in 9.7.15**: the PID file is written and `daemon status` reports correctly (verified 2026-07-31). Still no `daemon install` service support on Windows (use Task Scheduler \u2014 see Persistence). The cosmetic `cliProbeError=spawn /opt/homebrew/bin/qmd ENOENT` macOS-path probe still appears in startup logs and is harmless.\n- **Server config path divergence (\u2265 9.3.7xx):** `remnic config` may resolve to `%USERPROFILE%\\.pi\\agent\\remnic.config.json` instead of the documented `%USERPROFILE%\\.config\\remnic\\config.json` \u2014 because `resolveConfigPath()` checks `process.cwd()/remnic.config.json` first (see the cwd-resolution rule above). If that file holds `${OPENAI_API_KEY}` / `${REMNIC_AUTH_TOKEN}` placeholders and no `qmdPath` / `openaiBaseUrl` / `model`, the server loads a placeholder config \u2192 `cliPathSource=auto-path` + `no gateway default AI`. Fix: overwrite THAT resolved file with the full schema (real values, not env placeholders). Note: `remnic connectors install pi` writes its connector-facing file at `.pi\\agent\\extensions\\remnic\\remnic.config.json` (recall/observe settings + a `remnic_p\u2026` token) \u2014 it does NOT write `.pi\\agent\\remnic.config.json`; the two files are distinct and the server config is always user-authored.\n- **Config resolution is cwd-dependent (root cause of all split-brain, verified 2026-07-31):** `resolveConfigPath()` order is `--config` flag \u2192 `REMNIC_CONFIG_PATH`/`ENGRAM_CONFIG_PATH` env \u2192 `process.cwd()/remnic.config.json` \u2192 `process.cwd()/engram.config.json` \u2192 `~/.config/remnic/config.json` \u2192 `~/.config/engram/config.json`. The `process.cwd()` candidates come FIRST, so the daemon's effective config depends on its launch working directory. A stray `remnic.config.json` in the home root (`%USERPROFILE%`) wins for any daemon launched from home and silently overrides the canonical config \u2014 producing a second memory store (`~/.remnic/memory`, the daemon default), `gpt-5.5` extraction 404s, and `remnic status` 401. Hard rule: never keep a config in the home root; delete it so fallthrough always reaches `~/.config/remnic/config.json`.\n- **Corrupt `meta.json` \u2192 silent per-extraction noise:** `state/meta.json` (extraction retry ledger) can be written as all-NUL bytes (e.g. 136K of `\\0`) after a crash/aborted write. `JSON.parse` then throws `SyntaxError: Unexpected token ' '` on every extraction (`runExtraction: failed to load meta for retry bookkeeping (non-fatal)`). It is non-fatal, but it disables retry bookkeeping and floods the log. Fix: move the file aside (`meta.json` \u2192 `meta.json.nul-corrupt.bak`); the server recreates a fresh valid one on the next write.\n- **Extraction model default is `gpt-5.5` (hardcoded):** `DEFAULT_REASONING_MODEL = \"gpt-5.5\"`. The chunk filename is **version-specific** \u2014 it was `chunk-2HAG7JOI.js:1067` in 9.3.x and is `chunk-INELTPRG.js:1187` in 9.7.15. Do not trust a pinned filename; find yours with `grep -rn \"DEFAULT_REASONING_MODEL =\" \"$(npm root -g)/@remnic/cli/node_modules/@remnic/core/dist/\"`. The override key is `remnic.model` (string) in the server config \u2014 parseConfig: `const explicitModel = typeof cfg.model === \"string\" &amp;&amp; cfg.model.length &gt; 0 ? cfg.model : \"\"; const model = explicitModel || DEFAULT_REASONING_MODEL;`. Most non-OpenAI gateways do NOT serve `gpt-5.5` \u2192 extraction 404s with a gateway-specific error (Manifest `M302`, Ollama-cloud/OpenRouter `404 model \"gpt-5.5\" not found`, etc.). Always set `remnic.model` to a model your gateway actually serves.\n- **`setx` vs shell `export` for daemon env:** `setx` writes to `HKCU\\Environment` (persists, future processes). But `remnic daemon start` forks a detached process that inherits the *spawning shell's* env, not the registry. So in the same session you must `export` the vars into the shell before `daemon start`, or reboot. See step 6 workaround.\n- **Detached daemon survives Pi host restarts:** `remnic daemon start` spawns a detached `node` process (not a child of the Pi host). When the Pi host \"restarts\", this process survives and holds stale config in memory. If the daemon doesn't pick up config changes after restart, check `netstat -ano | findstr :4318` \u2014 if the PID hasn't changed, the old daemon is still running. Kill it from an elevated shell: `Stop-Process -Id  -Force` (or `taskkill //F //PID `), then start fresh.\n- **Two LLM code paths (direct vs fallback):** Remnic routes LLM calls through two different clients:\n  - **Direct client** \u2014 used for extraction (facts, entities, profiles). Reads `remnic.model` (bare model ID, e.g. `minimax-m3`). Built on the OpenAI SDK directly.\n  - **Fallback client** (`FallbackLlmClient`) \u2014 used for hourly summaries, some Dreams phases, calibration, causal consolidation. Reads `gatewayConfig.agents.defaults.model.primary` (must be `provider/model` format, e.g. `openai/minimax-m3`). Supports multiple providers and model chains.\n  These are independent. Setting `remnic.model` alone does NOT satisfy the fallback client's needs. Conversely, setting `modelSource: \"gateway\"` forces extraction onto the fallback client, which will break if the model string isn't in `provider/model` format. Leave `modelSource` unset and configure both `model` (direct) and `gatewayConfig` (fallback) separately.\n- **Fallback client model string format:** `parseModelString` in `@remnic/core/src/fallback-llm.ts` splits on `/` and expects `provider/model`. A bare model ID like `minimax-m3` \u2192 `fallback LLM: invalid model format` \u2192 empty model chain \u2192 `fallback LLM: no models configured in gateway`. The `provider` prefix must map to a provider config in `gatewayConfig.models.providers` (e.g., `openai` \u2192 `{ baseUrl, api, apiKey, models: [] }`). Built-in providers (`openai`, `anthropic`) default to their public URLs \u2014 override `baseUrl` to point at your gateway.\n- **Fallback client 2500ms timeout:** After fixing model format and provider config, the fallback client may still fail with `fallback LLM: timed out after 2500ms`. This is a hardcoded timeout in the fallback client (`@remnic/core/src/fallback-llm.ts`). Reasoning models on cloud gateways typically need 3-5s. This affects scheduled functions (hourly summaries) but NOT extraction (which uses the direct client). No config override found as of 9.7.15.\n- **`cliPathSource=auto-path` diagnostic signal:** If the daemon startup log shows `cliPathSource=auto-path` (instead of `configured`), it loaded a config WITHOUT `qmdPath`. This means it fell back to PATH lookup. Common causes: (1) Pi extension template loaded instead of real config, (2) wrong config file, (3) config file missing `qmdPath`. Fix: check which path `remnic config` resolves to, ensure THAT file has `qmdPath`.\n- **Dreams pipeline cadence:** `lightSleep` and `deepSleep` run on a timer (every few hours). `rem` phase runs less frequently and may need more memory volume or longer daemon uptime before triggering. Zero runs in the first few hours is normal.\n\n---\n\n## Persistence (optional \u2014 auto-start daemon on boot)\n\nNo native Windows service path exists. Register a Task Scheduler entry:\n\n```powershell\n$action  = New-ScheduledTaskAction -Execute \"C:\\nvm4w\\nodejs\\remnic.cmd\" -Argument \"daemon start\"\n$trigger = New-ScheduledTaskTrigger -AtLogOn -User \"$env:USERNAME\"\n$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd\nRegister-ScheduledTask -TaskName \"Remnic Daemon\" -Action $action -Trigger $trigger -Settings $settings -Description \"Start Remnic memory daemon at login\"\n```\n\n**Verify after next login:**\n\n```bash\nnetstat -ano | findstr \":4318\" | findstr LISTENING\n```\n\nRemove with: `Unregister-ScheduledTask -TaskName \"Remnic Daemon\" -Confirm:$false`\n\n---\n\n**End of instructions.** Execute steps 1\u201310 sequentially, verify after each, stop on first failure and consult the matrix.\n", "creation_timestamp": "2026-07-31T05:50:06.151340Z"}