memorix 1.9.4 → 1.9.6

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 (90) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/README.md +12 -6
  3. package/README.zh-CN.md +12 -6
  4. package/dist/cli/index.js +2533 -585
  5. package/dist/index.d.ts +15 -8
  6. package/dist/index.js +1201 -284
  7. package/dist/maintenance-runner.js +387 -216
  8. package/dist/media-audio-runner.js +300 -129
  9. package/dist/media-video-runner.js +300 -129
  10. package/dist/memcode-runtime/CHANGELOG.md +75 -1
  11. package/dist/sdk.d.ts +1 -0
  12. package/dist/sdk.js +1882 -998
  13. package/dist/types.d.ts +1 -1
  14. package/dist/vector-backfill-runner.js +300 -129
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
  16. package/docs/API_REFERENCE.md +39 -1
  17. package/docs/CONFIGURATION.md +58 -1
  18. package/docs/DEVELOPMENT.md +1 -1
  19. package/docs/DOCKER.md +23 -3
  20. package/docs/INTEGRATIONS.md +1 -1
  21. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +61 -1
  22. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
  23. package/docs/PERFORMANCE.md +46 -2
  24. package/docs/README.md +2 -1
  25. package/docs/SETUP.md +56 -1
  26. package/package.json +3 -2
  27. package/plugins/antigravity/memorix/plugin.json +1 -1
  28. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  29. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  30. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  31. package/plugins/copilot/memorix/plugin.json +1 -1
  32. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  33. package/plugins/hermes/memorix/plugin.yaml +1 -1
  34. package/plugins/omp/memorix/package.json +1 -1
  35. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  36. package/plugins/pi/memorix/package.json +1 -1
  37. package/server.json +2 -2
  38. package/src/cli/capability-map.ts +1 -0
  39. package/src/cli/command-guide.ts +15 -3
  40. package/src/cli/commands/agent-integrations.ts +57 -0
  41. package/src/cli/commands/background.ts +11 -2
  42. package/src/cli/commands/context.ts +2 -0
  43. package/src/cli/commands/continuity.ts +155 -0
  44. package/src/cli/commands/init.ts +5 -2
  45. package/src/cli/commands/serve-http.ts +435 -84
  46. package/src/cli/commands/setup.ts +77 -10
  47. package/src/cli/commands/sync-store.ts +47 -12
  48. package/src/cli/commands/sync.ts +26 -0
  49. package/src/cli/index.ts +3 -1
  50. package/src/cli/tui/ConfigureView.tsx +2 -0
  51. package/src/codegraph/auto-context.ts +17 -0
  52. package/src/config/legacy-loader.ts +2 -2
  53. package/src/config/resolved-config.ts +8 -5
  54. package/src/config/yaml-loader.ts +20 -10
  55. package/src/dashboard/server.ts +10 -12
  56. package/src/embedding/api-provider.ts +4 -2
  57. package/src/embedding/fastembed-provider.ts +47 -14
  58. package/src/embedding/transformers-provider.ts +29 -7
  59. package/src/evaluation/memory-quality-gate.ts +90 -0
  60. package/src/hooks/official-skills.ts +2 -0
  61. package/src/integrations/registry.ts +1 -1
  62. package/src/knowledge/context-assembly.ts +1 -0
  63. package/src/knowledge/context-receipt.ts +15 -0
  64. package/src/knowledge/task-continuity.ts +413 -0
  65. package/src/knowledge/workset.ts +77 -0
  66. package/src/llm/provider.ts +2 -1
  67. package/src/memory/graph.ts +10 -8
  68. package/src/memory/session.ts +1 -1
  69. package/src/orchestrate/coordinator.ts +60 -0
  70. package/src/orchestrate/effect-ledger.ts +160 -0
  71. package/src/orchestrate/pipeline-trace.ts +40 -6
  72. package/src/runtime/maintenance-jobs.ts +12 -6
  73. package/src/sdk.ts +24 -0
  74. package/src/server/mcp-binding-store.ts +14 -3
  75. package/src/server/modern-mcp-bridge.ts +153 -27
  76. package/src/server/tool-profile.ts +3 -2
  77. package/src/server.ts +131 -7
  78. package/src/store/file-lock.ts +28 -3
  79. package/src/store/graph-store.ts +178 -46
  80. package/src/store/persistence.ts +14 -9
  81. package/src/store/sqlite-db.ts +150 -7
  82. package/src/store/sqlite-store.ts +32 -27
  83. package/src/sync/engine.ts +9 -7
  84. package/src/sync/namespace.ts +23 -0
  85. package/src/sync/policy.ts +19 -4
  86. package/src/sync/store-port.ts +43 -13
  87. package/src/sync/types.ts +2 -0
  88. package/src/team/team-store.ts +62 -27
  89. package/src/types.ts +1 -1
  90. package/src/workspace/mcp-adapters/grok.ts +32 -0
