deskpaw-memory/src/server.js

134 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
import { chmodSync, existsSync, lstatSync, mkdirSync, rmSync } from "node:fs";
import { createConnection, createServer } from "node:net";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { MemoryStore } from "./store.js";
import { ContinuityStore } from "./continuity.js";
import { Timeline } from "./timeline.js";
import { ChainStore } from "./chain-store.js";
import { CoreProjection } from "./core-projection.js";
const stateDir = resolve(process.env.DESKPAW_STATE_DIR ?? join(homedir(), ".deskpaw"));
const runtimeDir = join(stateDir, "runtime");
const memoryDir = join(stateDir, "memory");
const socketPath = process.env.DESKPAW_MEMORY_SOCKET ?? (process.platform === "win32" ? "\\\\.\\pipe\\deskpaw-memory" : join(runtimeDir, "memory.sock"));
const databasePath = resolve(process.env.DESKPAW_MEMORY_DB ?? join(memoryDir, "data", "memory.sqlite3"));
const bundlesDir = resolve(process.env.DESKPAW_MEMORY_BUNDLES ?? join(memoryDir, "bundles"));
process.umask(0o077);
mkdirSync(dirname(databasePath), { recursive: true, mode: 0o700 });
if (process.platform !== "win32") {
mkdirSync(dirname(socketPath), { recursive: true, mode: 0o700 });
if (existsSync(socketPath)) {
if (!lstatSync(socketPath).isSocket()) throw new Error("Refusing to replace non-socket memory path");
const active = await new Promise((resolve, reject) => {
const socket = createConnection(socketPath);
socket.once("connect", () => { socket.destroy(); resolve(true); });
socket.once("error", error => {
if (["ENOENT", "ECONNREFUSED"].includes(error.code)) resolve(false); else reject(error);
});
socket.setTimeout(1000, () => { socket.destroy(); reject(new Error("Cannot verify memory socket owner")); });
});
if (active) throw new Error("Memory socket already has an owner");
if (existsSync(socketPath)) rmSync(socketPath);
}
}
const store = new MemoryStore(databasePath, bundlesDir);
const continuity = new ContinuityStore(store.db);
const timeline = new Timeline(store.db);
const chain = new ChainStore(store.db, timeline);
const coreProjection = new CoreProjection(chain, join(stateDir, "super"));
coreProjection.refresh();
function call(method, params) {
if (method === "memory.chain.context") { coreProjection.refresh(); return chain.context(params); }
if (method === "memory.chain.tick") return chain.tick(params);
if (method === "memory.chain.status") return chain.status(params);
if (method === "memory.chain.core.bootstrap") return chain.bootstrapCore(params);
if (method === "memory.chain.core.update") { const result = chain.editCore(params); coreProjection.refresh(); return result; }
if (method === "memory.chain.find") return chain.graph.find(params);
if (method === "memory.chain.browse") return chain.graph.browse(params);
if (method === "memory.chain.read") return chain.graph.read(params);
if (method === "memory.chain.node") return chain.node(params);
if (method === "memory.chain.project_decision") return chain.projectDecision(params);
if (method === "memory.organize.request") return chain.request({ ...params, trigger: "explicit" });
if (method === "memory.organize.claim") return chain.claim(params);
if (method === "memory.organize.inspect") return chain.inspect(params);
if (method === "memory.organize.commit") { const receipt = chain.commit(params); coreProjection.refresh(); return receipt; }
if (method === "memory.organize.fail") return chain.fail(params);
if (method === "memory.events.ingest") return continuity.ingest(params);
if (method === "memory.observation.claim") return continuity.claim(params);
if (method === "memory.observation.commit") return continuity.commit(params);
if (method === "memory.observation.fail") return continuity.fail(params);
if (method === "memory.continuity.recall") return continuity.recall(params);
if (method === "memory.evidence.get") return continuity.evidence(params);
if (method === "timeline.append") return timeline.append({ ...params, at: params.at ?? Date.now() });
if (method === "timeline.query") return timeline.query(params);
if (method === "timeline.read" || method === "timeline.expand") return timeline.read(params);
if (method === "health") return { status: "ok", version: "0.1.0", ...store.status() };
if (method === "memory.recall") return store.recall(params);
if (method === "memory.maintenance.status") return store.maintenanceStatus(Number(params.now ?? Date.now()));
if (method === "memory.maintenance.plan") return store.maintenancePlan(params);
if (method === "memory.maintenance.commit") return store.commitMaintenance(params);
if (method === "memory.topic.get") return store.topicDetail(params);
if (method === "memory.skill.list") return store.listSkills(params);
if (method === "memory.skill.get") return store.skillDetail(params);
if (method === "memory.skill.set_status") return store.setSkillStatus(params);
if (method === "interaction.remember") return store.rememberInteractions(params);
if (method === "transition.assess") return store.assessTransition(params);
if (method === "context.prepare") return { ...store.prepareContext(params),
...(params.owner ? { continuity: continuity.recall({ owner: params.owner, query: params.objective,
subject_id: params.schedule_id ? `schedule:${params.schedule_id}` : undefined, limit: 4 }) } : {}) };
if (method === "task.complete") return store.completeTask(params);
if (method === "consolidation.record") return store.recordConsolidation(params);
if (method === "bundle.get") return store.bundle(String(params.scope ?? ""), String(params.object_id ?? ""));
if (method === "bundle.list") return store.listBundles(params.scope === undefined ? undefined : String(params.scope));
if (method === "bundle.document.put") return store.updateBundleDocument(params);
if (method === "task.list") return store.listTasks(Number(params.limit ?? 20));
throw new Error(`Unknown method: ${method}`);
}
const server = createServer((socket) => {
socket.setEncoding("utf8");
socket.setTimeout(5000, () => socket.destroy());
let input = "";
let handled = false;
socket.on("error", () => {});
socket.on("data", (chunk) => {
if (handled) return;
input += chunk;
if (Buffer.byteLength(input) > 1_000_000) { handled = true; socket.destroy(); return; }
const newline = input.indexOf("\n");
if (newline < 0) return;
handled = true;
const line = input.slice(0, newline);
let id = null;
try {
const request = JSON.parse(line);
id = request.id ?? null;
const result = call(String(request.method ?? ""), request.params ?? {});
socket.end(JSON.stringify({ id: request.id ?? null, ok: true, result }) + "\n");
} catch (error) {
socket.end(JSON.stringify({ id, ok: false, error: error instanceof Error ? error.message : String(error) }) + "\n");
}
});
});
server.maxConnections = 64;
server.listen(socketPath, () => {
if (process.platform !== "win32") chmodSync(socketPath, 0o600);
console.log(`DeskPaw memory ready at ${socketPath}`);
});
let stopping = false;
function shutdown() {
if (stopping) return;
stopping = true;
server.close(() => {
store.close();
if (process.platform !== "win32" && existsSync(socketPath)) rmSync(socketPath);
process.exit(0);
});
}
process.once("SIGTERM", shutdown);
process.once("SIGINT", shutdown);