memorix 1.6.0 → 1.6.1

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 (119) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +317 -62
  4. package/dist/cli/memcode.js +12 -1
  5. package/dist/index.js +56 -9
  6. package/dist/maintenance-runner.js +54 -7
  7. package/dist/media-audio-runner.js +54 -7
  8. package/dist/media-video-runner.js +54 -7
  9. package/dist/memcode-runtime/CHANGELOG.md +24 -0
  10. package/dist/sdk.js +55 -8
  11. package/dist/vector-backfill-runner.js +54 -7
  12. package/docs/AGENT_OPERATOR_PLAYBOOK.md +11 -3
  13. package/docs/API_REFERENCE.md +1 -1
  14. package/docs/DESIGN_DECISIONS.md +357 -357
  15. package/docs/DOCKER.md +5 -1
  16. package/docs/PERFORMANCE.md +2 -2
  17. package/docs/README.md +3 -1
  18. package/docs/SETUP.md +8 -2
  19. package/package.json +1 -1
  20. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  21. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  22. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  23. package/plugins/copilot/memorix/plugin.json +1 -1
  24. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  25. package/plugins/omp/memorix/package.json +1 -1
  26. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/pi/memorix/package.json +1 -1
  28. package/server.json +2 -2
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/commands/agent-integrations.ts +9 -3
  31. package/src/cli/commands/doctor.ts +46 -15
  32. package/src/cli/commands/formation.ts +48 -48
  33. package/src/cli/commands/git-hook-install.ts +111 -111
  34. package/src/cli/commands/handoff.ts +54 -54
  35. package/src/cli/commands/hook.ts +13 -1
  36. package/src/cli/commands/hooks-status.ts +63 -63
  37. package/src/cli/commands/ingest.ts +44 -44
  38. package/src/cli/commands/integrate-shared.ts +15 -15
  39. package/src/cli/commands/lock.ts +82 -82
  40. package/src/cli/commands/message.ts +104 -104
  41. package/src/cli/commands/poll.ts +58 -58
  42. package/src/cli/commands/reasoning.ts +118 -118
  43. package/src/cli/commands/serve-http.ts +68 -7
  44. package/src/cli/commands/skills.ts +114 -114
  45. package/src/cli/commands/task.ts +167 -167
  46. package/src/cli/commands/transfer.ts +47 -47
  47. package/src/cli/heap.ts +76 -0
  48. package/src/cli/index.ts +406 -406
  49. package/src/cli/invocation.ts +7 -4
  50. package/src/cli/tui/ChatView.tsx +234 -234
  51. package/src/cli/tui/CommandBar.tsx +312 -312
  52. package/src/cli/tui/ContextRail.tsx +118 -118
  53. package/src/cli/tui/HeaderBar.tsx +72 -72
  54. package/src/cli/tui/LogoBanner.tsx +51 -51
  55. package/src/cli/tui/index.ts +41 -41
  56. package/src/cli/version.ts +7 -7
  57. package/src/cli/workbench.ts +1 -1
  58. package/src/dashboard/project-classification.ts +64 -64
  59. package/src/embedding/provider.ts +76 -6
  60. package/src/git/hooks-path.ts +85 -85
  61. package/src/hooks/handler.ts +9 -18
  62. package/src/hooks/lifecycle.ts +173 -0
  63. package/src/hooks/pattern-detector.ts +173 -173
  64. package/src/hooks/significance-filter.ts +250 -250
  65. package/src/llm/memory-manager.ts +328 -328
  66. package/src/llm/quality.ts +248 -248
  67. package/src/memory/attribution-guard.ts +249 -249
  68. package/src/memory/disclosure-policy.ts +135 -135
  69. package/src/memory/entity-extractor.ts +197 -197
  70. package/src/memory/formation/evaluate.ts +217 -217
  71. package/src/memory/freshness.ts +122 -122
  72. package/src/memory/graph.ts +197 -197
  73. package/src/memory/refs.ts +94 -94
  74. package/src/memory/secret-filter.ts +79 -79
  75. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  76. package/src/orchestrate/adapters/claude.ts +111 -111
  77. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  78. package/src/orchestrate/adapters/codex.ts +41 -41
  79. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  80. package/src/orchestrate/adapters/gemini.ts +42 -42
  81. package/src/orchestrate/adapters/index.ts +73 -73
  82. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  83. package/src/orchestrate/adapters/opencode.ts +47 -47
  84. package/src/orchestrate/adapters/types.ts +77 -77
  85. package/src/orchestrate/capability-router.ts +284 -284
  86. package/src/orchestrate/cost-tracker.ts +219 -219
  87. package/src/orchestrate/error-recovery.ts +191 -191
  88. package/src/orchestrate/evidence.ts +140 -140
  89. package/src/orchestrate/ledger.ts +110 -110
  90. package/src/orchestrate/memorix-bridge.ts +343 -343
  91. package/src/orchestrate/output-budget.ts +80 -80
  92. package/src/orchestrate/permission.ts +152 -152
  93. package/src/orchestrate/pipeline-trace.ts +131 -131
  94. package/src/orchestrate/prompt-builder.ts +155 -155
  95. package/src/orchestrate/ring-buffer.ts +37 -37
  96. package/src/orchestrate/task-graph.ts +389 -389
  97. package/src/project/aliases.ts +374 -374
  98. package/src/rules/adapters/trae.ts +56 -56
  99. package/src/rules/syncer.ts +235 -235
  100. package/src/search/query-expansion.ts +52 -52
  101. package/src/server/formation-timeout.ts +27 -27
  102. package/src/skills/mini-skills.ts +386 -386
  103. package/src/store/chat-store.ts +119 -119
  104. package/src/store/graph-store.ts +249 -249
  105. package/src/store/mini-skill-store.ts +349 -349
  106. package/src/store/persistence-json.ts +212 -212
  107. package/src/store/persistence.ts +291 -291
  108. package/src/store/project-affinity.ts +195 -195
  109. package/src/team/event-bus.ts +76 -76
  110. package/src/team/handoff.ts +161 -161
  111. package/src/team/poll.ts +132 -132
  112. package/src/workspace/mcp-adapters/codex.ts +191 -191
  113. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  114. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  115. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  116. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  117. package/src/workspace/mcp-adapters/trae.ts +134 -134
  118. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  119. package/src/workspace/sanitizer.ts +60 -60
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,7 +71,7 @@ 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 |
@@ -86,7 +86,7 @@ On the release development machine used for this check, the healthy HTTP service
86
86
  ## Operator Guidance
