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.
- package/CHANGELOG.md +46 -0
- package/dist/cli/index.js +1888 -1122
- package/dist/cli/memcode.js +12 -1
- package/dist/index.js +641 -113
- package/dist/maintenance-runner.js +618 -90
- package/dist/media-audio-runner.js +618 -90
- package/dist/media-video-runner.js +618 -90
- package/dist/memcode-runtime/CHANGELOG.md +46 -0
- package/dist/sdk.js +674 -117
- package/dist/vector-backfill-runner.js +618 -90
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +11 -3
- package/docs/API_REFERENCE.md +1 -1
- package/docs/DOCKER.md +5 -1
- package/docs/PERFORMANCE.md +17 -2
- package/docs/README.md +3 -1
- package/docs/SETUP.md +8 -2
- package/package.json +2 -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/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/commands/agent-integrations.ts +9 -3
- package/src/cli/commands/background.ts +34 -2
- package/src/cli/commands/doctor.ts +46 -15
- package/src/cli/commands/hook.ts +13 -1
- package/src/cli/commands/operator-shared.ts +6 -3
- package/src/cli/commands/serve-http.ts +72 -7
- package/src/cli/heap.ts +76 -0
- package/src/cli/invocation.ts +7 -4
- package/src/embedding/api-provider.ts +503 -27
- package/src/embedding/provider.ts +96 -9
- package/src/hooks/handler.ts +9 -18
- package/src/hooks/lifecycle.ts +174 -0
- package/src/memory/observations.ts +68 -18
- package/src/sdk.ts +12 -8
- 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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
492
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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);
|
package/src/cli/heap.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/invocation.ts
CHANGED
|
@@ -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
|
|
92
|
-
if (!existsSync(
|
|
93
|
-
throw new Error(`CLI project directory does not exist: ${
|
|
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;
|