memorix 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/cli/index.js +1888 -1122
  3. package/dist/cli/memcode.js +12 -1
  4. package/dist/index.js +641 -113
  5. package/dist/maintenance-runner.js +618 -90
  6. package/dist/media-audio-runner.js +618 -90
  7. package/dist/media-video-runner.js +618 -90
  8. package/dist/memcode-runtime/CHANGELOG.md +46 -0
  9. package/dist/sdk.js +674 -117
  10. package/dist/vector-backfill-runner.js +618 -90
  11. package/docs/AGENT_OPERATOR_PLAYBOOK.md +11 -3
  12. package/docs/API_REFERENCE.md +1 -1
  13. package/docs/DOCKER.md +5 -1
  14. package/docs/PERFORMANCE.md +17 -2
  15. package/docs/README.md +3 -1
  16. package/docs/SETUP.md +8 -2
  17. package/package.json +2 -1
  18. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  19. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  20. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/copilot/memorix/plugin.json +1 -1
  22. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  23. package/plugins/omp/memorix/package.json +1 -1
  24. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/pi/memorix/package.json +1 -1
  26. package/server.json +2 -2
  27. package/src/cli/commands/agent-integrations.ts +9 -3
  28. package/src/cli/commands/background.ts +34 -2
  29. package/src/cli/commands/doctor.ts +46 -15
  30. package/src/cli/commands/hook.ts +13 -1
  31. package/src/cli/commands/operator-shared.ts +6 -3
  32. package/src/cli/commands/serve-http.ts +72 -7
  33. package/src/cli/heap.ts +76 -0
  34. package/src/cli/invocation.ts +7 -4
  35. package/src/embedding/api-provider.ts +503 -27
  36. package/src/embedding/provider.ts +96 -9
  37. package/src/hooks/handler.ts +9 -18
  38. package/src/hooks/lifecycle.ts +174 -0
  39. package/src/memory/observations.ts +68 -18
  40. package/src/sdk.ts +12 -8
  41. package/src/store/orama-store.ts +21 -3
@@ -29,13 +29,19 @@ import { canManageObservation, filterReadableObservations } from '../../memory/v
29
29
  import { parseTcpPortOrReport } from '../port.js';
30
30
  import { mcpFileUriToPath } from '../mcp-root-path.js';
31
31
 
32
- export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
32
+ /**
33
+ * HTTP MCP is a shared control plane. A normal thinking break should not
34
+ * silently invalidate a client session. Operators can choose a shorter GC
35
+ * window, or set 0 to disable idle GC entirely.
36
+ */
37
+ export const DEFAULT_SESSION_TIMEOUT_MS = 12 * 60 * 60 * 1000;
38
+ export const EXPIRED_SESSION_TTL_MS = 10 * 60 * 1000;
33
39
 
34
40
  export function parseSessionTimeoutMs(raw: string | undefined): number {
35
41
  const value = raw?.trim();
36
42
  if (!value) return DEFAULT_SESSION_TIMEOUT_MS;
37
43
  const parsed = Number(value);
38
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_SESSION_TIMEOUT_MS;
44
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_SESSION_TIMEOUT_MS;
39
45
  return Math.floor(parsed);
40
46
  }
41
47
 
