memorix 1.6.0 → 1.6.2

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/cli/index.js +1888 -1122
  3. package/dist/cli/memcode.js +12 -1
  4. package/dist/index.js +641 -113
  5. package/dist/maintenance-runner.js +618 -90
  6. package/dist/media-audio-runner.js +618 -90
  7. package/dist/media-video-runner.js +618 -90
  8. package/dist/memcode-runtime/CHANGELOG.md +46 -0
  9. package/dist/sdk.js +674 -117
  10. package/dist/vector-backfill-runner.js +618 -90
  11. package/docs/AGENT_OPERATOR_PLAYBOOK.md +11 -3
  12. package/docs/API_REFERENCE.md +1 -1
  13. package/docs/DOCKER.md +5 -1
  14. package/docs/PERFORMANCE.md +17 -2
  15. package/docs/README.md +3 -1
  16. package/docs/SETUP.md +8 -2
  17. package/package.json +2 -1
  18. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  19. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  20. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/copilot/memorix/plugin.json +1 -1
  22. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  23. package/plugins/omp/memorix/package.json +1 -1
  24. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/pi/memorix/package.json +1 -1
  26. package/server.json +2 -2
  27. package/src/cli/commands/agent-integrations.ts +9 -3
  28. package/src/cli/commands/background.ts +34 -2
  29. package/src/cli/commands/doctor.ts +46 -15
  30. package/src/cli/commands/hook.ts +13 -1
  31. package/src/cli/commands/operator-shared.ts +6 -3
  32. package/src/cli/commands/serve-http.ts +72 -7
  33. package/src/cli/heap.ts +76 -0
  34. package/src/cli/invocation.ts +7 -4
  35. package/src/embedding/api-provider.ts +503 -27
  36. package/src/embedding/provider.ts +96 -9
  37. package/src/hooks/handler.ts +9 -18
  38. package/src/hooks/lifecycle.ts +174 -0
  39. package/src/memory/observations.ts +68 -18
  40. package/src/sdk.ts +12 -8
  41. package/src/store/orama-store.ts +21 -3
@@ -58,7 +58,7 @@ An unbound terminal is intentionally project-scoped: it reads and writes project
58
58
 
59
59
  Long-term memory is a reviewed layer, not a generic global note store. `memorix memory long-term add` creates a candidate; `qualify` and `approve` are explicit audit transitions before it can appear in a bounded task Workset. A project-derived observation, code fact, Git fact, test, session, Claim, or workflow stays project-bound. Only manual or user-confirmed `user + portable` records can cross local projects for the same local installation identity. When one is task-matched in a Workset, it is intentionally usable background even if its origin differs; use its `durable:<id>` reference with `memorix_detail` only when the full record is needed. Use `archive` or `supersede` when a durable item is no longer current.
60
60
 
61
- `--cwd <git-project>` is the canonical way to run direct CLI work from outside a repository. It changes the command's project anchor only; Git remains the source of truth for final project identity.
61
+ `--cwd <git-project>` is the canonical way to run direct CLI work from outside a repository. It changes the command's project anchor only; Git remains the source of truth for final project identity. Existing directories are canonicalized to their real filesystem paths before binding, which prevents macOS aliases such as `/var` and `/private/var` from splitting one project or hiding a Claude local MCP entry.
62
62
 
63
63
  Use MCP when:
64
64
 
@@ -164,7 +164,12 @@ Do not assume the HTTP connection alone tells Memorix which project the user mea
164
164
 
165
165
  The HTTP service is normally started with `memorix background start`; the same project-binding rules apply when you run `memorix serve-http --port 3211` in the foreground.
166
166
 
