claude-memory-layer 1.0.7 → 1.0.8
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/.claude/settings.local.json +4 -1
- package/.history/package_20260201192048.json +47 -0
- package/dist/cli/index.js +569 -39
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +192 -5
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/session-end.js +262 -18
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +262 -18
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +262 -18
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +262 -18
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +404 -39
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +413 -46
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +269 -18
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/index.html +495 -15
- package/package.json +2 -1
- package/scripts/build.ts +3 -2
- package/src/cli/index.ts +226 -0
- package/src/core/db-wrapper.ts +8 -1
- package/src/core/event-store.ts +52 -3
- package/src/core/graduation-worker.ts +171 -0
- package/src/core/graduation.ts +15 -2
- package/src/core/index.ts +1 -0
- package/src/core/retriever.ts +18 -0
- package/src/server/api/citations.ts +7 -3
- package/src/server/api/events.ts +7 -3
- package/src/server/api/search.ts +7 -3
- package/src/server/api/sessions.ts +7 -3
- package/src/server/api/stats.ts +129 -12
- package/src/server/index.ts +18 -9
- package/src/services/memory-service.ts +107 -19
- package/src/ui/index.html +495 -15
package/src/cli/index.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { exec } from 'child_process';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
9
12
|
import {
|
|
10
13
|
getDefaultMemoryService,
|
|
11
14
|
getMemoryServiceForProject
|
|
@@ -13,6 +16,92 @@ import {
|
|
|
13
16
|
import { createSessionHistoryImporter } from '../services/session-history-importer.js';
|
|
14
17
|
import { startServer, stopServer, isServerRunning } from '../server/index.js';
|
|
15
18
|
|
|
19
|
+
// ============================================================
|
|
20
|
+
// Hook Installation Utilities
|
|
21
|
+
// ============================================================
|
|
22
|
+
|
|
23
|
+
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
24
|
+
|
|
25
|
+
interface ClaudeSettings {
|
|
26
|
+
hooks?: {
|
|
27
|
+
UserPromptSubmit?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
|
|
28
|
+
PostToolUse?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
|
|
29
|
+
SessionStart?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
|
|
30
|
+
Stop?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
|
|
31
|
+
};
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPluginPath(): string {
|
|
36
|
+
// Try to find the dist directory
|
|
37
|
+
const possiblePaths = [
|
|
38
|
+
path.join(__dirname, '..'), // When running from dist/cli
|
|
39
|
+
path.join(__dirname, '../..', 'dist'), // When running from src
|
|
40
|
+
path.join(process.cwd(), 'dist'), // Current working directory
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
for (const p of possiblePaths) {
|
|
44
|
+
const hooksPath = path.join(p, 'hooks', 'user-prompt-submit.js');
|
|
45
|
+
if (fs.existsSync(hooksPath)) {
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fallback to npm global installation path
|
|
51
|
+
return path.join(os.homedir(), '.npm-global', 'lib', 'node_modules', 'claude-memory-layer', 'dist');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadClaudeSettings(): ClaudeSettings {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
57
|
+
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8');
|
|
58
|
+
return JSON.parse(content);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Warning: Could not read existing settings:', error);
|
|
62
|
+
}
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function saveClaudeSettings(settings: ClaudeSettings): void {
|
|
67
|
+
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
68
|
+
if (!fs.existsSync(dir)) {
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Atomic write
|
|
73
|
+
const tempPath = CLAUDE_SETTINGS_PATH + '.tmp';
|
|
74
|
+
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
|
|
75
|
+
fs.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getHooksConfig(pluginPath: string): ClaudeSettings['hooks'] {
|
|
79
|
+
return {
|
|
80
|
+
UserPromptSubmit: [
|
|
81
|
+
{
|
|
82
|
+
matcher: '',
|
|
83
|
+
hooks: [
|
|
84
|
+
{
|
|
85
|
+
type: 'command',
|
|
86
|
+
command: `node ${path.join(pluginPath, 'hooks', 'user-prompt-submit.js')}`
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
PostToolUse: [
|
|
92
|
+
{
|
|
93
|
+
matcher: '',
|
|
94
|
+
hooks: [
|
|
95
|
+
{
|
|
96
|
+
type: 'command',
|
|
97
|
+
command: `node ${path.join(pluginPath, 'hooks', 'post-tool-use.js')}`
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
16
105
|
const program = new Command();
|
|
17
106
|
|
|
18
107
|
program
|
|
@@ -20,6 +109,143 @@ program
|
|
|
20
109
|
.description('Claude Code Memory Plugin CLI')
|
|
21
110
|
.version('1.0.0');
|
|
22
111
|
|
|
112
|
+
// ============================================================
|
|
113
|
+
// Install / Uninstall Commands
|
|
114
|
+
// ============================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Install command - register hooks with Claude Code
|
|
118
|
+
*/
|
|
119
|
+
program
|
|
120
|
+
.command('install')
|
|
121
|
+
.description('Install hooks into Claude Code settings')
|
|
122
|
+
.option('--path <path>', 'Custom plugin path (defaults to auto-detect)')
|
|
123
|
+
.action(async (options) => {
|
|
124
|
+
try {
|
|
125
|
+
const pluginPath = options.path || getPluginPath();
|
|
126
|
+
|
|
127
|
+
// Verify hooks exist
|
|
128
|
+
const userPromptHook = path.join(pluginPath, 'hooks', 'user-prompt-submit.js');
|
|
129
|
+
if (!fs.existsSync(userPromptHook)) {
|
|
130
|
+
console.error(`\n❌ Hook files not found at: ${pluginPath}`);
|
|
131
|
+
console.error(' Make sure you have built the plugin with "npm run build"');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Load existing settings
|
|
136
|
+
const settings = loadClaudeSettings();
|
|
137
|
+
|
|
138
|
+
// Add hooks (merge with existing)
|
|
139
|
+
const newHooks = getHooksConfig(pluginPath);
|
|
140
|
+
settings.hooks = {
|
|
141
|
+
...settings.hooks,
|
|
142
|
+
...newHooks
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Save settings
|
|
146
|
+
saveClaudeSettings(settings);
|
|
147
|
+
|
|
148
|
+
console.log('\n✅ Claude Memory Layer installed!\n');
|
|
149
|
+
console.log('Hooks registered:');
|
|
150
|
+
console.log(' - UserPromptSubmit: Memory retrieval on user input');
|
|
151
|
+
console.log(' - PostToolUse: Store tool observations\n');
|
|
152
|
+
console.log('Plugin path:', pluginPath);
|
|
153
|
+
console.log('\n⚠️ Restart Claude Code for changes to take effect.\n');
|
|
154
|
+
console.log('Commands:');
|
|
155
|
+
console.log(' claude-memory-layer dashboard - Open web dashboard');
|
|
156
|
+
console.log(' claude-memory-layer search - Search memories');
|
|
157
|
+
console.log(' claude-memory-layer stats - View statistics');
|
|
158
|
+
console.log(' claude-memory-layer uninstall - Remove hooks\n');
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Install failed:', error);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Uninstall command - remove hooks from Claude Code
|
|
167
|
+
*/
|
|
168
|
+
program
|
|
169
|
+
.command('uninstall')
|
|
170
|
+
.description('Remove hooks from Claude Code settings')
|
|
171
|
+
.action(async () => {
|
|
172
|
+
try {
|
|
173
|
+
// Load existing settings
|
|
174
|
+
const settings = loadClaudeSettings();
|
|
175
|
+
|
|
176
|
+
if (!settings.hooks) {
|
|
177
|
+
console.log('\n📋 No hooks installed.\n');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Remove our hooks
|
|
182
|
+
delete settings.hooks.UserPromptSubmit;
|
|
183
|
+
delete settings.hooks.PostToolUse;
|
|
184
|
+
|
|
185
|
+
// Clean up empty hooks object
|
|
186
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
187
|
+
delete settings.hooks;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Save settings
|
|
191
|
+
saveClaudeSettings(settings);
|
|
192
|
+
|
|
193
|
+
console.log('\n✅ Claude Memory Layer uninstalled!\n');
|
|
194
|
+
console.log('Hooks removed from Claude Code settings.');
|
|
195
|
+
console.log('Your memory data is preserved and can be accessed with:');
|
|
196
|
+
console.log(' claude-memory-layer dashboard\n');
|
|
197
|
+
console.log('⚠️ Restart Claude Code for changes to take effect.\n');
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Uninstall failed:', error);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Status command - check installation status
|
|
206
|
+
*/
|
|
207
|
+
program
|
|
208
|
+
.command('status')
|
|
209
|
+
.description('Check plugin installation status')
|
|
210
|
+
.action(async () => {
|
|
211
|
+
try {
|
|
212
|
+
const settings = loadClaudeSettings();
|
|
213
|
+
const pluginPath = getPluginPath();
|
|
214
|
+
|
|
215
|
+
console.log('\n🧠 Claude Memory Layer Status\n');
|
|
216
|
+
|
|
217
|
+
// Check hooks
|
|
218
|
+
const hasUserPromptHook = settings.hooks?.UserPromptSubmit?.some(h =>
|
|
219
|
+
h.hooks?.some(hook => hook.command?.includes('user-prompt-submit'))
|
|
220
|
+
);
|
|
221
|
+
const hasPostToolHook = settings.hooks?.PostToolUse?.some(h =>
|
|
222
|
+
h.hooks?.some(hook => hook.command?.includes('post-tool-use'))
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
console.log('Hooks:');
|
|
226
|
+
console.log(` UserPromptSubmit: ${hasUserPromptHook ? '✅ Installed' : '❌ Not installed'}`);
|
|
227
|
+
console.log(` PostToolUse: ${hasPostToolHook ? '✅ Installed' : '❌ Not installed'}`);
|
|
228
|
+
|
|
229
|
+
// Check plugin files
|
|
230
|
+
const hooksExist = fs.existsSync(path.join(pluginPath, 'hooks', 'user-prompt-submit.js'));
|
|
231
|
+
console.log(`\nPlugin files: ${hooksExist ? '✅ Found' : '❌ Not found'}`);
|
|
232
|
+
console.log(` Path: ${pluginPath}`);
|
|
233
|
+
|
|
234
|
+
// Check dashboard
|
|
235
|
+
const dashboardRunning = await isServerRunning(37777);
|
|
236
|
+
console.log(`\nDashboard: ${dashboardRunning ? '✅ Running at http://localhost:37777' : '⏹️ Not running'}`);
|
|
237
|
+
|
|
238
|
+
if (!hasUserPromptHook || !hasPostToolHook) {
|
|
239
|
+
console.log('\n💡 Run "claude-memory-layer install" to set up hooks.\n');
|
|
240
|
+
} else {
|
|
241
|
+
console.log('\n✅ Plugin is fully installed and configured.\n');
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Status check failed:', error);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
23
249
|
/**
|
|
24
250
|
* Search command
|
|
25
251
|
*/
|
package/src/core/db-wrapper.ts
CHANGED
|
@@ -37,10 +37,17 @@ export function toDate(value: unknown): Date {
|
|
|
37
37
|
return new Date(String(value));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export interface DatabaseOptions {
|
|
41
|
+
readOnly?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
/**
|
|
41
45
|
* Creates a new DuckDB database with Promise-based API
|
|
42
46
|
*/
|
|
43
|
-
export function createDatabase(path: string): Database {
|
|
47
|
+
export function createDatabase(path: string, options?: DatabaseOptions): Database {
|
|
48
|
+
if (options?.readOnly) {
|
|
49
|
+
return new duckdb.Database(path, { access_mode: 'READ_ONLY' });
|
|
50
|
+
}
|
|
44
51
|
return new duckdb.Database(path);
|
|
45
52
|
}
|
|
46
53
|
|
package/src/core/event-store.ts
CHANGED
|
@@ -12,14 +12,20 @@ import {
|
|
|
12
12
|
OutboxItem
|
|
13
13
|
} from './types.js';
|
|
14
14
|
import { makeCanonicalKey, makeDedupeKey } from './canonical-key.js';
|
|
15
|
-
import { createDatabase, dbRun, dbAll, dbClose, toDate, type Database } from './db-wrapper.js';
|
|
15
|
+
import { createDatabase, dbRun, dbAll, dbClose, toDate, type Database, type DatabaseOptions } from './db-wrapper.js';
|
|
16
|
+
|
|
17
|
+
export interface EventStoreOptions extends DatabaseOptions {
|
|
18
|
+
// Additional options can be added here
|
|
19
|
+
}
|
|
16
20
|
|
|
17
21
|
export class EventStore {
|
|
18
22
|
private db: Database;
|
|
19
23
|
private initialized = false;
|
|
24
|
+
private readonly readOnly: boolean;
|
|
20
25
|
|
|
21
|
-
constructor(private dbPath: string) {
|
|
22
|
-
this.
|
|
26
|
+
constructor(private dbPath: string, options?: EventStoreOptions) {
|
|
27
|
+
this.readOnly = options?.readOnly ?? false;
|
|
28
|
+
this.db = createDatabase(dbPath, { readOnly: this.readOnly });
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/**
|
|
@@ -28,6 +34,12 @@ export class EventStore {
|
|
|
28
34
|
async initialize(): Promise<void> {
|
|
29
35
|
if (this.initialized) return;
|
|
30
36
|
|
|
37
|
+
// In read-only mode, skip schema creation (tables already exist)
|
|
38
|
+
if (this.readOnly) {
|
|
39
|
+
this.initialized = true;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
// L0 EventStore: Single Source of Truth (immutable, append-only)
|
|
32
44
|
await dbRun(this.db, `
|
|
33
45
|
CREATE TABLE IF NOT EXISTS events (
|
|
@@ -611,6 +623,43 @@ export class EventStore {
|
|
|
611
623
|
return rows;
|
|
612
624
|
}
|
|
613
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Get events by memory level
|
|
628
|
+
*/
|
|
629
|
+
async getEventsByLevel(level: string, options?: { limit?: number; offset?: number }): Promise<MemoryEvent[]> {
|
|
630
|
+
await this.initialize();
|
|
631
|
+
|
|
632
|
+
const limit = options?.limit || 50;
|
|
633
|
+
const offset = options?.offset || 0;
|
|
634
|
+
|
|
635
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
636
|
+
this.db,
|
|
637
|
+
`SELECT e.* FROM events e
|
|
638
|
+
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
639
|
+
WHERE ml.level = ?
|
|
640
|
+
ORDER BY e.timestamp DESC
|
|
641
|
+
LIMIT ? OFFSET ?`,
|
|
642
|
+
[level, limit, offset]
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
return rows.map(row => this.rowToEvent(row));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Get memory level for a specific event
|
|
650
|
+
*/
|
|
651
|
+
async getEventLevel(eventId: string): Promise<string | null> {
|
|
652
|
+
await this.initialize();
|
|
653
|
+
|
|
654
|
+
const rows = await dbAll<{ level: string }>(
|
|
655
|
+
this.db,
|
|
656
|
+
`SELECT level FROM memory_levels WHERE event_id = ?`,
|
|
657
|
+
[eventId]
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
return rows.length > 0 ? rows[0].level : null;
|
|
661
|
+
}
|
|
662
|
+
|
|
614
663
|
// ============================================================
|
|
615
664
|
// Endless Mode Helper Methods
|
|
616
665
|
// ============================================================
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graduation Worker
|
|
3
|
+
* Periodically evaluates memory events for promotion to higher levels
|
|
4
|
+
* L0 → L1 → L2 → L3 → L4 based on access patterns and confidence
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MemoryLevel } from './types.js';
|
|
8
|
+
import { EventStore } from './event-store.js';
|
|
9
|
+
import { GraduationPipeline } from './graduation.js';
|
|
10
|
+
|
|
11
|
+
export interface GraduationWorkerConfig {
|
|
12
|
+
/** How often to run graduation evaluation (ms) */
|
|
13
|
+
evaluationIntervalMs: number;
|
|
14
|
+
/** Batch size for graduation evaluation */
|
|
15
|
+
batchSize: number;
|
|
16
|
+
/** Minimum time between evaluations of the same event (ms) */
|
|
17
|
+
cooldownMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CONFIG: GraduationWorkerConfig = {
|
|
21
|
+
evaluationIntervalMs: 300000, // 5 minutes
|
|
22
|
+
batchSize: 50,
|
|
23
|
+
cooldownMs: 3600000 // 1 hour cooldown between evaluations
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class GraduationWorker {
|
|
27
|
+
private running = false;
|
|
28
|
+
private timeout: NodeJS.Timeout | null = null;
|
|
29
|
+
private lastEvaluated: Map<string, number> = new Map();
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private eventStore: EventStore,
|
|
33
|
+
private graduation: GraduationPipeline,
|
|
34
|
+
private config: GraduationWorkerConfig = DEFAULT_CONFIG
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start the graduation worker
|
|
39
|
+
*/
|
|
40
|
+
start(): void {
|
|
41
|
+
if (this.running) return;
|
|
42
|
+
this.running = true;
|
|
43
|
+
this.scheduleNext();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stop the graduation worker
|
|
48
|
+
*/
|
|
49
|
+
stop(): void {
|
|
50
|
+
this.running = false;
|
|
51
|
+
if (this.timeout) {
|
|
52
|
+
clearTimeout(this.timeout);
|
|
53
|
+
this.timeout = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if currently running
|
|
59
|
+
*/
|
|
60
|
+
isRunning(): boolean {
|
|
61
|
+
return this.running;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Force a graduation evaluation run
|
|
66
|
+
*/
|
|
67
|
+
async forceRun(): Promise<GraduationRunResult> {
|
|
68
|
+
return await this.runGraduation();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Schedule the next graduation check
|
|
73
|
+
*/
|
|
74
|
+
private scheduleNext(): void {
|
|
75
|
+
if (!this.running) return;
|
|
76
|
+
|
|
77
|
+
this.timeout = setTimeout(
|
|
78
|
+
() => this.run(),
|
|
79
|
+
this.config.evaluationIntervalMs
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run graduation evaluation
|
|
85
|
+
*/
|
|
86
|
+
private async run(): Promise<void> {
|
|
87
|
+
if (!this.running) return;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await this.runGraduation();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Graduation error:', error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.scheduleNext();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Perform graduation evaluation across all levels
|
|
100
|
+
*/
|
|
101
|
+
private async runGraduation(): Promise<GraduationRunResult> {
|
|
102
|
+
const result: GraduationRunResult = {
|
|
103
|
+
evaluated: 0,
|
|
104
|
+
graduated: 0,
|
|
105
|
+
byLevel: {}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const levels: MemoryLevel[] = ['L0', 'L1', 'L2', 'L3'];
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
|
|
111
|
+
for (const level of levels) {
|
|
112
|
+
const events = await this.eventStore.getEventsByLevel(level, {
|
|
113
|
+
limit: this.config.batchSize
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let levelGraduated = 0;
|
|
117
|
+
|
|
118
|
+
for (const event of events) {
|
|
119
|
+
// Check cooldown
|
|
120
|
+
const lastEval = this.lastEvaluated.get(event.id);
|
|
121
|
+
if (lastEval && (now - lastEval) < this.config.cooldownMs) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
result.evaluated++;
|
|
126
|
+
this.lastEvaluated.set(event.id, now);
|
|
127
|
+
|
|
128
|
+
const gradResult = await this.graduation.evaluateGraduation(event.id, level);
|
|
129
|
+
|
|
130
|
+
if (gradResult.success) {
|
|
131
|
+
result.graduated++;
|
|
132
|
+
levelGraduated++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (levelGraduated > 0) {
|
|
137
|
+
result.byLevel[level] = levelGraduated;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Clean up old cooldown entries (keep last 1000)
|
|
142
|
+
if (this.lastEvaluated.size > 1000) {
|
|
143
|
+
const entries = Array.from(this.lastEvaluated.entries());
|
|
144
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
145
|
+
this.lastEvaluated = new Map(entries.slice(0, 1000));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface GraduationRunResult {
|
|
153
|
+
evaluated: number;
|
|
154
|
+
graduated: number;
|
|
155
|
+
byLevel: Record<string, number>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a Graduation Worker instance
|
|
160
|
+
*/
|
|
161
|
+
export function createGraduationWorker(
|
|
162
|
+
eventStore: EventStore,
|
|
163
|
+
graduation: GraduationPipeline,
|
|
164
|
+
config?: Partial<GraduationWorkerConfig>
|
|
165
|
+
): GraduationWorker {
|
|
166
|
+
return new GraduationWorker(
|
|
167
|
+
eventStore,
|
|
168
|
+
graduation,
|
|
169
|
+
{ ...DEFAULT_CONFIG, ...config }
|
|
170
|
+
);
|
|
171
|
+
}
|
package/src/core/graduation.ts
CHANGED
|
@@ -84,18 +84,31 @@ export class GraduationPipeline {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Track which sessions have accessed each event
|
|
88
|
+
private readonly sessionAccesses: Map<string, Set<string>> = new Map();
|
|
89
|
+
|
|
87
90
|
/**
|
|
88
91
|
* Record an access to an event (used for graduation scoring)
|
|
89
92
|
*/
|
|
90
93
|
recordAccess(eventId: string, fromSessionId: string, confidence: number = 1.0): void {
|
|
91
94
|
const existing = this.metrics.get(eventId);
|
|
92
95
|
|
|
96
|
+
// Track sessions that have accessed this event
|
|
97
|
+
if (!this.sessionAccesses.has(eventId)) {
|
|
98
|
+
this.sessionAccesses.set(eventId, new Set());
|
|
99
|
+
}
|
|
100
|
+
const sessions = this.sessionAccesses.get(eventId)!;
|
|
101
|
+
const isNewSession = !sessions.has(fromSessionId);
|
|
102
|
+
sessions.add(fromSessionId);
|
|
103
|
+
|
|
93
104
|
if (existing) {
|
|
94
105
|
existing.accessCount++;
|
|
95
106
|
existing.lastAccessed = new Date();
|
|
96
107
|
existing.confidence = Math.max(existing.confidence, confidence);
|
|
97
|
-
//
|
|
98
|
-
|
|
108
|
+
// Update cross-session references count
|
|
109
|
+
if (isNewSession && sessions.size > 1) {
|
|
110
|
+
existing.crossSessionRefs = sessions.size - 1;
|
|
111
|
+
}
|
|
99
112
|
} else {
|
|
100
113
|
this.metrics.set(eventId, {
|
|
101
114
|
eventId,
|
package/src/core/index.ts
CHANGED
package/src/core/retriever.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Embedder } from './embedder.js';
|
|
|
9
9
|
import { Matcher } from './matcher.js';
|
|
10
10
|
import { SharedStore } from './shared-store.js';
|
|
11
11
|
import { SharedVectorStore } from './shared-vector-store.js';
|
|
12
|
+
import { GraduationPipeline } from './graduation.js';
|
|
12
13
|
import type { MemoryEvent, MatchResult, Config, SharedTroubleshootingEntry } from './types.js';
|
|
13
14
|
|
|
14
15
|
export interface RetrievalOptions {
|
|
@@ -60,6 +61,7 @@ export class Retriever {
|
|
|
60
61
|
private readonly matcher: Matcher;
|
|
61
62
|
private sharedStore?: SharedStore;
|
|
62
63
|
private sharedVectorStore?: SharedVectorStore;
|
|
64
|
+
private graduation?: GraduationPipeline;
|
|
63
65
|
|
|
64
66
|
constructor(
|
|
65
67
|
eventStore: EventStore,
|
|
@@ -76,6 +78,13 @@ export class Retriever {
|
|
|
76
78
|
this.sharedVectorStore = sharedOptions?.sharedVectorStore;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Set graduation pipeline for access tracking
|
|
83
|
+
*/
|
|
84
|
+
setGraduationPipeline(graduation: GraduationPipeline): void {
|
|
85
|
+
this.graduation = graduation;
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
/**
|
|
80
89
|
* Set shared stores after construction
|
|
81
90
|
*/
|
|
@@ -237,6 +246,15 @@ export class Retriever {
|
|
|
237
246
|
const event = await this.eventStore.getEvent(result.eventId);
|
|
238
247
|
if (!event) continue;
|
|
239
248
|
|
|
249
|
+
// Record access for graduation scoring
|
|
250
|
+
if (this.graduation) {
|
|
251
|
+
this.graduation.recordAccess(
|
|
252
|
+
event.id,
|
|
253
|
+
options.sessionId || 'unknown',
|
|
254
|
+
result.score
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
240
258
|
let sessionContext: string | undefined;
|
|
241
259
|
if (options.includeSessionContext) {
|
|
242
260
|
sessionContext = await this.getSessionContext(event.sessionId, event.id);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
|
-
import {
|
|
7
|
+
import { getReadOnlyMemoryService } from '../../services/memory-service.js';
|
|
8
8
|
import { generateCitationId, parseCitationId } from '../../core/citation-generator.js';
|
|
9
9
|
|
|
10
10
|
export const citationsRouter = new Hono();
|
|
@@ -15,9 +15,9 @@ citationsRouter.get('/:id', async (c) => {
|
|
|
15
15
|
|
|
16
16
|
// Support both formats: "a7Bc3x" or "mem:a7Bc3x"
|
|
17
17
|
const citationId = parseCitationId(id) || id;
|
|
18
|
+
const memoryService = getReadOnlyMemoryService();
|
|
18
19
|
|
|
19
20
|
try {
|
|
20
|
-
const memoryService = getDefaultMemoryService();
|
|
21
21
|
await memoryService.initialize();
|
|
22
22
|
|
|
23
23
|
// Search through recent events to find the one matching this citation ID
|
|
@@ -48,6 +48,8 @@ citationsRouter.get('/:id', async (c) => {
|
|
|
48
48
|
});
|
|
49
49
|
} catch (error) {
|
|
50
50
|
return c.json({ error: (error as Error).message }, 500);
|
|
51
|
+
} finally {
|
|
52
|
+
await memoryService.shutdown();
|
|
51
53
|
}
|
|
52
54
|
});
|
|
53
55
|
|
|
@@ -55,9 +57,9 @@ citationsRouter.get('/:id', async (c) => {
|
|
|
55
57
|
citationsRouter.get('/:id/related', async (c) => {
|
|
56
58
|
const { id } = c.req.param();
|
|
57
59
|
const citationId = parseCitationId(id) || id;
|
|
60
|
+
const memoryService = getReadOnlyMemoryService();
|
|
58
61
|
|
|
59
62
|
try {
|
|
60
|
-
const memoryService = getDefaultMemoryService();
|
|
61
63
|
await memoryService.initialize();
|
|
62
64
|
|
|
63
65
|
const recentEvents = await memoryService.getRecentEvents(10000);
|
|
@@ -97,5 +99,7 @@ citationsRouter.get('/:id/related', async (c) => {
|
|
|
97
99
|
});
|
|
98
100
|
} catch (error) {
|
|
99
101
|
return c.json({ error: (error as Error).message }, 500);
|
|
102
|
+
} finally {
|
|
103
|
+
await memoryService.shutdown();
|
|
100
104
|
}
|
|
101
105
|
});
|
package/src/server/api/events.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
|
-
import {
|
|
7
|
+
import { getReadOnlyMemoryService } from '../../services/memory-service.js';
|
|
8
8
|
|
|
9
9
|
export const eventsRouter = new Hono();
|
|
10
10
|
|
|
@@ -14,9 +14,9 @@ eventsRouter.get('/', async (c) => {
|
|
|
14
14
|
const eventType = c.req.query('type');
|
|
15
15
|
const limit = parseInt(c.req.query('limit') || '100', 10);
|
|
16
16
|
const offset = parseInt(c.req.query('offset') || '0', 10);
|
|
17
|
+
const memoryService = getReadOnlyMemoryService();
|
|
17
18
|
|
|
18
19
|
try {
|
|
19
|
-
const memoryService = getDefaultMemoryService();
|
|
20
20
|
await memoryService.initialize();
|
|
21
21
|
|
|
22
22
|
let events = await memoryService.getRecentEvents(limit + offset + 1000);
|
|
@@ -51,15 +51,17 @@ eventsRouter.get('/', async (c) => {
|
|
|
51
51
|
});
|
|
52
52
|
} catch (error) {
|
|
53
53
|
return c.json({ error: (error as Error).message }, 500);
|
|
54
|
+
} finally {
|
|
55
|
+
await memoryService.shutdown();
|
|
54
56
|
}
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
// GET /api/events/:id - Get event details
|
|
58
60
|
eventsRouter.get('/:id', async (c) => {
|
|
59
61
|
const { id } = c.req.param();
|
|
62
|
+
const memoryService = getReadOnlyMemoryService();
|
|
60
63
|
|
|
61
64
|
try {
|
|
62
|
-
const memoryService = getDefaultMemoryService();
|
|
63
65
|
await memoryService.initialize();
|
|
64
66
|
|
|
65
67
|
const recentEvents = await memoryService.getRecentEvents(10000);
|
|
@@ -97,5 +99,7 @@ eventsRouter.get('/:id', async (c) => {
|
|
|
97
99
|
});
|
|
98
100
|
} catch (error) {
|
|
99
101
|
return c.json({ error: (error as Error).message }, 500);
|
|
102
|
+
} finally {
|
|
103
|
+
await memoryService.shutdown();
|
|
100
104
|
}
|
|
101
105
|
});
|