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.
- package/CHANGELOG.md +75 -1
- package/README.md +12 -6
- package/README.zh-CN.md +12 -6
- package/dist/cli/index.js +2533 -585
- package/dist/index.d.ts +15 -8
- package/dist/index.js +1201 -284
- package/dist/maintenance-runner.js +387 -216
- package/dist/media-audio-runner.js +300 -129
- package/dist/media-video-runner.js +300 -129
- package/dist/memcode-runtime/CHANGELOG.md +75 -1
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +1882 -998
- package/dist/types.d.ts +1 -1
- package/dist/vector-backfill-runner.js +300 -129
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
- package/docs/API_REFERENCE.md +39 -1
- package/docs/CONFIGURATION.md +58 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/DOCKER.md +23 -3
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +61 -1
- package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
- package/docs/PERFORMANCE.md +46 -2
- package/docs/README.md +2 -1
- package/docs/SETUP.md +56 -1
- package/package.json +3 -2
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/command-guide.ts +15 -3
- package/src/cli/commands/agent-integrations.ts +57 -0
- package/src/cli/commands/background.ts +11 -2
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/continuity.ts +155 -0
- package/src/cli/commands/init.ts +5 -2
- package/src/cli/commands/serve-http.ts +435 -84
- package/src/cli/commands/setup.ts +77 -10
- package/src/cli/commands/sync-store.ts +47 -12
- package/src/cli/commands/sync.ts +26 -0
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ConfigureView.tsx +2 -0
- package/src/codegraph/auto-context.ts +17 -0
- package/src/config/legacy-loader.ts +2 -2
- package/src/config/resolved-config.ts +8 -5
- package/src/config/yaml-loader.ts +20 -10
- package/src/dashboard/server.ts +10 -12
- package/src/embedding/api-provider.ts +4 -2
- package/src/embedding/fastembed-provider.ts +47 -14
- package/src/embedding/transformers-provider.ts +29 -7
- package/src/evaluation/memory-quality-gate.ts +90 -0
- package/src/hooks/official-skills.ts +2 -0
- package/src/integrations/registry.ts +1 -1
- package/src/knowledge/context-assembly.ts +1 -0
- package/src/knowledge/context-receipt.ts +15 -0
- package/src/knowledge/task-continuity.ts +413 -0
- package/src/knowledge/workset.ts +77 -0
- package/src/llm/provider.ts +2 -1
- package/src/memory/graph.ts +10 -8
- package/src/memory/session.ts +1 -1
- package/src/orchestrate/coordinator.ts +60 -0
- package/src/orchestrate/effect-ledger.ts +160 -0
- package/src/orchestrate/pipeline-trace.ts +40 -6
- package/src/runtime/maintenance-jobs.ts +12 -6
- package/src/sdk.ts +24 -0
- package/src/server/mcp-binding-store.ts +14 -3
- package/src/server/modern-mcp-bridge.ts +153 -27
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +131 -7
- package/src/store/file-lock.ts +28 -3
- package/src/store/graph-store.ts +178 -46
- package/src/store/persistence.ts +14 -9
- package/src/store/sqlite-db.ts +150 -7
- package/src/store/sqlite-store.ts +32 -27
- package/src/sync/engine.ts +9 -7
- package/src/sync/namespace.ts +23 -0
- package/src/sync/policy.ts +19 -4
- package/src/sync/store-port.ts +43 -13
- package/src/sync/types.ts +2 -0
- package/src/team/team-store.ts +62 -27
- package/src/types.ts +1 -1
- 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
|
|
149
|
+
export async function createModernMcpRuntime(
|
|
46
150
|
options: ModernMcpBridgeOptions,
|
|
47
|
-
): Promise<
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
102
|
-
await
|
|
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 —
|
|
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 —
|
|
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
|
-
|
|
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,
|
package/src/store/file-lock.ts
CHANGED
|
@@ -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.
|
|
43
|
-
|
|
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:
|
|
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');
|