Skip to content

Commit e736df2

Browse files
drewswiredinclaude
andcommitted
fix: hot memory writes directly to Redis, remove migration system
Hot memory MCP tools (write, recall, fieldnote, session-note) now use RESP over TCP to Redis instead of HTTP to the sidecar. Removes the migration system entirely — all global dir setup is now idempotent in ensureGlobalDir(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 751f2b4 commit e736df2

9 files changed

Lines changed: 432 additions & 468 deletions

File tree

.lore/harness/hooks/session-init.js

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,14 @@
1-
const path = require('path');
21
const fs = require('fs');
32
const { buildDynamicBanner, ensureStickyFiles } = require('../lib/banner');
43
const { debug } = require('../lib/debug');
54
const { logHookEvent } = require('../lib/hook-logger');
6-
const { getConfig } = require('../lib/config');
7-
const { getGlobalStructureVersion, getRequiredStructureVersion } = require('../lib/global');
8-
95
const root = process.cwd();
106
debug('session-init: root=%s', root);
117

128
// 1. Ensure sticky files (MEMORY.local.md, etc.) exist
139
ensureStickyFiles(root);
1410

15-
// 2. Check global directory version — warn if outdated (never auto-migrate)
16-
const migrationsDir = path.join(root, '.lore', 'harness', 'migrations');
17-
const requiredVersion = getRequiredStructureVersion(migrationsDir);
18-
const currentVersion = getGlobalStructureVersion();
19-
if (requiredVersion > 0 && currentVersion < requiredVersion) {
20-
fs.writeSync(1, [
21-
'\x1b[91m▆▆▆ [LORE-GLOBAL-VERSION-MISMATCH] ▆▆▆\x1b[0m',
22-
`~/.lore/ structure is v${currentVersion} — this harness requires v${requiredVersion}.`,
23-
'Run /lore update to migrate the global directory.',
24-
'Do NOT capture fieldnotes or modify the knowledge base until resolved.',
25-
'\x1b[91m▆▆▆ [LORE-GLOBAL-VERSION-MISMATCH-END] ▆▆▆\x1b[0m',
26-
'',
27-
].join('\n'));
28-
}
29-
30-
// 3. Build and print the dynamic session banner (Operator Profile + Session Memory)
11+
// 2. Build and print the dynamic session banner (Operator Profile + Session Memory)
3112
// Static content (Rules, Skills, Fieldnotes) is now handled via platform-native projections.
3213
async function run() {
3314
const banner = await buildDynamicBanner(root);

.lore/harness/lib/global.js

Lines changed: 111 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// Global directory (~/.lore/) lifecycle: versioning, migrations, sidecar config.
1+
// Global directory (~/.lore/) lifecycle: setup, sidecar config, Redis config.
2+
// All setup is idempotent — safe to run on every session.
23

34
const fs = require('fs');
45
const path = require('path');
@@ -7,87 +8,106 @@ const { debug } = require('./debug');
78
const { getGlobalPath } = require('./config');
89

910
const DEFAULT_SIDECAR_PORT = 9185;
11+
const DEFAULT_REDIS_PORT = 6379;
1012

11-
/**
12-
* Read globalStructureVersion from ~/.lore/config.json.
13-
* Returns 0 if the file or field is missing.
14-
*/
15-
function getGlobalStructureVersion() {
16-
try {
17-
const configPath = path.join(getGlobalPath(), 'config.json');
18-
if (!fs.existsSync(configPath)) return 0;
19-
const data = JSON.parse(fs.readFileSync(configPath, 'utf8'));
20-
return typeof data.globalStructureVersion === 'number' ? data.globalStructureVersion : 0;
21-
} catch (e) {
22-
debug('getGlobalStructureVersion: %s', e.message);
23-
return 0;
24-
}
25-
}
13+
const GLOBAL_DIRS = [
14+
'AGENTIC/skills',
15+
'AGENTIC/rules',
16+
'AGENTIC/agents',
17+
'knowledge-base/fieldnotes',
18+
'knowledge-base/runbooks',
19+
'knowledge-base/environment',
20+
'knowledge-base/work-items',
21+
'knowledge-base/drafts',
22+
'redis-data',
23+
];
2624

27-
/**
28-
* Return the highest version number from NNN-name.js migration files.
29-
* Returns 0 if the directory is missing or empty.
30-
*/
31-
function getRequiredStructureVersion(migrationsDir) {
32-
try {
33-
if (!fs.existsSync(migrationsDir)) return 0;
34-
const files = fs.readdirSync(migrationsDir).filter(f => /^\d{3}-.*\.js$/.test(f)).sort();
35-
if (files.length === 0) return 0;
36-
const last = files[files.length - 1];
37-
return parseInt(last.slice(0, 3), 10);
38-
} catch (e) {
39-
debug('getRequiredStructureVersion: %s', e.message);
40-
return 0;
41-
}
42-
}
25+
const OPERATOR_PROFILE_CONTENT = `# Operator Profile
26+
27+
<!-- Injected into every session as OPERATOR PROFILE context. -->
28+
<!-- This file is gitignored — it stays local to your machine. -->
29+
30+
## Identity
31+
32+
- **Name:**
33+
- **Role:**
34+
35+
## Preferences
36+
37+
Add any preferences, working style notes, or context that should be
38+
available to the agent in every session.
39+
`;
40+
41+
const COMPOSE_CONTENT = `name: lore
42+
services:
43+
lore-runtime:
44+
image: lorehq/lore-memory:latest
45+
ports:
46+
- '9185:8080'
47+
environment:
48+
- REDIS_URL=redis://lore-memory:6379
49+
- DOCS_SOURCE=/data/knowledge-base
50+
- LORE_TOKEN=\${LORE_TOKEN}
51+
volumes:
52+
- ./knowledge-base:/data/knowledge-base:ro
53+
- runtime_data:/runtime-data
54+
depends_on:
55+
- lore-memory
56+
restart: unless-stopped
57+
58+
lore-memory:
59+
image: redis/redis-stack-server:latest
60+
ports:
61+
- '6379:6379'
62+
volumes:
63+
- ./redis-data:/data
64+
restart: unless-stopped
65+
66+
volumes:
67+
runtime_data:
68+
`;
4369

4470
/**
45-
* Apply pending migrations in order, bumping globalStructureVersion after each.
46-
* Returns { ran: number, version: number }.
71+
* Ensure ~/.lore/ exists with the expected structure.
72+
* Fully idempotent — creates what's missing, never overwrites existing content.
4773
*/
48-
function runMigrations(migrationsDir) {
74+
function ensureGlobalDir() {
4975
const globalPath = getGlobalPath();
50-
const current = getGlobalStructureVersion();
51-
let ran = 0;
5276

53-
try {
54-
const files = fs.readdirSync(migrationsDir).filter(f => /^\d{3}-.*\.js$/.test(f)).sort();
55-
for (const file of files) {
56-
const version = parseInt(file.slice(0, 3), 10);
57-
if (version <= current) continue;
58-
59-
debug('running migration %s', file);
60-
const migration = require(path.resolve(migrationsDir, file));
61-
migration.up(globalPath);
62-
63-
// Bump version after each migration (crash-safe: partial runs resume correctly)
64-
const configPath = path.join(globalPath, 'config.json');
65-
let config = {};
66-
try {
67-
if (fs.existsSync(configPath)) {
68-
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
69-
}
70-
} catch { /* start fresh */ }
71-
config.globalStructureVersion = version;
72-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
73-
ran++;
74-
}
75-
} catch (e) {
76-
debug('runMigrations: %s', e.message);
77-
throw e;
77+
// Directories
78+
for (const dir of GLOBAL_DIRS) {
79+
fs.mkdirSync(path.join(globalPath, dir), { recursive: true });
7880
}
7981

80-
return { ran, version: getGlobalStructureVersion() };
81-
}
82+
// Operator profile — only if missing
83+
const profilePath = path.join(globalPath, 'knowledge-base', 'operator-profile.md');
84+
if (!fs.existsSync(profilePath)) {
85+
fs.writeFileSync(profilePath, OPERATOR_PROFILE_CONTENT);
86+
}
8287

83-
/**
84-
* Ensure ~/.lore/ exists and run pending migrations.
85-
* Returns { ran: number, version: number }.
86-
*/
87-
function ensureGlobalDir(migrationsDir) {
88-
const globalPath = getGlobalPath();
89-
fs.mkdirSync(globalPath, { recursive: true });
90-
return runMigrations(migrationsDir);
88+
// docker-compose.yml — create if missing, patch Redis port if existing
89+
const composePath = path.join(globalPath, 'docker-compose.yml');
90+
if (!fs.existsSync(composePath)) {
91+
fs.writeFileSync(composePath, COMPOSE_CONTENT);
92+
} else {
93+
let yml = fs.readFileSync(composePath, 'utf8');
94+
if (yml.includes('lore-memory:') && !yml.includes("'6379:6379'")) {
95+
yml = yml.replace(
96+
/( lore-memory:\n image: [^\n]+\n)/,
97+
'$1 ports:\n - \'6379:6379\'\n',
98+
);
99+
fs.writeFileSync(composePath, yml);
100+
}
101+
}
102+
103+
// .env with LORE_TOKEN — only if missing or no token
104+
const envPath = path.join(globalPath, '.env');
105+
let envContent = '';
106+
try { envContent = fs.readFileSync(envPath, 'utf8'); } catch { /* missing */ }
107+
if (!envContent.includes('LORE_TOKEN=')) {
108+
const token = crypto.randomBytes(32).toString('hex');
109+
fs.appendFileSync(envPath, `LORE_TOKEN=${token}\n`);
110+
}
91111
}
92112

93113
/**
@@ -106,6 +126,22 @@ function getSidecarPort() {
106126
}
107127
}
108128

129+
/**
130+
* Return the Redis port. Reads redisPort from ~/.lore/config.json,
131+
* falls back to 6379.
132+
*/
133+
function getRedisPort() {
134+
try {
135+
const configPath = path.join(getGlobalPath(), 'config.json');
136+
if (!fs.existsSync(configPath)) return DEFAULT_REDIS_PORT;
137+
const data = JSON.parse(fs.readFileSync(configPath, 'utf8'));
138+
return data.redisPort || DEFAULT_REDIS_PORT;
139+
} catch (e) {
140+
debug('getRedisPort: %s', e.message);
141+
return DEFAULT_REDIS_PORT;
142+
}
143+
}
144+
109145
/**
110146
* Read LORE_TOKEN from ~/.lore/.env. Returns null if missing.
111147
*/
@@ -134,6 +170,7 @@ function ensureGlobalToken() {
134170
}
135171

136172
module.exports = {
137-
getGlobalStructureVersion, getRequiredStructureVersion, runMigrations, ensureGlobalDir,
138-
getSidecarPort, getGlobalToken, ensureGlobalToken, DEFAULT_SIDECAR_PORT,
173+
ensureGlobalDir,
174+
getSidecarPort, getRedisPort, getGlobalToken, ensureGlobalToken,
175+
DEFAULT_SIDECAR_PORT, DEFAULT_REDIS_PORT,
139176
};

0 commit comments

Comments
 (0)