@@ -132,7 +138,11 @@ export default defineCommand({
132
138
  console.error(`[memorix] HTTP transport starting on ${host}:${port}`);
133
139
  console.error(`[memorix] Project root: ${projectRoot}`);
134
140
  const sessionTimeoutMs = parseSessionTimeoutMs(process.env.MEMORIX_SESSION_TIMEOUT_MS);
135
- console.error(`[memorix] HTTP session idle timeout: ${Math.round(sessionTimeoutMs / 60000)}min (${sessionTimeoutMs}ms)`);
141
+ console.error(
142
+ sessionTimeoutMs === 0
143
+ ? '[memorix] HTTP session idle timeout: disabled'
144
+ : `[memorix] HTTP session idle timeout: ${Math.round(sessionTimeoutMs / 60000)}min (${sessionTimeoutMs}ms)`,
145
+ );
136
146
 
137
147
  // Per-project TeamStore cache (Option B) — each dataDir gets its own TeamStore instance.
138
148
  // This ensures true isolation between projects in HTTP control-plane mode.
@@ -166,6 +176,9 @@ export default defineCommand({
166
176
 
167
177
  // Session activity tracking (for GC timeout)
168
178
  const sessionLastActivity = new Map<string, number>();
179
+ // Keep a short-lived tombstone so an expired client gets an actionable
180
+ // reinitialize response instead of a generic bad request.
181
+ const expiredSessions = new Map<string, number>();
169
182
 
170
183
  // Probe session noise reduction: defer init logs, suppress short-lived sessions
171
184
  let suppressedProbeCount = 0;
@@ -182,6 +195,30 @@ export default defineCommand({
182
195
  }
183
196
  }
184
197
 
198
+ function forgetExpiredSessions(now = Date.now()): void {
199
+ for (const [sessionId, expiresAt] of expiredSessions) {
200
+ if (expiresAt <= now) expiredSessions.delete(sessionId);
201
+ }
202
+ }
203
+
204
+ function sendUnknownSession(res: ServerResponse, sessionId: string | undefined, jsonRpc = false): void {
205
+ const expired = Boolean(sessionId && expiredSessions.has(sessionId));
206
+ const message = expired
207
+ ? 'MCP session expired after inactivity. Initialize a new session and retry the request.'
208
+ : 'MCP session not found. Initialize a new session and retry the request.';
209
+ if (jsonRpc) {
210
+ res.writeHead(404, { 'Content-Type': 'application/json' });
211
+ res.end(JSON.stringify({
212
+ jsonrpc: '2.0',
213
+ error: { code: -32001, message },
214
+ id: null,
215
+ }));
216
+ return;
217
+ }
218
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
219
+ res.end(message);
220
+ }
221
+
185
222
  /**
186
223
  * Parse JSON body from IncomingMessage
187
224
  */
@@ -360,6 +397,11 @@ export default defineCommand({
360
397
  return;
361
398
  }
362
399
 
400
+ if (sessionId) {
401
+ sendUnknownSession(res, sessionId, true);
402
+ return;
403
+ }
404
+
363
405
  if (!sessionId && isInitializeRequest(body)) {
364
406
  // New session — create transport + server
365
407
  let createdState: SessionState | null = null;
@@ -408,6 +450,10 @@ export default defineCommand({
408
450
  {
409
451
  allowUntrackedFallback: false,
410
452
  deferProjectInitUntilBound: true,
453
+ // Same as stdio: initialize/tools/list must not hydrate 40k+
454
+ // observations on the HTTP thread. /health and the LaunchAgent
455
+ // watchdog die if createMemorixServer blocks here.
456
+ deferProjectRuntimeInit: true,
411
457
  dashboardMode: 'control-plane',
412
458
  dashboardPort: port,
413
459
  toolProfile,
@@ -488,8 +534,12 @@ export default defineCommand({
488
534
  async function handleGet(req: IncomingMessage, res: ServerResponse) {
489
535
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
490
536
  if (!sessionId || !sessions.has(sessionId)) {
491
- res.writeHead(400, { 'Content-Type': 'text/plain' });
492
- res.end('Invalid or missing session ID');
537
+ if (!sessionId) {
538
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
539
+ res.end('Missing MCP session ID');
540
+ return;
541
+ }
542
+ sendUnknownSession(res, sessionId);
493
543
  return;
494
544
  }
495
545
 
@@ -502,8 +552,12 @@ export default defineCommand({
502
552
  async function handleDelete(req: IncomingMessage, res: ServerResponse) {
503
553
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
504
554
  if (!sessionId || !sessions.has(sessionId)) {
505
- res.writeHead(400, { 'Content-Type': 'text/plain' });
506
- res.end('Invalid or missing session ID');
555
+ if (!sessionId) {
556
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
557
+ res.end('Missing MCP session ID');
558
+ return;
559
+ }
560
+ sendUnknownSession(res, sessionId);
507
561
  return;
508
562
  }
509
563
 
@@ -1414,6 +1468,7 @@ export default defineCommand({
1414
1468
  // Lightweight health check — responds immediately, no heavy init required.
1415
1469
  // This is the readiness signal for background start / status.
1416
1470
  if (url.pathname === '/health') {
1471
+ const { getEmbeddingRuntimeHealth } = await import('../../embedding/provider.js');
1417
1472
  res.writeHead(200, { 'Content-Type': 'application/json' });
1418
1473
  res.end(JSON.stringify({
1419
1474
  status: 'ok',
@@ -1421,6 +1476,7 @@ export default defineCommand({
1421
1476
  port,
1422
1477
  uptime: Math.round(process.uptime()),
1423
1478
  pid: process.pid,
1479
+ embedding: getEmbeddingRuntimeHealth(),
1424
1480
  }));
1425
1481
  return;
1426
1482
  }
@@ -1491,6 +1547,12 @@ export default defineCommand({
1491
1547
  await serveDashStatic(req, res);
1492
1548
  });
1493
1549
 
1550
+ // MCP session continuity is independent from one TCP socket. Keep Node's
1551
+ // socket timeout above its 5s default while the session map remains the
1552
+ // actual continuity boundary.
1553
+ httpServer.keepAliveTimeout = 60_000;
1554
+ httpServer.headersTimeout = 65_000;
1555
+
1494
1556
  httpServer.listen(port, host, () => {
1495
1557
  // Write readiness file — background start polls this for out-of-band readiness detection
1496
1558
  try {
@@ -1521,10 +1583,13 @@ export default defineCommand({
1521
1583
  const gcInterval = setInterval(() => {
1522
1584
  maybeProbeSummary(); // Flush suppressed probe count periodically
1523
1585
  const now = Date.now();
1586
+ forgetExpiredSessions(now);
1587
+ if (SESSION_TIMEOUT_MS === 0) return;
1524
1588
  for (const [sid, state] of sessions) {
1525
1589
  const lastActive = sessionLastActivity.get(sid) ?? 0;
1526
1590
  if (now - lastActive > SESSION_TIMEOUT_MS) {
1527
1591
  console.error(`[memorix] Session ${sid.slice(0, 8)}… timed out (idle ${Math.round((now - lastActive) / 60000)}min), closing`);
1592
+ expiredSessions.set(sid, now + EXPIRED_SESSION_TTL_MS);
1528
1593
  state.transport.close().catch(() => {});
1529
1594
  sessions.delete(sid);
1530
1595
  sessionLastActivity.delete(sid);
@@ -0,0 +1,76 @@
1
+ /**
2
+ * CLI heap selection for the tsup respawn banner.
3
+ *
4
+ * `memorix serve` and other long-lived commands need a large V8 heap for
5
+ * embeddings and the in-memory index. `memorix hook` is a per-event
6
+ * lifecycle process and must not reserve a 4 GB heap — copies accumulate.
7
+ */
8
+
9
+ export const DEFAULT_CLI_HEAP_MB = 4096;
10
+ export const DEFAULT_HOOK_HEAP_MB = 512;
11
+ export const MIN_HEAP_MB = 64;
12
+ export const MAX_HEAP_MB = 16_384;
13
+
14
+ function parseHeapMb(raw: string | undefined, fallback: number): number {
15
+ if (raw === undefined || raw.trim() === '') return fallback;
16
+ const parsed = Number(raw);
17
+ if (!Number.isFinite(parsed)) return fallback;
18
+ const mb = Math.floor(parsed);
19
+ if (mb < MIN_HEAP_MB || mb > MAX_HEAP_MB) return fallback;
20
+ return mb;
21
+ }
22
+
23
+ /**
24
+ * True when the CLI argv selects the short-lived `hook` command.
25
+ * `hooks` (install/status) is a different command and keeps the large heap.
26
+ */
27
+ export function isHookCliInvocation(argv: string[]): boolean {
28
+ for (let index = 0; index < argv.length; index += 1) {
29
+ const argument = argv[index];
30
+ if (argument === '--') break;
31
+ if (argument.startsWith('-')) {
32
+ if (!argument.includes('=') && argv[index + 1] && !argv[index + 1].startsWith('-')) {
33
+ index += 1;
34
+ }
35
+ continue;
36
+ }
37
+ return argument === 'hook';
38
+ }
39
+ return false;
40
+ }
41
+
42
+ export function resolveCliHeapMb(
43
+ argv: string[],
44
+ env: NodeJS.ProcessEnv = process.env,
45
+ ): number {
46
+ if (isHookCliInvocation(argv)) {
47
+ return parseHeapMb(env.MEMORIX_HOOK_HEAP_MB, DEFAULT_HOOK_HEAP_MB);
48
+ }
49
+ return parseHeapMb(env.MEMORIX_HEAP_MB, DEFAULT_CLI_HEAP_MB);
50
+ }
51
+
52
+ /**
53
+ * Self-contained JS prepended to the bundled CLI. It runs before the bundle
54
+ * loads, so the selection logic is inlined rather than imported.
55
+ */
56
+ export function buildCliHeapBannerPrelude(): string {
57
+ return [
58
+ '#!/usr/bin/env node',
59
+ 'import {spawnSync as __ms} from "node:child_process";',
60
+ 'import {fileURLToPath as __fu} from "node:url";',
61
+ 'if(!process.env.__MEMORIX_HEAP){process.env.__MEMORIX_HEAP="1";',
62
+ 'const __args=process.argv.slice(2);',
63
+ 'let __cmd;',
64
+ 'for(let __i=0;__i<__args.length;__i++){',
65
+ 'const __a=__args[__i];',
66
+ 'if(__a==="--")break;',
67
+ 'if(__a.startsWith("-")){if(!__a.includes("=")&&__args[__i+1]&&!__args[__i+1].startsWith("-"))__i++;continue;}',
68
+ '__cmd=__a;break;}',
69
+ `const __fallback=__cmd==="hook"?${DEFAULT_HOOK_HEAP_MB}:${DEFAULT_CLI_HEAP_MB};`,
70
+ 'const __raw=__cmd==="hook"?process.env.MEMORIX_HOOK_HEAP_MB:process.env.MEMORIX_HEAP_MB;',
71
+ 'const __n=Number(__raw);',
72
+ `const __heap=(Number.isFinite(__n)&&__n>=${MIN_HEAP_MB}&&__n<=${MAX_HEAP_MB})?String(Math.floor(__n)):String(__fallback);`,
73
+ 'let r=__ms(process.execPath,["--max-old-space-size="+__heap,__fu(import.meta.url),...process.argv.slice(2)],{stdio:"inherit",env:process.env,windowsHide:true});',
74
+ 'process.exit(r.status??1);}',
75
+ ].join('\n');
76
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, statSync } from 'node:fs';
1
+ import { existsSync, realpathSync, statSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
4
  export interface CliInvocation {
@@ -88,10 +88,13 @@ export function normalizeCliInvocation(argv: string[] = process.argv): CliInvoca
88
88
  }
89
89
 
90
90
  if (invocation.projectRoot) {
91
- const projectRoot = resolve(invocation.projectRoot);
92
- if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
93
- throw new Error(`CLI project directory does not exist: ${projectRoot}`);
91
+ const resolvedProjectRoot = resolve(invocation.projectRoot);
92
+ if (!existsSync(resolvedProjectRoot) || !statSync(resolvedProjectRoot).isDirectory()) {
93
+ throw new Error(`CLI project directory does not exist: ${resolvedProjectRoot}`);
94
94
  }
95
+ // Keep one project identity when an OS exposes the same directory through
96
+ // two paths (for example /var and /private/var on macOS).
97
+ const projectRoot = realpathSync(resolvedProjectRoot);
95
98
  process.chdir(projectRoot);
96
99
  process.env.MEMORIX_CLI_PROJECT_ROOT = projectRoot;
97
100
  invocation.projectRoot = projectRoot;