@@ -28,7 +28,9 @@ export interface ModernMcpBridgeOptions extends CreateMemorixServerOptions {
28
28
  export interface ModernMcpBridgeLifecycle {
29
29
  projectId: string;
30
30
  deferredInit: () => Promise<void>;
31
+ activateProjectRuntime: () => Promise<void>;
31
32
  switchProject: (newCwd: string, source?: ProjectBindingSource) => Promise<boolean>;
33
+ getProjectDataDir: () => string;
32
34
  isExplicitlyBound: () => boolean;
33
35
  handleTransportClose: () => void;
34
36
  }
@@ -37,14 +39,116 @@ export type ModernMcpBridge = ModernMcpServer & {
37
39
  memorix: ModernMcpBridgeLifecycle;
38
40
  };
39
41
 
42
+ let businessRuntimeQueue: Promise<unknown> = Promise.resolve();
43
+
44
+ /** Serialize access to the legacy process-global store layer. */
45
+ export function withBusinessRuntimeQueue<T>(work: () => Promise<T>): Promise<T> {
46
+ const result = businessRuntimeQueue.then(work);
47
+ businessRuntimeQueue = result.catch(() => undefined);
48
+ return result;
49
+ }
50
+
51
+ export interface ModernMcpRuntime {
52
+ projectId: string;
53
+ getProjectDataDir: () => string;
54
+ createBridge: () => ModernMcpBridge;
55
+ close: () => Promise<void>;
56
+ }
57
+
58
+ interface RuntimePoolEntry {
59
+ runtime: ModernMcpRuntime;
60
+ refs: number;
61
+ lastUsedAt: number;
62
+ }
63
+
64
+ /**
65
+ * Reuses the expensive legacy business runtime while giving the v2 handler a
66
+ * fresh protocol server per request, as required by the official SDK.
67
+ */
68
+ export class ModernMcpRuntimePool {
69
+ private readonly entries = new Map<string, RuntimePoolEntry>();
70
+ private readonly pending = new Map<string, Promise<RuntimePoolEntry>>();
71
+
72
+ constructor(private readonly maxEntries = 32) {}
73
+
74
+ async createBridge(
75
+ key: string,
76
+ create: () => Promise<ModernMcpRuntime>,
77
+ ): Promise<ModernMcpBridge> {
78
+ let entry = this.entries.get(key);
79
+ if (!entry) {
80
+ let pending = this.pending.get(key);
81
+ if (!pending) {
82
+ pending = create().then(runtime => {
83
+ const created: RuntimePoolEntry = { runtime, refs: 0, lastUsedAt: Date.now() };
84
+ this.entries.set(key, created);
85
+ return created;
86
+ });
87
+ this.pending.set(key, pending);
88
+ }
89
+ try {
90
+ entry = await pending;
91
+ } finally {
92
+ this.pending.delete(key);
93
+ }
94
+ }
95
+
96
+ entry.refs++;
97
+ entry.lastUsedAt = Date.now();
98
+ const bridge = entry.runtime.createBridge();
99
+ const close = bridge.close.bind(bridge);
100
+ let released = false;
101
+ bridge.close = async () => {
102
+ try {
103
+ await close();
104
+ } finally {
105
+ if (!released) {
106
+ released = true;
107
+ entry!.refs = Math.max(0, entry!.refs - 1);
108
+ entry!.lastUsedAt = Date.now();
109
+ }
110
+ }
111
+ };
112
+ return bridge;
113
+ }
114
+
115
+ async evictIdle(): Promise<number> {
116
+ let closed = 0;
117
+ const idle = [...this.entries.entries()]
118
+ .filter(([, entry]) => entry.refs === 0)
119
+ .sort(([, left], [, right]) => left.lastUsedAt - right.lastUsedAt);
120
+ for (const [key, entry] of idle) {
121
+ if (this.entries.size <= this.maxEntries || entry.refs > 0) continue;
122
+ this.entries.delete(key);
123
+ await entry.runtime.close();
124
+ closed++;
125
+ }
126
+ return closed;
127
+ }
128
+
129
+ async close(): Promise<void> {
130
+ const entries = [...this.entries.values()];
131
+ this.entries.clear();
132
+ await Promise.all(entries.map(entry => entry.runtime.close().catch(() => undefined)));
133
+ }
134
+
135
+ stats(): { entries: number; activeRefs: number; pending: number } {
136
+ return {
137
+ entries: this.entries.size,
138
+ activeRefs: [...this.entries.values()].reduce((sum, entry) => sum + entry.refs, 0),
139
+ pending: this.pending.size,
140
+ };
141
+ }
142
+ }
143
+
40
144
  /**
41
145
  * Build an official v2 server whose tools delegate to the existing Memorix
42
146
  * implementation. The v1 side is process-local and never becomes a second
43
147
  * network endpoint, so project isolation and business behavior stay shared.
44
148
  */
45
- export async function createModernMcpBridge(
149
+ export async function createModernMcpRuntime(
46
150
  options: ModernMcpBridgeOptions,
47
- ): Promise<ModernMcpBridge> {
151
+ ): Promise<ModernMcpRuntime> {
48
152
  const { createMemorixServer } = await import('../server.js');
49
153
  const legacy = await createMemorixServer(
50
154
  options.projectRoot,
@@ -67,42 +171,64 @@ export async function createModernMcpBridge(
67
171
  await legacyClient.connect(clientTransport);
68
172
 
69
173
  const listed = await legacyClient.listTools();
70
- const modern = new ModernMcpServer(getMcpServerInfo());
71
-
72
- for (const tool of listed.tools) {
73
- modern.registerTool(
74
- tool.name,
75
- {
76
- ...(tool.title ? { title: tool.title } : {}),
77
- ...(tool.description ? { description: tool.description } : {}),
78
- inputSchema: fromJsonSchema(tool.inputSchema as Record<string, unknown>),
79
- },
80
- async (args) => await legacyClient.callTool({
81
- name: tool.name,
82
- arguments: args as Record<string, unknown>,
83
- }) as never,
84
- );
85
- }
174
+ const callTool = (name: string, args: Record<string, unknown>) =>
175
+ withBusinessRuntimeQueue(async () => {
176
+ await legacy.activateProjectRuntime();
177
+ return await legacyClient.callTool({ name, arguments: args });
178
+ });
86
179
 
87
- const bridge = modern as ModernMcpBridge;
88
- bridge.memorix = {
180
+ const lifecycle: ModernMcpBridgeLifecycle = {
89
181
  projectId: legacy.projectId,
90
182
  deferredInit: legacy.deferredInit,
183
+ activateProjectRuntime: legacy.activateProjectRuntime,
91
184
  switchProject: legacy.switchProject,
185
+ getProjectDataDir: legacy.getProjectDataDir,
92
186
  isExplicitlyBound: legacy.isExplicitlyBound,
93
187
  handleTransportClose: legacy.handleTransportClose,
94
188
  };
95
189
 
96
190
  let closed = false;
97
- const modernClose = modern.close.bind(modern);
98
- modern.close = async () => {
191
+ return {
192
+ projectId: legacy.projectId,
193
+ getProjectDataDir: legacy.getProjectDataDir,
194
+ createBridge: () => {
195
+ const requestServer = new ModernMcpServer(getMcpServerInfo()) as ModernMcpBridge;
196
+ for (const tool of listed.tools) {
197
+ requestServer.registerTool(
198
+ tool.name,
199
+ {
200
+ ...(tool.title ? { title: tool.title } : {}),
201
+ ...(tool.description ? { description: tool.description } : {}),
202
+ inputSchema: fromJsonSchema(tool.inputSchema as Record<string, unknown>),
203
+ },
204
+ async (args) => await callTool(tool.name, args as Record<string, unknown>) as never,
205
+ );
206
+ }
207
+ requestServer.memorix = lifecycle;
208
+ return requestServer;
209
+ },
210
+ close: async () => {
211
+ if (closed) return;
212
+ closed = true;
213
+ await legacyClient.close().catch(() => undefined);
214
+ await legacy.server.close().catch(() => undefined);
215
+ legacy.handleTransportClose();
216
+ },
217
+ };
218
+ }
219
+
220
+ export async function createModernMcpBridge(
221
+ options: ModernMcpBridgeOptions,
222
+ ): Promise<ModernMcpBridge> {
223
+ const runtime = await createModernMcpRuntime(options);
224
+ const bridge = runtime.createBridge();
225
+ const close = bridge.close.bind(bridge);
226
+ let closed = false;
227
+ bridge.close = async () => {
99
228
  if (closed) return;
100
229
  closed = true;
101
- await modernClose();
102
- await legacyClient.close().catch(() => undefined);
103
- await legacy.server.close().catch(() => undefined);
104
- legacy.handleTransportClose();
230
+ await close();
231
+ await runtime.close();
105
232
  };
106
-
107
233
  return bridge;
108
234
  }
@@ -8,9 +8,9 @@
8
8
  * We provide four profiles:
9
9
  * - "micro" (stdio default): Agent-ready project context + bounded recovery/media controls — 9 tools.
10
10
  * Suitable for normal MCP clients where every tool schema costs context tokens.
11
- * - "lite": Core memory CRUD, sessions, reasoning, retention, backup — 20 tools.
11
+ * - "lite": Core memory CRUD, sessions, reasoning, continuity, retention, backup — 21 tools.
12
12
  * Suitable for solo users who want the full memory surface without team tools.
13
- * - "team" (HTTP default): lite + orchestration coordination tools, dashboard, and Knowledge Workspace — 28 tools.
13
+ * - "team" (HTTP default): lite + orchestration coordination tools, dashboard, and Knowledge Workspace — 29 tools.
14
14
  * Suitable when an operator explicitly wants task/message/lock surfaces.
15
15
  * - "full": Everything, including niche / advanced tools (consolidate, dedup,
16
16
  * formation metrics, skills, rules/workspace sync, KG-official, image ingest).
@@ -44,6 +44,7 @@ export const TOOL_PROFILES: Record<string, ReadonlyArray<ToolProfile>> = Object.
44
44
  // keep the default micro profile at its nine-tool token budget.
45
45
  memorix_evidence: ['lite', 'team', 'full'],
46
46
  memorix_feedback: ['lite', 'team', 'full'],
47
+ memorix_continuity: ['lite', 'team', 'full'],
47
48
 
48
49
  // ── lite: extended solo memory surface ────────────────────────────
49
50
  memorix_graph_context: ['lite', 'team', 'full'],
package/src/server.ts CHANGED
@@ -253,7 +253,9 @@ export async function createMemorixServer(
253
253
  graphManager: KnowledgeGraphManager;
254
254
  projectId: string;
255
255
  deferredInit: () => Promise<void>;
256
- switchProject: (newCwd: string) => Promise<boolean>;
256
+ activateProjectRuntime: () => Promise<void>;
257
+ switchProject: (newCwd: string, source?: ProjectBindingSource) => Promise<boolean>;
258
+ getProjectDataDir: () => string;
257
259
  isExplicitlyBound: () => boolean;
258
260
  getRequestContext: () => import('./server/request-context.js').MemorixRequestContext;
259
261
  handleTransportClose: () => void;
@@ -364,7 +366,7 @@ export async function createMemorixServer(
364
366
  const store = getObservationStore();
365
367
  console.error(`[memorix] ObservationStore backend: ${store.getBackendName()}, generation: ${store.getGeneration()}`);
366
368
  }
367
- let graphManager = new KnowledgeGraphManager(projectDir);
369
+ let graphManager = new KnowledgeGraphManager(projectDir, project.id);
368
370
  if (!deferProjectRuntimeInit) {
369
371
  await graphManager.init();
370
372
  await initObservations(projectDir);
@@ -378,7 +380,7 @@ export async function createMemorixServer(
378
380
  await initObservationStore(projectDir);
379
381
  await initMiniSkillStore(projectDir);
380
382
  await initSessionStore(projectDir);
381
- graphManager = new KnowledgeGraphManager(projectDir);
383
+ graphManager = new KnowledgeGraphManager(projectDir, project.id);
382
384
  await graphManager.init();
383
385
  await initObservations(projectDir);
384
386
 
@@ -497,6 +499,31 @@ export async function createMemorixServer(
497
499
  await projectRuntimeInitPromise;
498
500
  };
499
501
 
502
+ /**
503
+ * Restore process-wide stores/config before a pooled modern runtime handles
504
+ * a tool call. The durable database is shared, but project-root config and
505
+ * the in-memory observation facade are process-global legacy state.
506
+ */
507
+ const activateProjectRuntime = async (): Promise<void> => {
508
+ if (!projectResolved) return;
509
+
510
+ await initObservationStore(projectDir);
511
+ await initMiniSkillStore(projectDir);
512
+ await initSessionStore(projectDir);
513
+ await initObservations(projectDir, { skipCorpusLoad: true });
514
+
515
+ try {
516
+ const { initProjectRoot } = await import('./config/yaml-loader.js');
517
+ const { resetDotenv, loadDotenv } = await import('./config/dotenv-loader.js');
518
+ initProjectRoot(project.rootPath);
519
+ resetDotenv();
520
+ loadDotenv(project.rootPath);
521
+ } catch {
522
+ // Config activation is best-effort; durable project scoping remains explicit.
523
+ }
524
+ initLLM({ projectRoot: project.rootPath });
525
+ };
526
+
500
527
  let maintenanceWorker: { stop(): void } | null = null;
501
528
  const startProjectMaintenanceWorker = async (autoCleanup = false): Promise<void> => {
502
529
  maintenanceWorker?.stop();
@@ -838,8 +865,9 @@ export async function createMemorixServer(
838
865
  currentFormationStage = event.stage;
839
866
  if (event.stageDurationMs !== undefined) {
840
867
  completedFormationStages[event.stage] = event.stageDurationMs;
841
- }
842
- };
868
+ }
869
+ };
870
+
843
871
  try {
844
872
  const formationConfig: FormationConfig = {
845
873
  mode: 'active',
@@ -1593,6 +1621,7 @@ export async function createMemorixServer(
1593
1621
  'reliable code-bound memories, stale/suspect cautions, and verification hints. Use this at the start of a new coding turn or after switching tasks.',
1594
1622
  inputSchema: {
1595
1623
  task: z.string().optional().describe('Current coding task or question'),
1624
+ taskId: z.string().max(120).optional().describe('Optional task continuity ledger id to include in the brief'),
1596
1625
  refresh: z.enum(['auto', 'always', 'never']).optional().default('auto').describe(
1597
1626
  'Code Memory refresh policy. auto refreshes only when missing or stale.',
1598
1627
  ),
@@ -1606,7 +1635,7 @@ export async function createMemorixServer(
1606
1635
  limit: z.number().optional().describe('Reserved for future source limits; current prompt stays compact by default.'),
1607
1636
  },
1608
1637
  },
1609
- async ({ task, refresh, format, agent }) => {
1638
+ async ({ task, taskId, refresh, format, agent }) => {
1610
1639
  const unresolved = requireResolvedProject('build project context for the current project');
1611
1640
  if (unresolved) return unresolved;
1612
1641
 
@@ -1637,6 +1666,7 @@ export async function createMemorixServer(
1637
1666
  dataDir: projectDir,
1638
1667
  observations,
1639
1668
  task,
1669
+ taskId,
1640
1670
  agent,
1641
1671
  refresh: refresh ?? 'auto',
1642
1672
  reader: getObservationReader(),
@@ -4594,6 +4624,99 @@ export async function createMemorixServer(
4594
4624
  },
4595
4625
  );
4596
4626
 
4627
+ server.registerTool(
4628
+ 'memorix_continuity',
4629
+ {
4630
+ title: 'Task Continuity Ledger',
4631
+ description:
4632
+ 'Maintain a bounded, append-only continuity ledger for a coding task. ' +
4633
+ 'Use start for requirements, record for decisions/verification/risks, ' +
4634
+ 'show or list to inspect, and close for the final outcome. Pass the returned taskId ' +
4635
+ 'to memorix_project_context so the current brief includes this ledger.',
4636
+ inputSchema: {
4637
+ action: z.enum(['start', 'record', 'show', 'list', 'close']).describe('Continuity operation'),
4638
+ taskId: z.string().max(120).optional().describe('Continuity task id'),
4639
+ task: z.string().max(1_000).optional().describe('Task description for start'),
4640
+ requirements: z.array(z.string().max(2_000)).max(8).optional().describe('Requirements for start'),
4641
+ kind: z.enum(['requirement', 'decision', 'verification', 'risk']).optional().describe('Entry kind for record'),
4642
+ content: z.string().max(2_000).optional().describe('Requirement, decision, verification, risk, or outcome text'),
4643
+ verificationStatus: z.enum(['pending', 'passed', 'failed', 'skipped']).optional(),
4644
+ verificationId: z.string().max(120).optional().describe('Stable verification obligation id; reuse it to move pending to passed/failed'),
4645
+ idempotencyKey: z.string().max(200).optional().describe('Stable retry key; repeating the same operation returns the original event'),
4646
+ status: z.enum(['completed', 'blocked', 'abandoned']).optional().describe('Outcome status for close'),
4647
+ sourceRef: z.string().max(500).optional().describe('Evidence or source reference'),
4648
+ evidenceRefs: z.array(z.string().max(500)).max(8).optional(),
4649
+ actor: z.string().max(200).optional(),
4650
+ limit: z.number().int().positive().max(100).optional().default(20),
4651
+ },
4652
+ },
4653
+ async ({ action, taskId, task, requirements, kind, content, verificationStatus, verificationId, idempotencyKey, status, sourceRef, evidenceRefs, actor, limit }) => {
4654
+ const unresolved = requireResolvedProject('manage task continuity for the current project');
4655
+ if (unresolved) return unresolved;
4656
+ const { TaskContinuityStore } = await import('./knowledge/task-continuity.js');
4657
+ const continuity = new TaskContinuityStore();
4658
+ await continuity.init(projectDir);
4659
+ const effectiveActor = actor ?? currentAgentId;
4660
+ try {
4661
+ if (action === 'start') {
4662
+ if (!task?.trim()) return { content: [{ type: 'text' as const, text: 'task is required for continuity start.' }], isError: true };
4663
+ const result = continuity.start({
4664
+ projectId: project.id,
4665
+ task,
4666
+ ...(taskId ? { taskId } : {}),
4667
+ requirements,
4668
+ ...(effectiveActor ? { actor: effectiveActor } : {}),
4669
+ ...(idempotencyKey ? { idempotencyKey } : {}),
4670
+ });
4671
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ...result }, null, 2) }] };
4672
+ }
4673
+ if (action === 'list') {
4674
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ledgers: continuity.list(project.id, limit), }, null, 2) }] };
4675
+ }
4676
+ if (!taskId?.trim()) return { content: [{ type: 'text' as const, text: 'taskId is required for this continuity action.' }], isError: true };
4677
+ if (action === 'show') {
4678
+ const ledger = continuity.get(project.id, taskId);
4679
+ if (!ledger) return { content: [{ type: 'text' as const, text: 'Task continuity ledger not found.' }], isError: true };
4680
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ledger }, null, 2) }] };
4681
+ }
4682
+ if (action === 'record') {
4683
+ if (!kind || !content?.trim()) {
4684
+ return { content: [{ type: 'text' as const, text: 'record requires kind and content.' }], isError: true };
4685
+ }
4686
+ const result = continuity.record({
4687
+ projectId: project.id,
4688
+ taskId,
4689
+ kind,
4690
+ content,
4691
+ ...(verificationStatus ? { verificationStatus } : {}),
4692
+ ...(sourceRef ? { sourceRef } : {}),
4693
+ ...(evidenceRefs ? { evidenceRefs } : {}),
4694
+ ...(effectiveActor ? { actor: effectiveActor } : {}),
4695
+ ...(verificationId ? { verificationId } : {}),
4696
+ ...(idempotencyKey ? { idempotencyKey } : {}),
4697
+ });
4698
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ...result }, null, 2) }] };
4699
+ }
4700
+ if (!status || !content?.trim()) {
4701
+ return { content: [{ type: 'text' as const, text: 'close requires status and content.' }], isError: true };
4702
+ }
4703
+ const result = continuity.close({
4704
+ projectId: project.id,
4705
+ taskId,
4706
+ status,
4707
+ content,
4708
+ ...(sourceRef ? { sourceRef } : {}),
4709
+ ...(evidenceRefs ? { evidenceRefs } : {}),
4710
+ ...(effectiveActor ? { actor: effectiveActor } : {}),
4711
+ ...(idempotencyKey ? { idempotencyKey } : {}),
4712
+ });
4713
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, ...result }, null, 2) }] };
4714
+ } catch (error) {
4715
+ return { content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }], isError: true };
4716
+ }
4717
+ },
4718
+ );
4719
+
4597
4720
  // Use shared TeamStore (from HTTP server) or create new one (stdio mode).
