memorix 1.9.4 → 1.9.5
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 +33 -1
- package/README.md +6 -5
- package/README.zh-CN.md +6 -5
- package/dist/cli/index.js +890 -325
- package/dist/index.d.ts +3 -1
- package/dist/index.js +251 -104
- package/dist/maintenance-runner.js +144 -113
- package/dist/media-audio-runner.js +57 -26
- package/dist/media-video-runner.js +57 -26
- package/dist/memcode-runtime/CHANGELOG.md +33 -1
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +1012 -842
- package/dist/types.d.ts +1 -1
- package/dist/vector-backfill-runner.js +57 -26
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
- package/docs/API_REFERENCE.md +3 -1
- package/docs/CONFIGURATION.md +15 -1
- package/docs/DOCKER.md +23 -3
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +26 -1
- package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
- package/docs/SETUP.md +55 -0
- package/package.json +2 -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/command-guide.ts +2 -2
- package/src/cli/commands/agent-integrations.ts +57 -0
- package/src/cli/commands/background.ts +11 -2
- package/src/cli/commands/serve-http.ts +193 -42
- 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/dashboard/server.ts +10 -12
- package/src/integrations/registry.ts +1 -1
- package/src/memory/graph.ts +10 -8
- package/src/memory/session.ts +1 -1
- package/src/sdk.ts +24 -0
- package/src/server/mcp-binding-store.ts +14 -3
- package/src/server.ts +2 -2
- 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 +6 -3
- 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/types.ts +1 -1
- package/src/workspace/mcp-adapters/grok.ts +32 -0
|
@@ -168,6 +168,8 @@ function worstStatus(statuses: AgentIntegrationStatus[]): AgentIntegrationStatus
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function isMcpConfigAgent(agent: AgentName): agent is McpConfigAgent {
|
|
171
|
+
// Grok has a setup-only HTTP adapter. Doctor/repair skip it so a default
|
|
172
|
+
// host-owned Grok MCP is not reported as missing.
|
|
171
173
|
return agent !== 'pi' && agent !== 'grok';
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -773,7 +775,62 @@ async function inspectCodexPluginMcp(): Promise<AgentMcpCheck | null> {
|
|
|
773
775
|
}
|
|
774
776
|
}
|
|
775
777
|
|
|
778
|
+
async function inspectOptionalGrokMcp(scope: AgentIntegrationScope): Promise<AgentIntegrationEntry['mcp']> {
|
|
779
|
+
if (scope === 'project' || scope === 'local') {
|
|
780
|
+
return { status: 'skipped', issues: ['mcp-host-owned'], checks: [] };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const adapter = getMcpAdapter('grok');
|
|
784
|
+
const configPath = adapter.getConfigPath();
|
|
785
|
+
if (!existsSync(configPath)) {
|
|
786
|
+
return {
|
|
787
|
+
status: 'skipped',
|
|
788
|
+
issues: ['mcp-host-owned'],
|
|
789
|
+
checks: [{
|
|
790
|
+
scope: 'global',
|
|
791
|
+
path: configPath,
|
|
792
|
+
exists: false,
|
|
793
|
+
status: 'skipped',
|
|
794
|
+
issues: ['mcp-host-owned'],
|
|
795
|
+
}],
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const content = await readFile(configPath, 'utf-8');
|
|
800
|
+
const server = adapter.parse(content).find((entry) => entry.name === 'memorix');
|
|
801
|
+
if (!server) {
|
|
802
|
+
return {
|
|
803
|
+
status: 'skipped',
|
|
804
|
+
issues: ['mcp-host-owned'],
|
|
805
|
+
checks: [{
|
|
806
|
+
scope: 'global',
|
|
807
|
+
path: configPath,
|
|
808
|
+
exists: true,
|
|
809
|
+
status: 'skipped',
|
|
810
|
+
issues: ['mcp-host-owned'],
|
|
811
|
+
}],
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const issues: string[] = [];
|
|
816
|
+
if (!server.url && looksLikeStaleMemorixCommand(server)) issues.push('stale-command-path');
|
|
817
|
+
if (!server.url && !isRecommendedStdioServer(server)) issues.push('nonstandard-mcp-command');
|
|
818
|
+
return {
|
|
819
|
+
status: issues.length > 0 ? 'repairable' : 'ok',
|
|
820
|
+
issues,
|
|
821
|
+
checks: [{
|
|
822
|
+
scope: 'global',
|
|
823
|
+
path: configPath,
|
|
824
|
+
exists: true,
|
|
825
|
+
status: issues.length > 0 ? 'repairable' : 'ok',
|
|
826
|
+
issues,
|
|
827
|
+
server: sanitizeServer(server),
|
|
828
|
+
}],
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
776
832
|
async function inspectMcp(agent: AgentName, projectRoot: string, scope: AgentIntegrationScope): Promise<AgentIntegrationEntry['mcp']> {
|
|
833
|
+
if (agent === 'grok') return inspectOptionalGrokMcp(scope);
|
|
777
834
|
if (!isMcpConfigAgent(agent)) {
|
|
778
835
|
return { status: 'skipped', issues: ['mcp-managed-by-package'], checks: [] };
|
|
779
836
|
}
|
|
@@ -570,8 +570,17 @@ async function doStartUnlocked(port: number): Promise<void> {
|
|
|
570
570
|
if (healthy) {
|
|
571
571
|
process.stderr.write('[OK] Control plane is running and healthy.\n');
|
|
572
572
|
} else {
|
|
573
|
-
process.stderr.write('[
|
|
574
|
-
|
|
573
|
+
process.stderr.write('[ERROR] Health check timed out; refusing to leave an unverified background state.\n');
|
|
574
|
+
if (isProcessRunning(pid)) {
|
|
575
|
+
killProcess(pid);
|
|
576
|
+
for (let i = 0; i < 10 && isProcessRunning(pid); i++) {
|
|
577
|
+
await new Promise(r => setTimeout(r, 250));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
clearState();
|
|
581
|
+
try { fs.unlinkSync(getMemorixDir() + '/background.ready'); } catch { /* best effort */ }
|
|
582
|
+
process.exitCode = 1;
|
|
583
|
+
return;
|
|
575
584
|
}
|
|
576
585
|
|
|
577
586
|
const footer = [
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { defineCommand } from 'citty';
|
|
23
23
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
24
24
|
import { getHeapStatistics } from 'node:v8';
|
|
25
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
25
26
|
import type { ObservationStore } from '../../store/obs-store.js';
|
|
26
27
|
import { resolveToolProfile } from '../../server/tool-profile.js';
|
|
27
28
|
import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
|
|
@@ -39,6 +40,9 @@ import { mcpFileUriToPath } from '../mcp-root-path.js';
|
|
|
39
40
|
*/
|
|
40
41
|
export const DEFAULT_SESSION_TIMEOUT_MS = 12 * 60 * 60 * 1000;
|
|
41
42
|
export const EXPIRED_SESSION_TTL_MS = 10 * 60 * 1000;
|
|
43
|
+
export const DEFAULT_HTTP_BODY_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
44
|
+
export const MAX_HTTP_BODY_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
45
|
+
export const MIN_HTTP_BODY_LIMIT_BYTES = 64 * 1024;
|
|
42
46
|
/** Pinned SDK protocol accepted internally while the HTTP boundary speaks the current stateless contract. */
|
|
43
47
|
export const MCP_SDK_COMPAT_PROTOCOL_VERSION = '2025-11-25';
|
|
44
48
|
|
|
@@ -56,6 +60,101 @@ export function parseSessionTimeoutMs(raw: string | undefined): number {
|
|
|
56
60
|
return Math.floor(parsed);
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
export function parseHttpBodyLimit(raw: string | undefined): number {
|
|
64
|
+
const value = raw?.trim();
|
|
65
|
+
if (!value) return DEFAULT_HTTP_BODY_LIMIT_BYTES;
|
|
66
|
+
const parsed = Number(value);
|
|
67
|
+
if (!Number.isSafeInteger(parsed) || parsed < MIN_HTTP_BODY_LIMIT_BYTES) {
|
|
68
|
+
return DEFAULT_HTTP_BODY_LIMIT_BYTES;
|
|
69
|
+
}
|
|
70
|
+
return Math.min(parsed, MAX_HTTP_BODY_LIMIT_BYTES);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isLoopbackHost(host: string): boolean {
|
|
74
|
+
const normalized = host.trim().toLowerCase();
|
|
75
|
+
return normalized === 'localhost'
|
|
76
|
+
|| normalized === '127.0.0.1'
|
|
77
|
+
|| normalized === '::1'
|
|
78
|
+
|| normalized === '[::1]';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validateHttpBindSecurity(
|
|
82
|
+
host: string,
|
|
83
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
84
|
+
): { requiresAuth: boolean; explicitlyUnauthenticated: boolean } {
|
|
85
|
+
const hasToken = Boolean(env.MEMORIX_HTTP_AUTH_TOKEN?.trim());
|
|
86
|
+
const explicitlyUnauthenticated = env.MEMORIX_HTTP_ALLOW_UNAUTHENTICATED_BIND === '1';
|
|
87
|
+
if (!isLoopbackHost(host) && !hasToken && !explicitlyUnauthenticated) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[memorix] Refusing unauthenticated non-loopback HTTP bind on ${host}. `
|
|
90
|
+
+ 'Set MEMORIX_HTTP_AUTH_TOKEN or explicitly set MEMORIX_HTTP_ALLOW_UNAUTHENTICATED_BIND=1.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return { requiresAuth: hasToken, explicitlyUnauthenticated };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isAuthorizedHttpRequest(req: IncomingMessage, token: string): boolean {
|
|
97
|
+
const header = req.headers.authorization;
|
|
98
|
+
if (!header) return false;
|
|
99
|
+
const match = /^Bearer\s+(.+)$/iu.exec(header);
|
|
100
|
+
if (!match) return false;
|
|
101
|
+
const provided = Buffer.from(match[1], 'utf8');
|
|
102
|
+
const expected = Buffer.from(token, 'utf8');
|
|
103
|
+
return provided.length === expected.length && timingSafeEqual(provided, expected);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class HttpPayloadTooLargeError extends Error {
|
|
107
|
+
readonly statusCode = 413;
|
|
108
|
+
|
|
109
|
+
constructor(limitBytes: number) {
|
|
110
|
+
super(`Request body exceeds the ${limitBytes} byte limit.`);
|
|
111
|
+
this.name = 'HttpPayloadTooLargeError';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseJsonBody(req: IncomingMessage, limitBytes: number): Promise<unknown> {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const contentLength = Number(req.headers['content-length']);
|
|
118
|
+
if (Number.isSafeInteger(contentLength) && contentLength > limitBytes) {
|
|
119
|
+
req.resume();
|
|
120
|
+
reject(new HttpPayloadTooLargeError(limitBytes));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const chunks: Buffer[] = [];
|
|
125
|
+
let size = 0;
|
|
126
|
+
let settled = false;
|
|
127
|
+
const fail = (error: unknown) => {
|
|
128
|
+
if (settled) return;
|
|
129
|
+
settled = true;
|
|
130
|
+
reject(error);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
req.on('data', (chunk: Buffer) => {
|
|
134
|
+
if (settled) return;
|
|
135
|
+
size += chunk.length;
|
|
136
|
+
if (size > limitBytes) {
|
|
137
|
+
req.removeAllListeners('data');
|
|
138
|
+
req.resume();
|
|
139
|
+
fail(new HttpPayloadTooLargeError(limitBytes));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
chunks.push(chunk);
|
|
143
|
+
});
|
|
144
|
+
req.on('end', () => {
|
|
145
|
+
if (settled) return;
|
|
146
|
+
settled = true;
|
|
147
|
+
try {
|
|
148
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
149
|
+
resolve(body ? JSON.parse(body) : undefined);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
reject(error);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
req.on('error', fail);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
59
158
|
export interface ServeHttpConfiguredDefaults {
|
|
60
159
|
port?: number;
|
|
61
160
|
transport?: 'stdio' | 'http';
|
|
@@ -146,6 +245,14 @@ export default defineCommand({
|
|
|
146
245
|
if (resolved.transportNotice) console.error(resolved.transportNotice);
|
|
147
246
|
const port = resolved.port;
|
|
148
247
|
const host = args.host || '127.0.0.1';
|
|
248
|
+
const bindSecurity = validateHttpBindSecurity(host);
|
|
249
|
+
const httpAuthToken = process.env.MEMORIX_HTTP_AUTH_TOKEN?.trim() || '';
|
|
250
|
+
if (bindSecurity.explicitlyUnauthenticated && !isLoopbackHost(host)) {
|
|
251
|
+
console.error(`[memorix] WARNING: HTTP control plane is unauthenticated on ${host}; keep it behind a private network or authenticated proxy.`);
|
|
252
|
+
}
|
|
253
|
+
if (bindSecurity.requiresAuth) {
|
|
254
|
+
console.error('[memorix] HTTP control plane authentication is enabled (Bearer token).');
|
|
255
|
+
}
|
|
149
256
|
const toolProfile = resolveToolProfile({ explicit: args.mode, envValue: process.env.MEMORIX_MODE, fallback: 'team' });
|
|
150
257
|
|
|
151
258
|
// Priority: explicit --cwd arg > MEMORIX_PROJECT_ROOT env > process.cwd().
|
|
@@ -176,6 +283,7 @@ export default defineCommand({
|
|
|
176
283
|
console.error(`[memorix] HTTP transport starting on ${host}:${port}`);
|
|
177
284
|
console.error(`[memorix] Project root: ${projectRoot}`);
|
|
178
285
|
const sessionTimeoutMs = parseSessionTimeoutMs(process.env.MEMORIX_SESSION_TIMEOUT_MS);
|
|
286
|
+
const httpBodyLimitBytes = parseHttpBodyLimit(process.env.MEMORIX_HTTP_MAX_BODY_BYTES);
|
|
179
287
|
console.error(
|
|
180
288
|
sessionTimeoutMs === 0
|
|
181
289
|
? '[memorix] HTTP session idle timeout: disabled'
|
|
@@ -283,25 +391,6 @@ export default defineCommand({
|
|
|
283
391
|
res.end(message);
|
|
284
392
|
}
|
|
285
393
|
|
|
286
|
-
/**
|
|
287
|
-
* Parse JSON body from IncomingMessage
|
|
288
|
-
*/
|
|
289
|
-
function parseBody(req: IncomingMessage): Promise<unknown> {
|
|
290
|
-
return new Promise((resolve, reject) => {
|
|
291
|
-
const chunks: Buffer[] = [];
|
|
292
|
-
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
293
|
-
req.on('end', () => {
|
|
294
|
-
try {
|
|
295
|
-
const body = Buffer.concat(chunks).toString('utf-8');
|
|
296
|
-
resolve(body ? JSON.parse(body) : undefined);
|
|
297
|
-
} catch (err) {
|
|
298
|
-
reject(err);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
req.on('error', reject);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
394
|
/**
|
|
306
395
|
* Patch ServerResponse for MCP compatibility with strict HTTP clients (rmcp/reqwest).
|
|
307
396
|
*
|
|
@@ -442,7 +531,7 @@ export default defineCommand({
|
|
|
442
531
|
// No Access-Control-Allow-Origin at all for disallowed origins —
|
|
443
532
|
// browser will block the response (fail-closed).
|
|
444
533
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
445
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Mcp-Project-Handle, Mcp-Project-Root, Mcp-Stateless, Last-Event-Id');
|
|
534
|
+
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Project-Handle, Mcp-Project-Root, Mcp-Stateless, Last-Event-Id');
|
|
446
535
|
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id, Mcp-Project-Handle, Mcp-Protocol-Version, Mcp-Method, Mcp-Name');
|
|
447
536
|
res.setHeader('Mcp-Protocol-Version', MCP_SDK_COMPAT_PROTOCOL_VERSION);
|
|
448
537
|
res.setHeader('Mcp-Method', req.method || 'UNKNOWN');
|
|
@@ -454,7 +543,7 @@ export default defineCommand({
|
|
|
454
543
|
*/
|
|
455
544
|
async function handlePost(req: IncomingMessage, res: ServerResponse) {
|
|
456
545
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
457
|
-
const body = await
|
|
546
|
+
const body = await parseJsonBody(req, httpBodyLimitBytes);
|
|
458
547
|
|
|
459
548
|
// Keep the pre-2026 startup probe compatible. Older MCP clients sent a
|
|
460
549
|
// claim-less server/discover before initialize and expect a plain JSON
|
|
@@ -861,7 +950,7 @@ export default defineCommand({
|
|
|
861
950
|
sendJson({ error: 'Maintenance actions require POST.' }, 405);
|
|
862
951
|
return;
|
|
863
952
|
}
|
|
864
|
-
const bodyValue = await
|
|
953
|
+
const bodyValue = await parseJsonBody(req, Math.min(httpBodyLimitBytes, 1024 * 1024));
|
|
865
954
|
const body = bodyValue && typeof bodyValue === 'object' && !Array.isArray(bodyValue)
|
|
866
955
|
? bodyValue as Record<string, unknown>
|
|
867
956
|
: {};
|
|
@@ -1023,9 +1112,9 @@ export default defineCommand({
|
|
|
1023
1112
|
|
|
1024
1113
|
if (apiPath === '/stats') {
|
|
1025
1114
|
const { projectId: statsProjectId, dataDir: statsDataDir } = await resolveRequestProject(url);
|
|
1026
|
-
const { initGraphStore
|
|
1027
|
-
await initGraphStore(statsDataDir);
|
|
1028
|
-
const graph = { entities:
|
|
1115
|
+
const { initGraphStore } = await import('../../store/graph-store.js');
|
|
1116
|
+
const gStore = await initGraphStore(statsDataDir, statsProjectId);
|
|
1117
|
+
const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
|
|
1029
1118
|
|
|
1030
1119
|
const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active');
|
|
1031
1120
|
const feedback = await loadProjectFeedback(statsDataDir, statsProjectId);
|
|
@@ -1210,9 +1299,9 @@ export default defineCommand({
|
|
|
1210
1299
|
|
|
1211
1300
|
if (apiPath === '/graph') {
|
|
1212
1301
|
const { projectId: graphProjectId, dataDir: graphDataDir } = await resolveRequestProject(url);
|
|
1213
|
-
const { initGraphStore
|
|
1214
|
-
await initGraphStore(graphDataDir);
|
|
1215
|
-
const fullGraph = { entities:
|
|
1302
|
+
const { initGraphStore } = await import('../../store/graph-store.js');
|
|
1303
|
+
const gStore = await initGraphStore(graphDataDir, graphProjectId);
|
|
1304
|
+
const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
|
|
1216
1305
|
|
|
1217
1306
|
const allObs = await loadDashboardProjectObservations(graphDataDir, graphProjectId, 'active');
|
|
1218
1307
|
const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
|
|
@@ -1298,15 +1387,15 @@ export default defineCommand({
|
|
|
1298
1387
|
const { projectId: kgProjectId, dataDir: kgDataDir } = await resolveRequestProject(url);
|
|
1299
1388
|
const { generateKnowledgeGraph } = await import('../../wiki/knowledge-graph.js');
|
|
1300
1389
|
const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
|
|
1301
|
-
const { initGraphStore
|
|
1390
|
+
const { initGraphStore } = await import('../../store/graph-store.js');
|
|
1302
1391
|
|
|
1303
1392
|
await initMiniSkillStore(kgDataDir);
|
|
1304
|
-
await initGraphStore(kgDataDir);
|
|
1393
|
+
const gStore = await initGraphStore(kgDataDir, kgProjectId);
|
|
1305
1394
|
|
|
1306
1395
|
const allObs = await loadDashboardProjectObservations(kgDataDir, kgProjectId, 'active');
|
|
1307
1396
|
const skills = await getMiniSkillStore().loadByProject(kgProjectId);
|
|
1308
1397
|
|
|
1309
|
-
const fullGraph = { entities:
|
|
1398
|
+
const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
|
|
1310
1399
|
const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
|
|
1311
1400
|
|
|
1312
1401
|
const graph = generateKnowledgeGraph({
|
|
@@ -1634,9 +1723,8 @@ export default defineCommand({
|
|
|
1634
1723
|
await store.remove(obsId);
|
|
1635
1724
|
// Sync: clean up graph entity references
|
|
1636
1725
|
try {
|
|
1637
|
-
const { initGraphStore
|
|
1638
|
-
await initGraphStore(delDataDir);
|
|
1639
|
-
const gStore = getGraphStore();
|
|
1726
|
+
const { initGraphStore } = await import('../../store/graph-store.js');
|
|
1727
|
+
const gStore = await initGraphStore(delDataDir, delProjectId);
|
|
1640
1728
|
const prefix = `[#${obsId}] `;
|
|
1641
1729
|
const deletions: { entityName: string; observations: string[] }[] = [];
|
|
1642
1730
|
for (const entity of gStore.loadEntities()) {
|
|
@@ -1653,9 +1741,9 @@ export default defineCommand({
|
|
|
1653
1741
|
// GET /api/export — export observations as JSON
|
|
1654
1742
|
if (apiPath === '/export') {
|
|
1655
1743
|
const { projectId: expProjectId, projectName: expProjectName, dataDir: expDataDir } = await resolveRequestProject(url);
|
|
1656
|
-
const { initGraphStore
|
|
1657
|
-
await initGraphStore(expDataDir);
|
|
1658
|
-
const fullGraph = { entities:
|
|
1744
|
+
const { initGraphStore } = await import('../../store/graph-store.js');
|
|
1745
|
+
const gStore = await initGraphStore(expDataDir, expProjectId);
|
|
1746
|
+
const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
|
|
1659
1747
|
const observations = await loadDashboardProjectObservations(expDataDir, expProjectId, 'active');
|
|
1660
1748
|
const expStore = await getDashboardObservationStore(expDataDir);
|
|
1661
1749
|
const nextId = await expStore.loadIdCounter();
|
|
@@ -1694,7 +1782,8 @@ export default defineCommand({
|
|
|
1694
1782
|
}, 503);
|
|
1695
1783
|
return;
|
|
1696
1784
|
}
|
|
1697
|
-
|
|
1785
|
+
const status = err instanceof HttpPayloadTooLargeError ? err.statusCode : 500;
|
|
1786
|
+
sendJson({ error: err instanceof Error ? err.message : 'Unknown error' }, status);
|
|
1698
1787
|
}
|
|
1699
1788
|
}
|
|
1700
1789
|
|
|
@@ -1711,6 +1800,15 @@ export default defineCommand({
|
|
|
1711
1800
|
|
|
1712
1801
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
1713
1802
|
|
|
1803
|
+
if (bindSecurity.requiresAuth && url.pathname !== '/health' && !isAuthorizedHttpRequest(req, httpAuthToken)) {
|
|
1804
|
+
res.writeHead(401, {
|
|
1805
|
+
'Content-Type': 'application/json',
|
|
1806
|
+
'WWW-Authenticate': 'Bearer',
|
|
1807
|
+
});
|
|
1808
|
+
res.end(JSON.stringify({ error: 'Authentication required' }));
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1714
1812
|
// Lightweight health check — responds immediately, no heavy init required.
|
|
1715
1813
|
// This is the readiness signal for background start / status.
|
|
1716
1814
|
if (url.pathname === '/health') {
|
|
@@ -1792,10 +1890,15 @@ export default defineCommand({
|
|
|
1792
1890
|
} catch (err) {
|
|
1793
1891
|
console.error('[memorix] HTTP handler error:', err);
|
|
1794
1892
|
if (!res.headersSent) {
|
|
1795
|
-
|
|
1893
|
+
const status = err instanceof HttpPayloadTooLargeError ? err.statusCode : 500;
|
|
1894
|
+
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
1895
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1796
1896
|
res.end(JSON.stringify({
|
|
1797
1897
|
jsonrpc: '2.0',
|
|
1798
|
-
error: {
|
|
1898
|
+
error: {
|
|
1899
|
+
code: status === 413 ? -32013 : -32603,
|
|
1900
|
+
message: status === 413 ? message : 'Internal server error',
|
|
1901
|
+
},
|
|
1799
1902
|
id: null,
|
|
1800
1903
|
}));
|
|
1801
1904
|
}
|
|
@@ -1819,7 +1922,13 @@ export default defineCommand({
|
|
|
1819
1922
|
httpServer.keepAliveTimeout = 60_000;
|
|
1820
1923
|
httpServer.headersTimeout = 65_000;
|
|
1821
1924
|
|
|
1822
|
-
|
|
1925
|
+
const listenPromise = new Promise<void>((resolve, reject) => {
|
|
1926
|
+
const onError = (error: Error) => {
|
|
1927
|
+
if (typeof (httpServer as any).off === 'function') httpServer.off('listening', onListening);
|
|
1928
|
+
reject(error);
|
|
1929
|
+
};
|
|
1930
|
+
const onListening = () => {
|
|
1931
|
+
if (typeof (httpServer as any).off === 'function') httpServer.off('error', onError);
|
|
1823
1932
|
httpReadyAt = new Date().toISOString();
|
|
1824
1933
|
// Write readiness file — background start polls this for out-of-band readiness detection
|
|
1825
1934
|
try {
|
|
@@ -1841,7 +1950,29 @@ export default defineCommand({
|
|
|
1841
1950
|
// Fire-and-forget: background update check after server is fully started.
|
|
1842
1951
|
// Default is notify-only. All output goes to stderr only. Failures never affect the server.
|
|
1843
1952
|
import('../update-checker.js').then(m => m.checkForUpdates()).catch(() => {});
|
|
1953
|
+
resolve();
|
|
1954
|
+
};
|
|
1955
|
+
if (typeof (httpServer as any).once === 'function') {
|
|
1956
|
+
httpServer.once('error', onError);
|
|
1957
|
+
httpServer.once('listening', onListening);
|
|
1958
|
+
httpServer.listen(port, host);
|
|
1959
|
+
} else {
|
|
1960
|
+
// Lightweight test doubles may only implement listen(callback).
|
|
1961
|
+
httpServer.listen(port, host, onListening);
|
|
1962
|
+
}
|
|
1844
1963
|
});
|
|
1964
|
+
try {
|
|
1965
|
+
await listenPromise;
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
console.error(`[memorix] HTTP server failed to listen on ${host}:${port}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1968
|
+
for (const store of dashboardObservationStores.values()) {
|
|
1969
|
+
try { store.close(); } catch { /* best-effort */ }
|
|
1970
|
+
}
|
|
1971
|
+
dashboardObservationStores.clear();
|
|
1972
|
+
const { closeAllDatabases } = await import('../../store/sqlite-db.js');
|
|
1973
|
+
closeAllDatabases();
|
|
1974
|
+
throw error;
|
|
1975
|
+
}
|
|
1845
1976
|
|
|
1846
1977
|
// Session timeout GC — close sessions idle past the configured threshold.
|
|
1847
1978
|
// MEMORIX_SESSION_TIMEOUT_MS lets operators work around HTTP clients that do
|
|
@@ -1851,6 +1982,7 @@ export default defineCommand({
|
|
|
1851
1982
|
maybeProbeSummary(); // Flush suppressed probe count periodically
|
|
1852
1983
|
const now = Date.now();
|
|
1853
1984
|
forgetExpiredSessions(now);
|
|
1985
|
+
statelessBindingStore.cleanupExpired(now);
|
|
1854
1986
|
if (SESSION_TIMEOUT_MS === 0) return;
|
|
1855
1987
|
for (const [sid, state] of sessions) {
|
|
1856
1988
|
const lastActive = sessionLastActivity.get(sid) ?? 0;
|
|
@@ -1866,7 +1998,10 @@ export default defineCommand({
|
|
|
1866
1998
|
gcInterval.unref(); // Don't prevent process exit
|
|
1867
1999
|
|
|
1868
2000
|
// Graceful shutdown
|
|
2001
|
+
let shutdownStarted = false;
|
|
1869
2002
|
const shutdown = async () => {
|
|
2003
|
+
if (shutdownStarted) return;
|
|
2004
|
+
shutdownStarted = true;
|
|
1870
2005
|
if (suppressedProbeCount > 0) {
|
|
1871
2006
|
console.error(`[memorix] ${suppressedProbeCount} probe connection(s) suppressed during this session`);
|
|
1872
2007
|
}
|
|
@@ -1891,7 +2026,17 @@ export default defineCommand({
|
|
|
1891
2026
|
console.error(`[memorix] Error closing session ${sid}:`, err);
|
|
1892
2027
|
}
|
|
1893
2028
|
}
|
|
1894
|
-
|
|
2029
|
+
clearInterval(gcInterval);
|
|
2030
|
+
clearInterval(heartbeatInterval);
|
|
2031
|
+
await new Promise<void>((resolve) => {
|
|
2032
|
+
if (!httpServer.listening) {
|
|
2033
|
+
resolve();
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
httpServer.close(() => resolve());
|
|
2037
|
+
});
|
|
2038
|
+
const { closeAllDatabases } = await import('../../store/sqlite-db.js');
|
|
2039
|
+
closeAllDatabases();
|
|
1895
2040
|
process.exit(0);
|
|
1896
2041
|
};
|
|
1897
2042
|
|
|
@@ -1943,6 +2088,12 @@ export default defineCommand({
|
|
|
1943
2088
|
/** @internal */
|
|
1944
2089
|
export const _testing = {
|
|
1945
2090
|
DEFAULT_SESSION_TIMEOUT_MS,
|
|
2091
|
+
DEFAULT_HTTP_BODY_LIMIT_BYTES,
|
|
2092
|
+
MAX_HTTP_BODY_LIMIT_BYTES,
|
|
2093
|
+
MIN_HTTP_BODY_LIMIT_BYTES,
|
|
1946
2094
|
parseSessionTimeoutMs,
|
|
2095
|
+
parseHttpBodyLimit,
|
|
2096
|
+
isLoopbackHost,
|
|
2097
|
+
validateHttpBindSecurity,
|
|
1947
2098
|
parseTcpPortOrReport,
|
|
1948
2099
|
};
|