87
87
 
88
88
  - 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.
89
+ - 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
90
  - 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
91
  - For Docker, use it when you want a managed HTTP service. Do not use image size alone as the runtime memory estimate.
92
92
  - For orchestrated subagent work, expect CPU and disk activity proportional to the spawned agents and verification commands.
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.1",
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,
@@ -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.1",
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.1",
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.1",
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.1",
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.1",
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.1",
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.1",
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.1",
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.1",
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.1",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -1,156 +1,156 @@
1
- /**
2
- * Audit Module - Track Memorix-written files
3
- *
4
- * Records all files written by Memorix to distinguish them from
5
- * project-native files. Provides audit trail for cleanup and rollback.
6
- */
7
-
8
- import * as fs from 'node:fs/promises';
9
- import * as path from 'node:path';
10
- import { homedir } from 'node:os';
11
-
12
- function getAuditFilePath(): string {
13
- return process.env.MEMORIX_AUDIT_FILE || path.join(homedir(), '.memorix', 'audit.json');
14
- }
15
-
16
- export interface AuditEntry {
17
- type: 'hook' | 'rule' | 'other';
18
- agent?: string;
19
- path: string;
20
- createdAt: string;
21
- }
22
-
23
- export interface ProjectAudit {
24
- projectRoot: string;
25
- installedAt: string;
26
- entries: AuditEntry[];
27
- }
28
-
29
- export interface AuditData {
30
- version: string;
31
- projects: Record<string, ProjectAudit>;
32
- }
33
-
34
- /**
35
- * Load audit data from disk.
36
- */
37
- export async function loadAudit(): Promise<AuditData> {
38
- try {
39
- const content = await fs.readFile(getAuditFilePath(), 'utf-8');
40
- return JSON.parse(content) as AuditData;
41
- } catch {
42
- // No audit file yet
43
- return {
44
- version: '1.0.0',
45
- projects: {},
46
- };
47
- }
48
- }
49
-
50
- /**
51
- * Save audit data to disk.
52
- */
53
- export async function saveAudit(data: AuditData): Promise<void> {
54
- const auditFile = getAuditFilePath();
55
- const dir = path.dirname(auditFile);
56
- await fs.mkdir(dir, { recursive: true });
57
- await fs.writeFile(auditFile, JSON.stringify(data, null, 2), 'utf-8');
58
- }
59
-
60
- /**
61
- * Get project ID from project root via .git detection.
62
- */
63
- export function getProjectId(projectRoot: string): string {
64
- try {
65
- const { detectProject } = require('../project/detector.js');
66
- const project = detectProject(projectRoot);
67
- if (project) return project.id;
68
- } catch { /* fallback below */ }
69
- const normalized = projectRoot.replace(/\\/g, '/');
70
- return `untracked/${normalized}`;
71
- }
72
-
73
- /**
74
- * Record a file written by Memorix.
75
- */
76
- export async function recordFile(
77
- projectRoot: string,
78
- type: AuditEntry['type'],
79
- filePath: string,
80
- agent?: string,
81
- ): Promise<void> {
82
- const data = await loadAudit();
83
- const projectId = getProjectId(projectRoot);
84
-
85
- if (!data.projects[projectId]) {
86
- data.projects[projectId] = {
87
- projectRoot,
88
- installedAt: new Date().toISOString(),
89
- entries: [],
90
- };
91
- }
92
-
93
- // Check if entry already exists
94
- const existingIndex = data.projects[projectId].entries.findIndex(
95
- (e) => e.path === filePath
96
- );
97
-
98
- if (existingIndex === -1) {
99
- data.projects[projectId].entries.push({
100
- type,
101
- agent,
102
- path: filePath,
103
- createdAt: new Date().toISOString(),
104
- });
105
- }
106
-
107
- await saveAudit(data);
108
- }
109
-
110
- /**
111
- * Get all files written by Memorix for a project.
112
- */
113
- export async function getProjectFiles(projectRoot: string): Promise<AuditEntry[]> {
114
- const data = await loadAudit();
115
- const projectId = getProjectId(projectRoot);
116
- return data.projects[projectId]?.entries || [];
117
- }
118
-
119
- /**
120
- * Remove a file from audit (when uninstalled).
121
- */
122
- export async function removeFile(projectRoot: string, filePath: string): Promise<void> {
123
- const data = await loadAudit();
124
- const projectId = getProjectId(projectRoot);
125
-
126
- if (!data.projects[projectId]) return;
127
-
128
- data.projects[projectId].entries = data.projects[projectId].entries.filter(
129
- (e) => e.path !== filePath
130
- );
131
-
132
- // If no entries left, remove the project
133
- if (data.projects[projectId].entries.length === 0) {
134
- delete data.projects[projectId];
135
- }
136
-
137
- await saveAudit(data);
138
- }
139
-
140
- /**
141
- * Get all audit entries across all projects.
142
- */
143
- export async function getAllAuditEntries(): Promise<
144
- Array<{ projectId: string; entry: AuditEntry }>
145
- > {
146
- const data = await loadAudit();
147
- const entries: Array<{ projectId: string; entry: AuditEntry }> = [];
148
-
149
- for (const [projectId, project] of Object.entries(data.projects)) {
150
- for (const entry of project.entries) {
151
- entries.push({ projectId, entry });
152
- }
153
- }
154
-
155
- return entries;
156
- }
1
+ /**
2
+ * Audit Module - Track Memorix-written files
3
+ *
4
+ * Records all files written by Memorix to distinguish them from
5
+ * project-native files. Provides audit trail for cleanup and rollback.
6
+ */
7
+
8
+ import * as fs from 'node:fs/promises';
9
+ import * as path from 'node:path';
10
+ import { homedir } from 'node:os';
11
+
12
+ function getAuditFilePath(): string {
13
+ return process.env.MEMORIX_AUDIT_FILE || path.join(homedir(), '.memorix', 'audit.json');
14
+ }
15
+
16
+ export interface AuditEntry {
17
+ type: 'hook' | 'rule' | 'other';
18
+ agent?: string;
19
+ path: string;
20
+ createdAt: string;
21
+ }
22
+
23
+ export interface ProjectAudit {
24
+ projectRoot: string;
25
+ installedAt: string;
26
+ entries: AuditEntry[];
27
+ }
28
+
29
+ export interface AuditData {
30
+ version: string;
31
+ projects: Record<string, ProjectAudit>;
32
+ }
33
+
34
+ /**
35
+ * Load audit data from disk.
36
+ */
37
+ export async function loadAudit(): Promise<AuditData> {
38
+ try {
39
+ const content = await fs.readFile(getAuditFilePath(), 'utf-8');
40
+ return JSON.parse(content) as AuditData;
41
+ } catch {
42
+ // No audit file yet
43
+ return {
44
+ version: '1.0.0',
45
+ projects: {},
46
+ };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Save audit data to disk.
52
+ */
53
+ export async function saveAudit(data: AuditData): Promise<void> {
54
+ const auditFile = getAuditFilePath();
55
+ const dir = path.dirname(auditFile);
56
+ await fs.mkdir(dir, { recursive: true });
57
+ await fs.writeFile(auditFile, JSON.stringify(data, null, 2), 'utf-8');
58
+ }
59
+
60
+ /**
61
+ * Get project ID from project root via .git detection.
62
+ */
63
+ export function getProjectId(projectRoot: string): string {
64
+ try {
65
+ const { detectProject } = require('../project/detector.js');
66
+ const project = detectProject(projectRoot);
67
+ if (project) return project.id;
68
+ } catch { /* fallback below */ }
69
+ const normalized = projectRoot.replace(/\\/g, '/');
70
+ return `untracked/${normalized}`;
71
+ }
72
+
73
+ /**
74
+ * Record a file written by Memorix.
75
+ */
76
+ export async function recordFile(
77
+ projectRoot: string,
78
+ type: AuditEntry['type'],
79
+ filePath: string,
80
+ agent?: string,
81
+ ): Promise<void> {
82
+ const data = await loadAudit();
83
+ const projectId = getProjectId(projectRoot);
84
+
85
+ if (!data.projects[projectId]) {
86
+ data.projects[projectId] = {
87
+ projectRoot,
88
+ installedAt: new Date().toISOString(),
89
+ entries: [],
90
+ };
91
+ }
92
+
93
+ // Check if entry already exists
94
+ const existingIndex = data.projects[projectId].entries.findIndex(
95
+ (e) => e.path === filePath
96
+ );
97
+
98
+ if (existingIndex === -1) {
99
+ data.projects[projectId].entries.push({
100
+ type,
101
+ agent,
102
+ path: filePath,
103
+ createdAt: new Date().toISOString(),
104
+ });
105
+ }
106
+
107
+ await saveAudit(data);
108
+ }
109
+
110
+ /**
111
+ * Get all files written by Memorix for a project.
112
+ */
113
+ export async function getProjectFiles(projectRoot: string): Promise<AuditEntry[]> {
114
+ const data = await loadAudit();
115
+ const projectId = getProjectId(projectRoot);
116
+ return data.projects[projectId]?.entries || [];
117
+ }
118
+
119
+ /**
120
+ * Remove a file from audit (when uninstalled).
121
+ */
122
+ export async function removeFile(projectRoot: string, filePath: string): Promise<void> {
123
+ const data = await loadAudit();
124
+ const projectId = getProjectId(projectRoot);
125
+
126
+ if (!data.projects[projectId]) return;
127
+
128
+ data.projects[projectId].entries = data.projects[projectId].entries.filter(
129
+ (e) => e.path !== filePath
130
+ );
131
+
132
+ // If no entries left, remove the project
133
+ if (data.projects[projectId].entries.length === 0) {
134
+ delete data.projects[projectId];
135
+ }
136
+
137
+ await saveAudit(data);
138
+ }
139
+
140
+ /**
141
+ * Get all audit entries across all projects.
142
+ */
143
+ export async function getAllAuditEntries(): Promise<
144
+ Array<{ projectId: string; entry: AuditEntry }>
145
+ > {
146
+ const data = await loadAudit();
147
+ const entries: Array<{ projectId: string; entry: AuditEntry }> = [];
148
+
149
+ for (const [projectId, project] of Object.entries(data.projects)) {
150
+ for (const entry of project.entries) {
151
+ entries.push({ projectId, entry });
152
+ }
153
+ }
154
+
155
+ return entries;
156
+ }
@@ -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 {
@@ -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