4598
4721
  // All team state is canonical in SQLite — no JSON persistence, no sync/flush.
4599
4722
  if (teamFeaturesEnabled) {
@@ -5725,7 +5848,8 @@ export async function createMemorixServer(
5725
5848
  };
5726
5849
 
5727
5850
  return {
5728
- server, graphManager, projectId: project.id, deferredInit, switchProject,
5851
+ server, graphManager, projectId: project.id, deferredInit, activateProjectRuntime, switchProject,
5852
+ getProjectDataDir: () => projectDir,
5729
5853
  isExplicitlyBound: () => projectBinding.isExplicit(),
5730
5854
  getRequestContext,
5731
5855
  handleTransportClose,
@@ -20,6 +20,22 @@ const RETRY_INTERVAL_MS = 50;
20
20
  const MAX_RETRIES = 60;
21
21
  const atomicWriteTails = new Map<string, Promise<void>>();
22
22
 
23
+ function canReclaimStaleLock(raw: string): boolean {
24
+ try {
25
+ const parsed = JSON.parse(raw) as { pid?: unknown };
26
+ const pid = Number(parsed.pid);
27
+ if (!Number.isSafeInteger(pid) || pid < 1) return true;
28
+ try {
29
+ process.kill(pid, 0);
30
+ return false;
31
+ } catch (error) {
32
+ return (error as NodeJS.ErrnoException).code !== 'EPERM';
33
+ }
34
+ } catch {
35
+ return true;
36
+ }
37
+ }
38
+
23
39
  /**
24
40
  * Acquire a lock file atomically.
25
41
  * Uses O_WRONLY | O_CREAT | O_EXCL — fails if file already exists.
@@ -39,8 +55,11 @@ export async function acquireLock(lockPath: string): Promise<void> {
39
55
  try {
40
56
  const stat = await fs.stat(lockPath);
41
57
  if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
42
- await fs.unlink(lockPath).catch(() => {});
43
- continue;
58
+ const raw = await fs.readFile(lockPath, 'utf8').catch(() => '');
59
+ if (canReclaimStaleLock(raw)) {
60
+ await fs.unlink(lockPath).catch(() => {});
61
+ continue;
62
+ }
44
63
  }
45
64
  } catch {
46
65
  continue; // Lock disappeared — retry immediately
@@ -51,7 +70,13 @@ export async function acquireLock(lockPath: string): Promise<void> {
51
70
  }
52
71
  }
53
72
  }
54
- // Last resort: force-remove stale lock and try once more
73
+ // Last resort: reclaim only a stale lock whose recorded owner is no longer
74
+ // alive. Never delete a long-running live writer's lock just because the
75
+ // operation exceeded the retry window.
76
+ const raw = await fs.readFile(lockPath, 'utf8').catch(() => '');
77
+ if (!canReclaimStaleLock(raw)) {
78
+ throw new Error(`Failed to acquire lock: ${lockPath} (owner is still running)`);
79
+ }
55
80
  await fs.unlink(lockPath).catch(() => {});
56
81
  try {
57
82
  const fd = await fs.open(lockPath, 'wx');