167
- HTTP MCP sessions idle out after 30 minutes by default. If the user's HTTP MCP client is sensitive to stale session IDs after long idle periods, set `MEMORIX_SESSION_TIMEOUT_MS` before starting or restarting the service. Example: `MEMORIX_SESSION_TIMEOUT_MS=86400000` keeps sessions alive for 24 hours.
167
+ HTTP MCP sessions idle out after 12 hours by default. An expired session now
168
+ receives a fast `404` reinitialize hint instead of a generic transport failure.
169
+ If the user's HTTP MCP client needs a longer idle period, set
170
+ `MEMORIX_SESSION_TIMEOUT_MS` before starting or restarting the service.
171
+ Example: `MEMORIX_SESSION_TIMEOUT_MS=86400000` keeps sessions alive for 24
172
+ hours. Set it to `0` only when the host has its own reliable session cleanup.
168
173
 
169
174
  ### Do not confuse project config and global config
170
175
 
@@ -702,7 +707,10 @@ If it shows "Not running" or "dead":
702
707
  memorix background ensure
703
708
  ```
704
709
 
705
- If the client is connected but starts failing after roughly 30 minutes of no Memorix tool use, check for stale HTTP session expiry rather than treating it as project binding failure. Restart the service with a longer idle timeout:
710
+ If the client is connected but starts failing after a long inactive period,
711
+ check for the explicit stale-session `404` reinitialize hint rather than
712
+ treating it as a project-binding failure. The default idle window is 12 hours;
713
+ restart with a longer window only when the client genuinely needs it:
706
714
 
707
715
  ```powershell
708
716
  $env:MEMORIX_SESSION_TIMEOUT_MS = "86400000"
@@ -652,7 +652,7 @@ Coordination state is opt-in project state for tasks, handoff messages, locks, a
652
652
 
653
653
  Runtime environment:
654
654
 
655
- - `MEMORIX_SESSION_TIMEOUT_MS` — HTTP MCP session idle timeout in milliseconds. Default: `1800000` (30 minutes). Increase this for clients that do not transparently reinitialize after stale HTTP session IDs, for example `86400000` for 24 hours.
655
+ - `MEMORIX_SESSION_TIMEOUT_MS` — HTTP MCP session idle timeout in milliseconds. Default: `43200000` (12 hours). Expired session IDs receive a fast `404` with a reinitialize hint. Increase this for long-lived clients, for example `86400000` for 24 hours; set `0` only when the host provides reliable lifecycle cleanup.
656
656
 
657
657
  ### `team_manage`
658
658
 
package/docs/DOCKER.md CHANGED
@@ -128,7 +128,11 @@ docker compose ps
128
128
 
129
129
  ### HTTP session idle timeout
130
130
 
131
- The HTTP MCP transport defaults to a 30-minute idle timeout. The example `compose.yaml` raises this to 24 hours for IDEs that keep one HTTP MCP session open while the user reads diffs, runs tests, or switches focus.
131
+ The HTTP MCP transport defaults to a 12-hour idle timeout. The example
132
+ `compose.yaml` raises this to 24 hours for IDEs that keep one HTTP MCP session
133
+ open while the user reads diffs, runs tests, or switches focus. When a session
134
+ does expire, the service returns a fast `404` so the client can initialize a
135
+ fresh MCP session.
132
136
 
133
137
  To change it:
134
138
 
@@ -71,13 +71,14 @@ On the release development machine used for this check, the healthy HTTP service
71
71
 
72
72
  | Knob | Default | Use When |
73
73
  | --- | --- | --- |
74
- | `MEMORIX_SESSION_TIMEOUT_MS` | `1800000` (30 min) | Increase for HTTP MCP clients that do not recover from stale session IDs after idle time |
74
+ | `MEMORIX_SESSION_TIMEOUT_MS` | `43200000` (12 h) | Set a shorter GC window for supervised clients, or `0` to disable idle session GC |
75
75
  | `MEMORIX_FORMATION_TIMEOUT_MS` | `12000` (12 s) | Raise when LLM-backed formation should outlive slow proxy/provider hops |
76
76
  | `MEMORIX_LLM_API_KEY` / `OPENAI_API_KEY` | unset | Enable LLM-backed enrichment, extraction, rerank, or skill generation |
77
77
  | `MEMORIX_LLM_TIMEOUT_MS` | `30000` (30 s) | Bound a single LLM-backed extraction/resolve call |
