feat: Implement tier 1 memory buffer

This commit is contained in:
2026-07-08 21:33:25 +05:30
parent 1aa00532ed
commit 74ffb456a7
12 changed files with 609 additions and 15 deletions

View File

@@ -5,5 +5,10 @@
"type": "module",
"exports": {
".": "./dist/index.js"
},
"dependencies": {
"@omnia/core": "workspace:*",
"@omnia/intent": "workspace:*",
"zod": "^4.4.3"
}
}

View File

@@ -0,0 +1,161 @@
import Database from "better-sqlite3";
import { Entity } from "@omnia/core";
import { Intent } from "@omnia/intent";
export interface BufferEntry {
id: string;
ownerId: string; // Whose subjective memory buffer this lives in
timestamp: string; // WorldClock.get().toISOString() at write time
locationId: string | null; // Actor's location when this happened
intent: Intent; // The actual dialogue/action intent, reused as-is
outcome?: {
// Present only for "action" intents processed by the Architect
isValid: boolean;
reason: string;
};
}
export function resolveAlias(viewer: Entity, targetId: string): string {
if (targetId === viewer.id) return "you";
return viewer.aliases.get(targetId) ?? "an unfamiliar figure";
}
export function serializeSubjectiveBufferEntry(
entry: BufferEntry,
viewer: Entity,
): string {
const dateObj = new Date(entry.timestamp);
// Ensure a deterministic timezone/format for testing and model inputs:
const timeStr = dateObj.toLocaleTimeString("en-US", {
hour12: true,
timeZone: "UTC",
});
const actorAlias = resolveAlias(viewer, entry.intent.actorId);
const targetAliases = entry.intent.targetIds.map((tid) =>
resolveAlias(viewer, tid),
);
let details: string;
if (entry.intent.type === "dialogue") {
details = `spoke to ${targetAliases.join(", ") || "someone"}: "${entry.intent.description}"`;
} else {
details = `${entry.intent.description}`;
if (entry.outcome) {
details += ` (Outcome: ${entry.outcome.isValid ? "Succeeded" : `Failed - ${entry.outcome.reason}`})`;
}
}
return `[${timeStr}] ${actorAlias} ${details}`;
}
export class BufferRepository {
constructor(private db: Database.Database) {
// Enable foreign keys for cascading deletes
this.db.exec("PRAGMA foreign_keys = ON;");
this.initializeSchema();
}
private initializeSchema(): void {
this.db.exec(`
CREATE TABLE IF NOT EXISTS buffer_entries (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
timestamp TEXT NOT NULL,
location_id TEXT,
intent_json TEXT NOT NULL,
outcome_json TEXT,
FOREIGN KEY (owner_id) REFERENCES objects(id) ON DELETE CASCADE
);
`);
}
save(entry: BufferEntry): void {
this.db
.prepare(
`
INSERT INTO buffer_entries (id, owner_id, timestamp, location_id, intent_json, outcome_json)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
owner_id = excluded.owner_id,
timestamp = excluded.timestamp,
location_id = excluded.location_id,
intent_json = excluded.intent_json,
outcome_json = excluded.outcome_json
`,
)
.run(
entry.id,
entry.ownerId,
entry.timestamp,
entry.locationId,
JSON.stringify(entry.intent),
entry.outcome ? JSON.stringify(entry.outcome) : null,
);
}
load(id: string): BufferEntry | null {
const row = this.db
.prepare(
`
SELECT id, owner_id, timestamp, location_id, intent_json, outcome_json
FROM buffer_entries WHERE id = ?
`,
)
.get(id) as
| {
id: string;
owner_id: string;
timestamp: string;
location_id: string | null;
intent_json: string;
outcome_json: string | null;
}
| undefined;
if (!row) return null;
return {
id: row.id,
ownerId: row.owner_id,
timestamp: row.timestamp,
locationId: row.location_id,
intent: JSON.parse(row.intent_json),
outcome: row.outcome_json ? JSON.parse(row.outcome_json) : undefined,
};
}
listForOwner(ownerId: string): BufferEntry[] {
const rows = this.db
.prepare(
`
SELECT id, owner_id, timestamp, location_id, intent_json, outcome_json
FROM buffer_entries WHERE owner_id = ?
ORDER BY timestamp ASC
`,
)
.all(ownerId) as {
id: string;
owner_id: string;
timestamp: string;
location_id: string | null;
intent_json: string;
outcome_json: string | null;
}[];
return rows.map((row) => ({
id: row.id,
ownerId: row.owner_id,
timestamp: row.timestamp,
locationId: row.location_id,
intent: JSON.parse(row.intent_json),
outcome: row.outcome_json ? JSON.parse(row.outcome_json) : undefined,
}));
}
delete(id: string): void {
this.db.prepare(`DELETE FROM buffer_entries WHERE id = ?`).run(id);
}
}

View File

@@ -1 +1 @@
export {};
export * from "./buffer.js";

View File

