memorix 1.6.2 → 1.7.0
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 +34 -0
- package/dist/cli/index.js +2042 -1430
- package/dist/cli/memcode.js +0 -0
- package/dist/dashboard/static/app.js +293 -33
- package/dist/dashboard/static/index.html +17 -0
- package/dist/dashboard/static/style.css +310 -0
- package/dist/index.js +1669 -970
- package/dist/maintenance-runner.js +1061 -1017
- package/dist/media-audio-runner.js +1052 -1009
- package/dist/media-video-runner.js +1052 -1009
- package/dist/memcode-runtime/CHANGELOG.md +34 -0
- package/dist/sdk.js +2293 -1684
- package/dist/vector-backfill-runner.js +1050 -1007
- package/docs/ARCHITECTURE.md +3 -0
- package/docs/DASHBOARD.md +54 -0
- package/docs/README.md +2 -0
- package/package.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/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/background.ts +17 -3
- package/src/cli/commands/cleanup.ts +13 -114
- package/src/cli/commands/memory.ts +12 -43
- package/src/cli/commands/operator-shared.ts +12 -8
- package/src/cli/commands/serve-http.ts +25 -7
- package/src/cli/commands/serve-shared.ts +49 -6
- package/src/cli/commands/serve.ts +4 -0
- package/src/dashboard/maintenance.ts +303 -0
- package/src/dashboard/server.ts +100 -2
- package/src/dashboard/static/index.html +17 -0
- package/src/dashboard/static/style.css +310 -0
- package/src/memory/cleanup.ts +143 -0
- package/src/memory/deduplication.ts +178 -0
- package/src/memory/session.ts +13 -3
- package/src/project/launch-root.ts +115 -0
- package/src/runtime/isolated-maintenance.ts +12 -1
- package/src/server.ts +50 -51
- package/src/store/obs-store.ts +2 -2
- package/src/store/sqlite-db.ts +2 -0
- package/src/store/sqlite-store.ts +19 -4
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch-root guards for HTTP/stdio/background.
|
|
3
|
+
*
|
|
4
|
+
* LaunchAgents and Cursor stdio often start with cwd=$HOME. Binding that path
|
|
5
|
+
* treats the home directory as a project and can create a decoy ~/memorix.db
|
|
6
|
+
* while the real store lives in ~/.memorix/data/memorix.db.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
export const LAST_PROJECT_ROOT_FILENAME = 'last-project-root';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the home directory used by Memorix boundary guards.
|
|
17
|
+
*
|
|
18
|
+
* Node's os.homedir() is intentionally stable on Windows and does not always
|
|
19
|
+
* follow a process-level HOME/USERPROFILE override. The CLI and test harnesses
|
|
20
|
+
* use those environment variables as the configured home, so all guards need
|
|
21
|
+
* one shared resolution rule instead of mixing the two sources.
|
|
22
|
+
*/
|
|
23
|
+
export function effectiveHomeDirectory(): string {
|
|
24
|
+
const configuredHome = process.platform === 'win32'
|
|
25
|
+
? process.env.USERPROFILE?.trim() || process.env.HOME?.trim()
|
|
26
|
+
: process.env.HOME?.trim() || process.env.USERPROFILE?.trim();
|
|
27
|
+
return path.resolve(configuredHome || os.homedir());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isHomeDirectory(candidate: string, homeDir: string = effectiveHomeDirectory()): boolean {
|
|
31
|
+
try {
|
|
32
|
+
return path.resolve(candidate) === path.resolve(homeDir);
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function lastProjectRootPath(homeDir: string = effectiveHomeDirectory()): string {
|
|
39
|
+
return path.join(homeDir, '.memorix', LAST_PROJECT_ROOT_FILENAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readLastProjectRoot(homeDir: string = effectiveHomeDirectory()): string | null {
|
|
43
|
+
try {
|
|
44
|
+
const raw = readFileSync(lastProjectRootPath(homeDir), 'utf8').trim();
|
|
45
|
+
if (!raw) return null;
|
|
46
|
+
const resolved = path.resolve(raw);
|
|
47
|
+
if (isHomeDirectory(resolved, homeDir)) return null;
|
|
48
|
+
if (!existsSync(resolved)) return null;
|
|
49
|
+
return resolved;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeLastProjectRoot(projectRoot: string, homeDir?: string): void {
|
|
56
|
+
// Tests pass an explicit homeDir. Default writes are skipped under Vitest so
|
|
57
|
+
// unit tests do not mutate the developer's ~/.memorix/last-project-root.
|
|
58
|
+
if (process.env.VITEST && homeDir === undefined) return;
|
|
59
|
+
const resolvedHome = homeDir ?? effectiveHomeDirectory();
|
|
60
|
+
const resolved = path.resolve(projectRoot);
|
|
61
|
+
if (isHomeDirectory(resolved, resolvedHome)) return;
|
|
62
|
+
if (!existsSync(resolved)) return;
|
|
63
|
+
const memorixDir = path.join(resolvedHome, '.memorix');
|
|
64
|
+
mkdirSync(memorixDir, { recursive: true });
|
|
65
|
+
writeFileSync(lastProjectRootPath(resolvedHome), `${resolved}\n`, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function homeProjectRootError(candidate: string): string {
|
|
69
|
+
return (
|
|
70
|
+
`Refusing to bind $HOME as a project root (${candidate}). ` +
|
|
71
|
+
'Pass a git project via --cwd, MEMORIX_PROJECT_ROOT, or memorix_session_start({ projectRoot }). ' +
|
|
72
|
+
'Memorix will not create ~/memorix.db.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* When the launcher cwd is $HOME, inherit the last git project or an explicit
|
|
78
|
+
* MEMORIX_PROJECT_ROOT. System directories stay fail-closed (no restore).
|
|
79
|
+
*/
|
|
80
|
+
export function inheritProjectRootFromHome(options: {
|
|
81
|
+
homeDir: string;
|
|
82
|
+
envProjectRoot?: string;
|
|
83
|
+
readLastRoot?: () => string | null;
|
|
84
|
+
}): string | null {
|
|
85
|
+
const envRoot = options.envProjectRoot?.trim();
|
|
86
|
+
if (envRoot && !isHomeDirectory(envRoot, options.homeDir) && existsSync(envRoot)) {
|
|
87
|
+
return path.resolve(envRoot);
|
|
88
|
+
}
|
|
89
|
+
const lastRoot = (options.readLastRoot ?? (() => readLastProjectRoot(options.homeDir)))();
|
|
90
|
+
if (lastRoot && !isHomeDirectory(lastRoot, options.homeDir) && existsSync(lastRoot)) {
|
|
91
|
+
return lastRoot;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Shared SQLite must never be opened at $HOME — that creates a decoy ~/memorix.db.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveControlPlaneDataDir(options: {
|
|
100
|
+
requestedDataDir?: string;
|
|
101
|
+
homeDir?: string;
|
|
102
|
+
globalDataDir: string;
|
|
103
|
+
}): string {
|
|
104
|
+
const homeDir = options.homeDir ?? effectiveHomeDirectory();
|
|
105
|
+
if (options.requestedDataDir && !isHomeDirectory(options.requestedDataDir, homeDir)) {
|
|
106
|
+
return options.requestedDataDir;
|
|
107
|
+
}
|
|
108
|
+
return options.globalDataDir;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function assertNotHomeDataDir(dataDir: string, homeDir: string = effectiveHomeDirectory()): void {
|
|
112
|
+
if (isHomeDirectory(dataDir, homeDir)) {
|
|
113
|
+
throw new Error(homeProjectRootError(dataDir));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
|
|
@@ -52,6 +53,16 @@ function childError(prefix: string, detail: string): Error {
|
|
|
52
53
|
return new Error(cleaned ? `${prefix}: ${cleaned}` : prefix);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Isolated jobs already receive an absolute projectRoot on stdin. Using that
|
|
58
|
+
* directory as spawn cwd keeps the folder locked on Windows (EBUSY on rmdir)
|
|
59
|
+
* for the life of the child. Park the process in a disposable temp dir instead.
|
|
60
|
+
*/
|
|
61
|
+
export function isolatedMaintenanceCwd(): string {
|
|
62
|
+
const tmp = os.tmpdir();
|
|
63
|
+
return existsSync(tmp) ? tmp : process.cwd();
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
/**
|
|
56
67
|
* Resolve the runner beside the compiled package entry. The CLI bundle lives
|
|
57
68
|
* in dist/cli while the library entry lives directly in dist.
|
|
@@ -148,7 +159,7 @@ export async function runMaintenanceInChildProcess(
|
|
|
148
159
|
let child: ChildProcessWithoutNullStreams;
|
|
149
160
|
try {
|
|
150
161
|
child = spawn(process.execPath, [runnerPath], {
|
|
151
|
-
cwd:
|
|
162
|
+
cwd: isolatedMaintenanceCwd(),
|
|
152
163
|
env,
|
|
153
164
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
154
165
|
windowsHide: true,
|
package/src/server.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { canManageObservation, canReadObservation, filterReadableObservations, r
|
|
|
33
33
|
import { compactSearch, compactTimeline, compactDetail } from './compact/engine.js';
|
|
34
34
|
import { buildGraphContextPacket, formatGraphContextPrompt } from './memory/graph-context.js';
|
|
35
35
|
import { detectProject } from './project/detector.js';
|
|
36
|
+
import { homeProjectRootError, isHomeDirectory, writeLastProjectRoot } from './project/launch-root.js';
|
|
36
37
|
import { registerAlias, initAliasRegistry, resolveAliases, autoMergeByBaseName } from './project/aliases.js';
|
|
37
38
|
import { getProjectDataDir } from './store/persistence.js';
|
|
38
39
|
import type { ObservationType, RuleSource, AgentTarget, MCPServerEntry, ObservationReader } from './types.js';
|
|
@@ -45,7 +46,8 @@ import {
|
|
|
45
46
|
type ToolProfile,
|
|
46
47
|
} from './server/tool-profile.js';
|
|
47
48
|
import { initLLM, isLLMEnabled, getLLMConfig } from './llm/provider.js';
|
|
48
|
-
import { compactOnWrite
|
|
49
|
+
import { compactOnWrite } from './llm/memory-manager.js';
|
|
50
|
+
import { applyDeduplicationPlan, planMemoryDeduplication } from './memory/deduplication.js';
|
|
49
51
|
import type { ExistingMemory } from './llm/memory-manager.js';
|
|
50
52
|
import { runFormation, getMetricsSummary, getBeforeAfterMetrics } from './memory/formation/index.js';
|
|
51
53
|
import type { FormationConfig, SearchHit, FormedMemory, FormationStage, FormationStageEvent } from './memory/formation/types.js';
|
|
@@ -228,6 +230,7 @@ export const BOOTSTRAP_SAFE_TOOL_NAMES = new Set([
|
|
|
228
230
|
'memorix_codegraph_status',
|
|
229
231
|
'memorix_graph_context',
|
|
230
232
|
'memorix_context_pack',
|
|
233
|
+
'memorix_session_start',
|
|
231
234
|
]);
|
|
232
235
|
|
|
233
236
|
const AUTOPILOT_RETRIEVAL_BOUNDARY_TTL_MS = 2 * 60 * 1000;
|
|
@@ -265,7 +268,10 @@ export async function createMemorixServer(
|
|
|
265
268
|
});
|
|
266
269
|
const teamFeaturesEnabled = isToolInProfile('team_manage', toolProfile);
|
|
267
270
|
const projectBinding = options.projectBinding ?? createProjectBindingController(cwd ?? process.cwd());
|
|
268
|
-
const
|
|
271
|
+
const detectedProjectRaw = detectProject(cwd);
|
|
272
|
+
const detectedProject = detectedProjectRaw && !isHomeDirectory(detectedProjectRaw.rootPath)
|
|
273
|
+
? detectedProjectRaw
|
|
274
|
+
: null;
|
|
269
275
|
let rawProject: import('./types.js').ProjectInfo;
|
|
270
276
|
let projectResolved = true;
|
|
271
277
|
let projectResolutionError: string | null = null;
|
|
@@ -316,6 +322,7 @@ export async function createMemorixServer(
|
|
|
316
322
|
}
|
|
317
323
|
if (projectResolved) {
|
|
318
324
|
projectBinding.recordResolvedProject(project.id, project.rootPath);
|
|
325
|
+
writeLastProjectRoot(project.rootPath);
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
const registerMaintenanceTarget = async (): Promise<void> => {
|
|
@@ -362,6 +369,7 @@ export async function createMemorixServer(
|
|
|
362
369
|
|
|
363
370
|
const lightweightUnresolvedSession = !projectResolved && deferProjectInitUntilBound;
|
|
364
371
|
let projectRuntimeInitPromise: Promise<void> | null = null;
|
|
372
|
+
let runtimeHydratedForDir: string | null = null;
|
|
365
373
|
|
|
366
374
|
const initializeProjectRuntime = async (logPrefix: 'startup' | 'switch'): Promise<void> => {
|
|
367
375
|
await initObservationStore(projectDir);
|
|
@@ -430,14 +438,20 @@ export async function createMemorixServer(
|
|
|
430
438
|
} catch { /* migration is optional */ }
|
|
431
439
|
|
|
432
440
|
await initializeProjectRuntime('startup');
|
|
441
|
+
runtimeHydratedForDir = projectDir;
|
|
433
442
|
} else {
|
|
434
443
|
// Intentionally silent — serve-http.ts deferred logging handles session lifecycle visibility.
|
|
435
444
|
// Noisy per-probe 'awaiting binding' log was removed to reduce terminal spam.
|
|
436
445
|
}
|
|
437
446
|
|
|
438
447
|
const ensureProjectRuntimeInitialized = async (): Promise<void> => {
|
|
439
|
-
if (
|
|
448
|
+
if (!projectResolved) return;
|
|
449
|
+
if (runtimeHydratedForDir === projectDir) {
|
|
450
|
+
if (projectRuntimeInitPromise) await projectRuntimeInitPromise;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
440
453
|
if (!projectRuntimeInitPromise) {
|
|
454
|
+
const targetDir = projectDir;
|
|
441
455
|
projectRuntimeInitPromise = (async () => {
|
|
442
456
|
// Auto-merge obvious alias groups by scanning observed projectIds in data.
|
|
443
457
|
try {
|
|
@@ -470,7 +484,8 @@ export async function createMemorixServer(
|
|
|
470
484
|
}
|
|
471
485
|
} catch { /* migration is optional */ }
|
|
472
486
|
|
|
473
|
-
await initializeProjectRuntime('startup');
|
|
487
|
+
await initializeProjectRuntime(runtimeHydratedForDir ? 'switch' : 'startup');
|
|
488
|
+
runtimeHydratedForDir = targetDir;
|
|
474
489
|
})().catch((err) => {
|
|
475
490
|
projectRuntimeInitPromise = null;
|
|
476
491
|
throw err;
|
|
@@ -2489,66 +2504,31 @@ export async function createMemorixServer(
|
|
|
2489
2504
|
return { content: [{ type: 'text' as const, text: 'Not enough memories in scope to deduplicate.' }] };
|
|
2490
2505
|
}
|
|
2491
2506
|
|
|
2492
|
-
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
byEntity.set(obs.entityName, list);
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
const actions: string[] = [];
|
|
2501
|
-
const toResolve: number[] = [];
|
|
2502
|
-
|
|
2503
|
-
for (const [entity, group] of byEntity) {
|
|
2504
|
-
if (group.length < 2) continue;
|
|
2505
|
-
|
|
2506
|
-
// Compare each pair within entity group
|
|
2507
|
-
for (let i = 0; i < group.length; i++) {
|
|
2508
|
-
for (let j = i + 1; j < group.length; j++) {
|
|
2509
|
-
const newer = group[j];
|
|
2510
|
-
const older = group[i];
|
|
2511
|
-
try {
|
|
2512
|
-
const decision = await deduplicateMemory(
|
|
2513
|
-
{ title: newer.title, narrative: newer.narrative, facts: newer.facts },
|
|
2514
|
-
[{ id: older.id, title: older.title, narrative: older.narrative, facts: older.facts.join('\n') }],
|
|
2515
|
-
);
|
|
2516
|
-
if (decision && decision.action === 'UPDATE' && decision.targetId) {
|
|
2517
|
-
actions.push(`[UPDATED] #${older.id} "${older.title}" → superseded by #${newer.id} (${decision.reason})${decision.usedLLM ? ' [LLM]' : ' [heuristic]'}`);
|
|
2518
|
-
toResolve.push(older.id);
|
|
2519
|
-
} else if (decision && decision.action === 'NONE') {
|
|
2520
|
-
actions.push(`[DELETE] #${newer.id} "${newer.title}" → redundant (${decision.reason})${decision.usedLLM ? ' [LLM]' : ' [heuristic]'}`);
|
|
2521
|
-
toResolve.push(newer.id);
|
|
2522
|
-
} else if (decision && decision.action === 'DELETE') {
|
|
2523
|
-
actions.push(`[ERROR] #${decision.targetId ?? older.id} → outdated (${decision.reason})${decision.usedLLM ? ' [LLM]' : ' [heuristic]'}`);
|
|
2524
|
-
toResolve.push(decision.targetId ?? older.id);
|
|
2525
|
-
}
|
|
2526
|
-
} catch (dedupErr) { actions.push(`[WARN] comparison failed: ${(dedupErr as Error)?.message ?? dedupErr}`); }
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
}
|
|
2507
|
+
const manageableCandidates = candidates.filter((observation) => canManageObservation(observation, reader));
|
|
2508
|
+
const plan = await planMemoryDeduplication(manageableCandidates);
|
|
2509
|
+
const actionLines = plan.actions.map((item) =>
|
|
2510
|
+
`Resolve #${item.resolveId} "${item.resolveTitle}"; keep #${item.keepId} "${item.keepTitle}" (${item.reason})${item.usedLLM ? ' [LLM]' : ' [heuristic]'}`,
|
|
2511
|
+
);
|
|
2530
2512
|
|
|
2531
|
-
if (
|
|
2532
|
-
return { content: [{ type: 'text' as const, text: `[OK] Scanned ${
|
|
2513
|
+
if (actionLines.length === 0) {
|
|
2514
|
+
return { content: [{ type: 'text' as const, text: `[OK] Scanned ${plan.scanned} memories across ${plan.entities} entities — no duplicates found.` }] };
|
|
2533
2515
|
}
|
|
2534
2516
|
|
|
2535
2517
|
if (dryRun) {
|
|
2536
2518
|
return {
|
|
2537
2519
|
content: [{
|
|
2538
2520
|
type: 'text' as const,
|
|
2539
|
-
text: `[SEARCH] DRY RUN — ${
|
|
2521
|
+
text: `[SEARCH] DRY RUN — ${actionLines.length} action(s) found:\n\n${actionLines.join('\n')}\n\nRun with dryRun=false to apply.`,
|
|
2540
2522
|
}],
|
|
2541
2523
|
};
|
|
2542
2524
|
}
|
|
2543
2525
|
|
|
2544
|
-
|
|
2545
|
-
const unique = [...new Set(toResolve)];
|
|
2546
|
-
await resolveObservations(unique, 'resolved');
|
|
2526
|
+
const result = await applyDeduplicationPlan(getObservationStore(), project.id, plan.resolveIds, reader);
|
|
2547
2527
|
|
|
2548
2528
|
return {
|
|
2549
2529
|
content: [{
|
|
2550
2530
|
type: 'text' as const,
|
|
2551
|
-
text: `[CLEANUP] Deduplicated: resolved ${
|
|
2531
|
+
text: `[CLEANUP] Deduplicated: resolved ${result.resolved.length} memory(ies)\n\n${actionLines.join('\n')}`,
|
|
2552
2532
|
}],
|
|
2553
2533
|
};
|
|
2554
2534
|
},
|
|
@@ -3846,6 +3826,15 @@ export async function createMemorixServer(
|
|
|
3846
3826
|
// BEFORE checking whether the project is resolved. This is the primary
|
|
3847
3827
|
// mechanism for HTTP/control-plane multi-project support.
|
|
3848
3828
|
if (explicitRoot && typeof explicitRoot === 'string') {
|
|
3829
|
+
if (isHomeDirectory(explicitRoot)) {
|
|
3830
|
+
return {
|
|
3831
|
+
content: [{
|
|
3832
|
+
type: 'text' as const,
|
|
3833
|
+
text: homeProjectRootError(explicitRoot),
|
|
3834
|
+
}],
|
|
3835
|
+
isError: true as const,
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3849
3838
|
let bound = await switchProject(explicitRoot, 'explicit-project-root');
|
|
3850
3839
|
// switchProject returns false for both "same project, no-op" and "no git repo".
|
|
3851
3840
|
// Only scan subdirectories when explicitRoot is not itself a git repo; otherwise
|
|
@@ -5503,8 +5492,12 @@ export async function createMemorixServer(
|
|
|
5503
5492
|
): Promise<boolean> => {
|
|
5504
5493
|
if (source === 'mcp-roots' && projectBinding.isExplicit()) return false;
|
|
5505
5494
|
const { detectProjectWithDiagnostics } = await import('./project/detector.js');
|
|
5495
|
+
if (isHomeDirectory(newCwd)) {
|
|
5496
|
+
console.error(`[memorix] Refusing to bind $HOME as a project root: ${newCwd}`);
|
|
5497
|
+
return false;
|
|
5498
|
+
}
|
|
5506
5499
|
const result = detectProjectWithDiagnostics(newCwd);
|
|
5507
|
-
if (!result.project) {
|
|
5500
|
+
if (!result.project || isHomeDirectory(result.project.rootPath)) {
|
|
5508
5501
|
if (result.failure) {
|
|
5509
5502
|
console.error(`[memorix] Project detection failed for "${newCwd}": [${result.failure.reason}] ${result.failure.detail}`);
|
|
5510
5503
|
}
|
|
@@ -5523,6 +5516,7 @@ export async function createMemorixServer(
|
|
|
5523
5516
|
else if (source === 'mcp-roots') projectBinding.bindFromRoots(newDetected.rootPath);
|
|
5524
5517
|
else projectBinding.bindStartup(newDetected.rootPath);
|
|
5525
5518
|
projectBinding.recordResolvedProject(project.id, project.rootPath);
|
|
5519
|
+
writeLastProjectRoot(project.rootPath);
|
|
5526
5520
|
return false; // same project, no-op
|
|
5527
5521
|
}
|
|
5528
5522
|
|
|
@@ -5580,7 +5574,12 @@ export async function createMemorixServer(
|
|
|
5580
5574
|
}
|
|
5581
5575
|
} catch { /* best-effort - coordination features degrade gracefully */ }
|
|
5582
5576
|
|
|
5583
|
-
|
|
5577
|
+
writeLastProjectRoot(project.rootPath);
|
|
5578
|
+
runtimeHydratedForDir = null;
|
|
5579
|
+
projectRuntimeInitPromise = null;
|
|
5580
|
+
void ensureProjectRuntimeInitialized().catch((err) => {
|
|
5581
|
+
console.error(`[memorix] Background project runtime init failed: ${err instanceof Error ? err.message : err}`);
|
|
5582
|
+
});
|
|
5584
5583
|
try {
|
|
5585
5584
|
await startProjectMaintenanceWorker();
|
|
5586
5585
|
} catch {
|
package/src/store/obs-store.ts
CHANGED
|
@@ -59,7 +59,7 @@ export interface ObservationStore {
|
|
|
59
59
|
/** Load one project's observations without scanning the shared flat store. */
|
|
60
60
|
loadByProject(
|
|
61
61
|
projectId: string,
|
|
62
|
-
options?: { status?: string; limit?: number; offset?: number; afterId?: number },
|
|
62
|
+
options?: { status?: string; limit?: number; offset?: number; afterId?: number; newestFirst?: boolean },
|
|
63
63
|
): Promise<Observation[]>;
|
|
64
64
|
|
|
65
65
|
/** Count one project's observations without materializing the rows. */
|
|
@@ -231,7 +231,7 @@ export class DegradedBackend implements ObservationStore {
|
|
|
231
231
|
return [];
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
async loadByProject(_projectId: string, _options?: { status?: string; limit?: number; offset?: number; afterId?: number }): Promise<Observation[]> {
|
|
234
|
+
async loadByProject(_projectId: string, _options?: { status?: string; limit?: number; offset?: number; afterId?: number; newestFirst?: boolean }): Promise<Observation[]> {
|
|
235
235
|
return [];
|
|
236
236
|
}
|
|
237
237
|
|
package/src/store/sqlite-db.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import { createDatabase, loadSqlite } from './bun-sqlite-compat.js';
|
|
19
|
+
import { assertNotHomeDataDir } from '../project/launch-root.js';
|
|
19
20
|
|
|
20
21
|
// Dynamic require for SQLite (better-sqlite3, node:sqlite, or bun:sqlite)
|
|
21
22
|
let BetterSqlite3: any;
|
|
@@ -998,6 +999,7 @@ const _dbCache = new Map<string, any>();
|
|
|
998
999
|
*/
|
|
999
1000
|
export function getDatabase(dataDir: string): any {
|
|
1000
1001
|
const normalized = path.resolve(dataDir);
|
|
1002
|
+
assertNotHomeDataDir(normalized);
|
|
1001
1003
|
const existing = _dbCache.get(normalized);
|
|
1002
1004
|
if (existing) return existing;
|
|
1003
1005
|
|
|
@@ -122,6 +122,8 @@ export class SqliteBackend implements ObservationStore {
|
|
|
122
122
|
private stmtSelectByProjectStatus: any = null;
|
|
123
123
|
private stmtSelectByProjectAfterId: any = null;
|
|
124
124
|
private stmtSelectByProjectStatusAfterId: any = null;
|
|
125
|
+
private stmtSelectByProjectNewest: any = null;
|
|
126
|
+
private stmtSelectByProjectStatusNewest: any = null;
|
|
125
127
|
private stmtCountByProject: any = null;
|
|
126
128
|
private stmtCountByProjectStatus: any = null;
|
|
127
129
|
private stmtSelectByTopicKey: any = null;
|
|
@@ -175,6 +177,14 @@ export class SqliteBackend implements ObservationStore {
|
|
|
175
177
|
SELECT * FROM observations WHERE projectId = ? AND status = ? AND id > ?
|
|
176
178
|
ORDER BY id ASC LIMIT ?
|
|
177
179
|
`);
|
|
180
|
+
this.stmtSelectByProjectNewest = this.db.prepare(`
|
|
181
|
+
SELECT * FROM observations WHERE projectId = ?
|
|
182
|
+
ORDER BY createdAt DESC, id DESC LIMIT ? OFFSET ?
|
|
183
|
+
`);
|
|
184
|
+
this.stmtSelectByProjectStatusNewest = this.db.prepare(`
|
|
185
|
+
SELECT * FROM observations WHERE projectId = ? AND status = ?
|
|
186
|
+
ORDER BY createdAt DESC, id DESC LIMIT ? OFFSET ?
|
|
187
|
+
`);
|
|
178
188
|
this.stmtCountByProject = this.db.prepare(
|
|
179
189
|
`SELECT COUNT(*) AS count FROM observations WHERE projectId = ?`,
|
|
180
190
|
);
|
|
@@ -288,15 +298,20 @@ export class SqliteBackend implements ObservationStore {
|
|
|
288
298
|
|
|
289
299
|
async loadByProject(
|
|
290
300
|
projectId: string,
|
|
291
|
-
options: { status?: string; limit?: number; offset?: number; afterId?: number } = {},
|
|
301
|
+
options: { status?: string; limit?: number; offset?: number; afterId?: number; newestFirst?: boolean } = {},
|
|
292
302
|
): Promise<Observation[]> {
|
|
293
303
|
const limit = options.limit == null ? -1 : Math.max(1, Math.floor(options.limit));
|
|
294
304
|
const offset = options.offset == null ? 0 : Math.max(0, Math.floor(options.offset));
|
|
295
305
|
const afterId = options.afterId == null ? null : Math.max(0, Math.floor(options.afterId));
|
|
306
|
+
const newestFirst = options.newestFirst === true && afterId === null;
|
|
296
307
|
const rows = afterId === null
|
|
297
|
-
? (
|
|
298
|
-
?
|
|
299
|
-
|
|
308
|
+
? (newestFirst
|
|
309
|
+
? (options.status
|
|
310
|
+
? this.stmtSelectByProjectStatusNewest.all(projectId, options.status, limit, offset)
|
|
311
|
+
: this.stmtSelectByProjectNewest.all(projectId, limit, offset))
|
|
312
|
+
: (options.status
|
|
313
|
+
? this.stmtSelectByProjectStatus.all(projectId, options.status, limit, offset)
|
|
314
|
+
: this.stmtSelectByProject.all(projectId, limit, offset)))
|
|
300
315
|
: (options.status
|
|
301
316
|
? this.stmtSelectByProjectStatusAfterId.all(projectId, options.status, afterId, limit)
|
|
302
317
|
: this.stmtSelectByProjectAfterId.all(projectId, afterId, limit));
|