78
78
  | `MEMORIX_RERANK_TIMEOUT_MS` | provider default | Bound slow LLM rerank calls |
79
79
  | `memorix memory search --quality fast` | n/a | Force a fully local retrieval path for a latency-sensitive call |
80
80
  | `npm run benchmark:retrieval -- --records 1000 --runs 100` | n/a | Reproduce hot in-process lexical retrieval latency; not an end-to-end claim |
81
+ | `npm run gate:large-store -- --records 40000` | n/a | Exercise SDK, HTTP MCP, hook persistence, cache integrity, and reopen behavior against a large isolated store |
81
82
  | `[codegraph].max_file_bytes` | `2097152` | Raise only when a large file is intentional source that should enter Code Memory |
82
83
  | `memorix retention status` | report only | Inspect whether memory growth needs cleanup |
83
84
  | `memorix retention archive` | explicit | Archive expired memories when the project gets noisy |
@@ -86,7 +87,7 @@ On the release development machine used for this check, the healthy HTTP service
86
87
  ## Operator Guidance
87
88
 
88
89
  - For memory-only use, prefer stdio MCP or a lightweight `memorix_session_start`; do not join orchestration coordination state by default.
89
- - For long-lived IDE sessions over HTTP, set `MEMORIX_SESSION_TIMEOUT_MS=86400000` before `memorix background start` if your client is stale-session-sensitive.
90
+ - HTTP sessions now default to 12 hours and return a fast `404` with a reinitialize hint after expiry. Set `MEMORIX_SESSION_TIMEOUT_MS=0` only when the host has its own reliable lifecycle cleanup; otherwise retain a bounded GC window.
90
91
  - If LLM-backed formation is timing out against a slow proxy/provider, raise `MEMORIX_FORMATION_TIMEOUT_MS` and keep it higher than `MEMORIX_LLM_TIMEOUT_MS`, because the full pipeline can include multiple LLM-backed stages.
91
92
  - For Docker, use it when you want a managed HTTP service. Do not use image size alone as the runtime memory estimate.
92
93
  - For orchestrated subagent work, expect CPU and disk activity proportional to the spawned agents and verification commands.
@@ -96,6 +97,20 @@ On the release development machine used for this check, the healthy HTTP service
96
97
  `/api/maintenance` on that local dashboard before assuming a Code Memory scan
97
98
  or lifecycle task completed.
98
99
 
100
+ ## Large-Store Release Gate
101
+
102
+ `npm run gate:large-store -- --records 40000` builds Memorix and creates a
103
+ temporary Git project with an isolated data directory. It runs with embeddings
104
+ disabled, so it does not use provider credentials or make paid network calls.
105
+ The gate measures steady-state writes, first lexical search, process memory,
106
+ HTTP MCP and hook persistence, and an SDK close/reopen cycle. It also verifies
107
+ that the personal hook candidate remains durable on disk without becoming
108
+ visible to an unbound SDK reader.
109
+
110
+ Results are machine-specific and should be compared on the same OS and Node
111
+ version. The release gate is an integrity and regression check, not a public
112
+ latency guarantee.
113
+
99
114
  ## Current Optimization Opportunities
100
115
 
101
116
  These are not release blockers, but they are reasonable future improvements:
package/docs/README.md CHANGED
@@ -117,11 +117,13 @@ Historical/deep-reference documents may describe older designs. If they conflict
117
117
 
118
118
  ## Current Product Line
119
119
 
120
- The released product is on the **1.4 line**. The current baseline has:
120
+ The released product is on the **1.6 line**. The current baseline has:
121
121
 
122
122
  - `memorix setup --agent <agent> --global` is the default agent integration command
123
123
  - `memorix serve` is the manual stdio MCP server for external agents
124
124
  - `memorix background start` runs the shared HTTP MCP service and dashboard
125
+ - context and CodeGraph commands return bounded task receipts before detailed diagnostics
126
+ - HTTP MCP sessions have a bounded, explicit lifecycle and fail with a reinitialize hint after expiry
125
127
  - `memorix integrate --agent <agent>` and `memorix hooks install --agent <agent>` remain manual/fallback generation commands