@@ -0,0 +1,200 @@
import { describe, test, expect } from "vitest";
import Database from "better-sqlite3";
import { Entity, SQLiteRepository } from "@omnia/core";
import { Intent } from "@omnia/intent";
import {
BufferEntry,
BufferRepository,
serializeSubjectiveBufferEntry,
resolveAlias,
} from "@omnia/memory";
describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
test("resolveAlias correctly handles self and fallbacks", () => {
const viewer = new Entity("alice");
viewer.aliases.set("bob", "the hooded figure");
expect(resolveAlias(viewer, "alice")).toBe("you");
expect(resolveAlias(viewer, "bob")).toBe("the hooded figure");
expect(resolveAlias(viewer, "charlie")).toBe("an unfamiliar figure");
});
test("serializes dialogue intent substituting target/actor aliases", () => {
const viewer = new Entity("alice");
viewer.aliases.set("bob", "the hooded figure");
viewer.aliases.set("charlie", "the bartender");
const entry: BufferEntry = {
id: "entry-1",
ownerId: "alice",
timestamp: "2026-07-07T12:00:00.000Z",
locationId: "room-1",
intent: {
type: "dialogue",
originalText: '"Hello there," Bob said to Charlie.',
description: "Bob greets Charlie",
actorId: "bob",
targetIds: ["charlie"],
},
};
const result = serializeSubjectiveBufferEntry(entry, viewer);
expect(result).toBe('[12:00:00 PM] the hooded figure spoke to the bartender: "Bob greets Charlie"');
});
test("serializes action intent with outcome details", () => {
const viewer = new Entity("alice");
viewer.aliases.set("bob", "the hooded figure");
const entry: BufferEntry = {
id: "entry-2",
ownerId: "alice",
timestamp: "2026-07-07T12:05:00.000Z",
locationId: "room-1",
intent: {
type: "action",
originalText: "Bob tried to break the latch.",
description: "Bob attempts to break the lock latch",
actorId: "bob",
targetIds: [],
},
outcome: {
isValid: false,
reason: "The lock is made of reinforced steel.",
},
};
const result = serializeSubjectiveBufferEntry(entry, viewer);
expect(result).toBe('[12:05:00 PM] the hooded figure Bob attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)');
});
test("serializes self-reference and unfamiliar actors", () => {
const viewer = new Entity("alice");
const entrySelf: BufferEntry = {
id: "entry-self",
ownerId: "alice",
timestamp: "2026-07-07T12:10:00.000Z",
locationId: "room-1",
intent: {
type: "action",
originalText: "I opened the window.",
description: "open the window",
actorId: "alice",
targetIds: [],
},
};
const resultSelf = serializeSubjectiveBufferEntry(entrySelf, viewer);
expect(resultSelf).toBe("[12:10:00 PM] you open the window");
const entryUnfamiliar: BufferEntry = {
id: "entry-unfamiliar",
ownerId: "alice",
timestamp: "2026-07-07T12:15:00.000Z",
locationId: "room-1",
intent: {
type: "action",
originalText: "Someone knocked.",
description: "knock on the door",
actorId: "stranger-1",
targetIds: [],
},
};
const resultUnfamiliar = serializeSubjectiveBufferEntry(entryUnfamiliar, viewer);
expect(resultUnfamiliar).toBe("[12:15:00 PM] an unfamiliar figure knock on the door");
});
});
describe("BufferRepository Persistence Tests (Tier 1)", () => {
test("saves, loads, lists, and deletes buffer entries in SQLite database", () => {
const db = new Database(":memory:");
// We need SQLiteRepository to initialize the objects table because buffer_entries depends on objects(id) via FK
const coreRepo = new SQLiteRepository(db);
const repo = new BufferRepository(db);
// Add owner entity to objects table
const alice = new Entity("alice");
coreRepo.saveEntity(alice);
const intent: Intent = {
type: "action",
originalText: "Alice picked up a stick.",
description: "Alice gathers a stick",
actorId: "alice",
targetIds: [],
};
const entry: BufferEntry = {
id: "buf-1",
ownerId: "alice",
timestamp: "2026-07-07T14:30:00.000Z",
locationId: "forest",
intent,
outcome: {
isValid: true,
reason: "There are many dry sticks on the ground.",
},
};
// Save
repo.save(entry);
// Load
const loaded = repo.load("buf-1");
expect(loaded).not.toBeNull();
expect(loaded!.id).toBe("buf-1");
expect(loaded!.ownerId).toBe("alice");
expect(loaded!.timestamp).toBe("2026-07-07T14:30:00.000Z");
expect(loaded!.locationId).toBe("forest");
expect(loaded!.intent).toEqual(intent);
expect(loaded!.outcome).toEqual(entry.outcome);
// List
const list = repo.listForOwner("alice");
expect(list).toHaveLength(1);
expect(list[0].id).toBe("buf-1");
// Delete
repo.delete("buf-1");
expect(repo.load("buf-1")).toBeNull();
db.close();
});
test("cascades delete of buffer entries when owner entity is deleted", () => {
const db = new Database(":memory:");
const coreRepo = new SQLiteRepository(db);
const repo = new BufferRepository(db);
const alice = new Entity("alice");
coreRepo.saveEntity(alice);
const entry: BufferEntry = {
id: "buf-1",
ownerId: "alice",
timestamp: "2026-07-07T14:30:00.000Z",
locationId: "forest",
intent: {
type: "action",
originalText: "Alice sneezed.",
description: "Alice sneezes",
actorId: "alice",
targetIds: [],
},
};
repo.save(entry);
expect(repo.load("buf-1")).not.toBeNull();
// Delete owner entity from core repository
coreRepo.delete("alice");
// Verify buffer entry was cascade deleted
expect(repo.load("buf-1")).toBeNull();
db.close();
});
});

View File

@@ -5,5 +5,8 @@
"outDir": "dist"
},
"include": ["src"],
"references": []
"references": [
{ "path": "../core" },
{ "path": "../intent" }
]
}