126
128
  - `memorix` / `memcode` open memcode, the bundled terminal agent that uses the same Memorix memory pool
127
129
  - `~/.memorix/config.toml` and project `memorix.toml` are the user-facing configuration model
package/docs/SETUP.md CHANGED
@@ -42,7 +42,7 @@ Memorix project identity comes from Git. Open or initialize a real Git repositor
42
42
  git init
43
43
  ```
44
44
 
45
- `projectRoot` and current working directory are detection anchors. The final project identity is derived from the real Git root and remote metadata.
45
+ `projectRoot` and current working directory are detection anchors. The final project identity is derived from the real Git root and remote metadata. Memorix resolves an existing project directory to its real filesystem path before binding it, so equivalent macOS paths such as `/var/...` and `/private/var/...` do not create separate project or Claude local-MCP entries.
46
46
 
47
47
  ---
48
48
 
@@ -247,13 +247,19 @@ Generic HTTP MCP config:
247
247
 
248
248
  For multi-project HTTP usage, agents should call `memorix_session_start(projectRoot=...)` with the absolute workspace path when available. This prevents cross-project drift when the background service is shared.
249
249
 
250
- HTTP sessions idle out after 30 minutes by default. For clients that do not recover gracefully from stale HTTP session IDs:
250
+ HTTP sessions idle out after 12 hours by default. An expired session receives a
251
+ fast `404` that tells the client to initialize again, rather than being treated
252
+ as a project-binding failure. For clients that intentionally need a longer
253
+ window:
251
254
 
252
255
  ```powershell
253
256
  $env:MEMORIX_SESSION_TIMEOUT_MS = "86400000"
254
257
  memorix background restart
255
258
  ```
256
259
 
260
+ Set `MEMORIX_SESSION_TIMEOUT_MS=0` only when the host has its own reliable
261
+ session cleanup; otherwise keep a bounded idle window.
262
+
257
263
  ### Option E: Docker HTTP service
258
264
 
259
265
  ```bash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memorix",
3
3
  "mcpName": "io.github.AVIDS2/memorix",
4
- "version": "1.6.0",
4
+ "version": "1.6.2",
5
5
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -67,6 +67,7 @@
67
67
  "test:e2e": "vitest run tests/e2e/",
68
68
  "benchmark:codegraph": "node scripts/benchmark-codegraph-provider.cjs",
69
69
  "benchmark:retrieval": "npm run build && node scripts/benchmark-retrieval.mjs",
70
+ "gate:large-store": "npm run build && node scripts/large-store-gate.mjs",
70
71
  "generate:star-history": "node scripts/generate-star-history.mjs",
71
72
  "sync:mcp-registry": "node scripts/sync-mcp-registry-manifest.mjs",
72
73
  "check:mcp-registry": "node scripts/check-mcp-registry-manifest.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memorix",
4
- "version": "1.6.0",
4
+ "version": "1.6.2",
5
5
  "description": "Shared workspace memory for Claude Code and other AI coding agents.",
6
6
  "author": {
7
7
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Local-first project memory for CodeBuddy Code and other coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Shared workspace memory for Codex and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Shared local workspace memory for GitHub Copilot CLI and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/AVIDS2/memorix",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.6.0",
10
+ "version": "1.6.2",
11
11
  "websiteUrl": "https://github.com/AVIDS2/memorix#readme",
12
12
  "icons": [
13
13
  {
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "memorix",
25
- "version": "1.6.0",
25
+ "version": "1.6.2",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, realpathSync } from 'node:fs';
3
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { basename, dirname, resolve } from 'node:path';
5
5
  import { homedir } from 'node:os';
@@ -580,11 +580,17 @@ function sanitizeServer(server: MCPServerEntry): AgentMcpCheck['server'] {
580
580
  }
581
581
 
582
582
  function normalizeProjectKey(projectRoot: string): string {
583
- return resolve(projectRoot).replace(/\\/g, '/').toLowerCase();
583
+ return canonicalProjectPath(projectRoot).toLowerCase();
584
584
  }
585
585
 
586
586
  function defaultClaudeProjectKey(projectRoot: string): string {
587
- return resolve(projectRoot).replace(/\\/g, '/');
587
+ return canonicalProjectPath(projectRoot);
588
+ }
589
+
590
+ function canonicalProjectPath(projectRoot: string): string {
591
+ const resolved = resolve(projectRoot);
592
+ const canonical = existsSync(resolved) ? realpathSync(resolved) : resolved;
593
+ return canonical.replace(/\\/g, '/');
588
594
  }
589
595
 
590
596
  function getClaudeLocalConfigPath(): string {
@@ -19,6 +19,8 @@
19
19
 
20
20
  import { defineCommand } from 'citty';
21
21
  import * as fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
22
24
  import { spawn, execFileSync } from 'node:child_process';
23
25
  import { randomBytes } from 'node:crypto';
24
26
  import { parseTcpPort } from '../port.js';
@@ -73,6 +75,33 @@ function parseBackgroundPort(value: string | undefined): number {
73
75
  return parseTcpPort(value, 3211);
74
76
  }
75
77
 
78
+ /**
79
+ * Resolve the CLI file that `background start` should spawn.
80
+ * `process.argv[1]` follows the launcher shim and can point at a different
81
+ * install (for example `$HOME/node_modules/memorix` after `bun add` in $HOME).
82
+ * When this module is bundled into `dist/cli/index.js`, `../index.js` is the
83
+ * library stdio entry (`dist/index.js`) — never spawn that.
84
+ */
85
+ function resolveBackgroundCliEntryFrom(here: string, argv1: string): string {
86
+ const dir = path.dirname(here);
87
+ if (path.basename(here) === 'index.js' && path.basename(dir) === 'cli') {
88
+ return here;
89
+ }
90
+ const compiledSibling = path.resolve(dir, '..', 'index.js');
91
+ if (fs.existsSync(compiledSibling) && path.basename(path.dirname(compiledSibling)) === 'cli') {
92
+ return compiledSibling;
93
+ }
94
+ return argv1;
95
+ }
96
+
97
+ function resolveBackgroundCliEntry(argv1 = process.argv[1]): string {
98
+ try {
99
+ return resolveBackgroundCliEntryFrom(fileURLToPath(import.meta.url), argv1);
100
+ } catch {
101
+ return argv1;
102
+ }
103
+ }
104
+
76
105
  function parseLogLines(value: string | undefined): number {
77
106
  return parseBoundedPositiveInteger(value, 50, 'Log line count', 10_000);
78
107
  }
@@ -343,8 +372,9 @@ export async function doStart(port: number): Promise<void> {
343
372
 
344
373
  // 5. Find the memorix CLI entry point
345
374
  // We need to spawn `node <cli-entry> serve-http --port <port>`
346
- // The entry point is the same binary that's running now
347
- const cliEntry = process.argv[1]; // e.g., dist/cli/index.js or node_modules/.bin/memorix
375
+ // Prefer this package's compiled CLI so a bun/npm shim cannot spawn a
376
+ // different (often unpatched) memorix from ~/node_modules.
377
+ const cliEntry = resolveBackgroundCliEntry();
348
378
 
349
379
  // 6. Start the service without a visible console. A regular non-detached child
350
380
  // can be reclaimed with its parent by terminal hosts on Windows; a detached Node
@@ -792,4 +822,6 @@ export const _testing = {
792
822
  parseBackgroundPort,
793
823
  parseLogLines,
794
824
  formatBackgroundCommandError,
825
+ resolveBackgroundCliEntry,
826
+ resolveBackgroundCliEntryFrom,
795
827
  };
@@ -131,6 +131,7 @@ export default defineCommand({
131
131
  let bgRunning = false;
132
132
  let bgPort = 0;
133
133
  let bgPid = 0;
134
+ let backgroundHealthy = false;
134
135
 
135
136
  try {
136
137
  if (existsSync(bgStatePath)) {
@@ -148,7 +149,7 @@ export default defineCommand({
148
149
  // Health check
149
150
  try {
150
151
  const http = await import('node:http');
151
- const healthy = await new Promise<boolean>((resolve) => {
152
+ backgroundHealthy = await new Promise<boolean>((resolve) => {
152
153
  const req = http.request({ hostname: '127.0.0.1', port: bgPort, path: '/api/team', timeout: 3000 }, (res) => {
153
154
  res.resume();
154
155
  resolve(res.statusCode === 200);
@@ -157,7 +158,7 @@ export default defineCommand({
157
158
  req.on('timeout', () => { req.destroy(); resolve(false); });
158
159
  req.end();
159
160
  });
160
- if (healthy) {
161
+ if (backgroundHealthy) {
161
162
  lines.push(ok('Health check: OK'));
162
163
  } else {
163
164
  lines.push(warn('Health check: FAILED (process alive but not responding)'));
@@ -165,8 +166,9 @@ export default defineCommand({
165
166
  }
166
167
  } catch {
167
168
  lines.push(warn('Health check: could not connect'));
169
+ issues.push('Background process is alive but its health endpoint could not be reached. Try "memorix background restart".');
168
170
  }
169
- report.mode = { type: 'background', pid: bgPid, port: bgPort, healthy: true };
171
+ report.mode = { type: 'background', pid: bgPid, port: bgPort, healthy: backgroundHealthy };
170
172
  } else {
171
173
  lines.push(info('Background control plane: not running'));
172
174
  lines.push(info('Current invocation: CLI (stdio)'));
@@ -210,20 +212,49 @@ export default defineCommand({
210
212
  } else {
211
213
  lines.push(ok(`Mode: ${mode}`));
212
214
 
213
- // Try to initialize provider to check real status
214
- const { getEmbeddingProvider } = await import('../../embedding/provider.js');
215
- const provider = await getEmbeddingProvider();
215
+ // A running control plane is the source of truth for runtime health.
216
+ // A fresh doctor process must not overwrite a live 401/402/timeout
217
+ // diagnosis with a static "configured = ready" result.
218
+ let controlPlaneEmbedding: {
219
+ status?: string;
220
+ provider?: { name?: string; dimensions?: number };
221
+ failure?: { kind?: string; retryAfterMs?: number };
222
+ } | undefined;
223
+ if (bgRunning) {
224
+ try {
225
+ const response = await fetch(`http://127.0.0.1:${bgPort}/health`, { signal: AbortSignal.timeout(3_000) });
226
+ if (response.ok) {
227
+ const health = await response.json() as { embedding?: typeof controlPlaneEmbedding };
228
+ controlPlaneEmbedding = health.embedding;
229
+ }
230
+ } catch { /* health is advisory; fall back to a local probe */ }
231
+ }
216
232
 
217
- if (provider) {
218
- lines.push(ok(`Provider: ${provider.name} (${provider.dimensions}d)`));
219
- lines.push(ok('Status: ready'));
220
- lines.push(info('Search: hybrid (BM25 + vector)'));
221
- report.embedding = { mode, status: 'ready', provider: provider.name, dimensions: provider.dimensions };
233
+ if (controlPlaneEmbedding?.status === 'degraded') {
234
+ const kind = controlPlaneEmbedding.failure?.kind ?? 'upstream';
235
+ const retryAfterMs = controlPlaneEmbedding.failure?.retryAfterMs;
236
+ const retry = typeof retryAfterMs === 'number' && retryAfterMs > 0
237
+ ? `; retry in ${Math.ceil(retryAfterMs / 1000)}s`
238
+ : '';
239
+ lines.push(warn(`Status: degraded in the running control plane (${kind}${retry})`));
240
+ lines.push(info('Search: degraded to BM25 fulltext until the provider recovers'));
241
+ issues.push('The running control plane has an unavailable embedding provider. Check provider credentials, billing, or connectivity.');
242
+ report.embedding = { mode, status: 'degraded', failure: kind };
222
243
  } else {
223
- lines.push(warn('Status: temporarily unavailable'));
224
- lines.push(info('Search: degraded to BM25 fulltext (no vector similarity)'));
225
- issues.push(`Embedding mode is "${mode}" but provider failed to initialize. Check API keys/connectivity.`);
226
- report.embedding = { mode, status: 'temporarily_unavailable' };
244
+ const { getEmbeddingProvider } = await import('../../embedding/provider.js');
245
+ const provider = await getEmbeddingProvider();
246
+
247
+ if (provider) {
248
+ lines.push(ok(`Provider: ${provider.name} (${provider.dimensions}d)`));
249
+ lines.push(ok('Status: ready'));
250
+ lines.push(info('Search: hybrid (BM25 + vector)'));
251
+ report.embedding = { mode, status: 'ready', provider: provider.name, dimensions: provider.dimensions };
252
+ } else {
253
+ lines.push(warn('Status: temporarily unavailable'));
254
+ lines.push(info('Search: degraded to BM25 fulltext (no vector similarity)'));
255
+ issues.push(`Embedding mode is "${mode}" but provider failed to initialize. Check API keys/connectivity.`);
256
+ report.embedding = { mode, status: 'temporarily_unavailable' };
257
+ }
227
258
  }
228
259
 
229
260
  // Check cached embeddings count
@@ -4,8 +4,16 @@
4
4
  * Entry point called by agent hooks via stdin/stdout.
5
5
  * Reads agent's JSON from stdin, normalizes, auto-stores, outputs response.
6
6
  *
7
+ * This command is short-lived. A leftover `memorix hook` process is a bug:
8
+ * hosts invoke it on every event, and hung copies exhaust RAM.
9
+ *
7
10
  * Usage (called by agent hook configs, not by users directly):
8
11
  * memorix hook
12
+ *
13
+ * Env:
14
+ * MEMORIX_HOOK_TIMEOUT_MS — wall-clock budget (default 20000)
15
+ * MEMORIX_HOOK_STDIN_TIMEOUT_MS — stdin idle budget (default 3000)
16
+ * MEMORIX_HOOK_HEAP_MB — V8 heap for this command only (default 512)
9
17
  */
10
18
 
11
19
  import { defineCommand } from 'citty';
@@ -29,6 +37,10 @@ export default defineCommand({
29
37
  },
30
38
  run: async ({ args }) => {
31
39
  const { runHook } = await import('../../hooks/handler.js');
32
- await runHook(args.agent as string | undefined, args.event as string | undefined);
40
+ const { resolveHookTimeoutMs, runHookWithDeadline } = await import('../../hooks/lifecycle.js');
41
+ await runHookWithDeadline({
42
+ timeoutMs: resolveHookTimeoutMs(),
43
+ run: () => runHook(args.agent as string | undefined, args.event as string | undefined),
44
+ });
33
45
  },
34
46
  });
@@ -65,10 +65,13 @@ async function resolveCliProjectContext(options?: CliContextOptions): Promise<{
65
65
  */
66
66
  export async function getCliReadContext(options?: CliContextOptions): Promise<CliReadContext> {
67
67
  const { project, dataDir, invocation } = await resolveCliProjectContext(options);
68
- await initObservations(dataDir);
68
+ await initObservations(dataDir, {
69
+ embeddingWriteMode: 'deferred',
70
+ projectRoot: project.rootPath,
71
+ });
69
72
 
70
73
  if (options?.searchIndex) {
71
- await prepareSearchIndex();
74
+ await prepareSearchIndex({ skipCachedVectors: true });
72
75
  }
73
76
 
74
77
  const storedIdentity = await loadCliIdentity(dataDir, project.id);
@@ -118,7 +121,7 @@ export async function getCliProjectContext(options?: CliContextOptions): Promise
118
121
  const teamStore = await initTeamStore(dataDir);
119
122
 
120
123
  if (options?.searchIndex) {
121
- await prepareSearchIndex();
124
+ await prepareSearchIndex({ skipCachedVectors: true });
122
125
  }
123
126
 
124
127
  const identity = await resolveCliIdentity({