feat: replace identitydb with supermemory

This commit is contained in:
2026-06-12 23:55:14 +09:00
parent e281b8a38f
commit 282c6f1348
17 changed files with 1207 additions and 1071 deletions

View File

@@ -1,4 +1,5 @@
DB_PATH=./brainbox.db
SUPERMEMORY_API_KEY=
BRAINDB_PATH=./braindb.json
OPENROUTER_API_KEY=

View File

@@ -9,9 +9,9 @@
"chalk": "^5.6.2",
"commander": "^15.0.0",
"dotenv": "^17.4.2",
"identitydb": "^0.5.2",
"ora": "^9.4.0",
"prettier": "^3.8.3",
"supermemory": "^4.24.12",
},
"devDependencies": {
"@types/bun": "latest",
@@ -31,8 +31,6 @@
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
@@ -43,90 +41,40 @@
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"identitydb": ["identitydb@0.5.2", "", { "dependencies": { "kysely": "^0.28.8", "mysql2": "^3.15.3", "pg": "^8.16.0" } }, "sha512-AkUmmAvpkgtIiHi7l1cTYFQDBYDMSi9HG94IAPWAq8Qa3wHbwaUaDCbPgxJM1ypN63SiW8vBllRTa28W3JXa3Q=="],
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="],
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"mysql2": ["mysql2@3.22.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ=="],
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="],
"pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="],
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
"pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
"pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
"stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="],
"string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"supermemory": ["supermemory@4.24.12", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-xAFextuqk4JuoW33jJaFGqT1oMppN2IgfWUrV18Fv3qAAZ6M1SR1tb+7EBq8vrEQIx4iY2MQh5p+qnfL6lI8Yw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],

View File

@@ -23,8 +23,8 @@
"chalk": "^5.6.2",
"commander": "^15.0.0",
"dotenv": "^17.4.2",
"identitydb": "^0.5.2",
"ora": "^9.4.0",
"prettier": "^3.8.3"
"prettier": "^3.8.3",
"supermemory": "^4.24.12"
}
}

View File

@@ -16,7 +16,7 @@ You will be given:
5. ALWAYS reply in real time. The user expects a person typing back, not a polished essay.
6. ALWAYS filter every response through the persona's voice, vocabulary, and emotional weather.
7. ALWAYS stay consistent with the date, time, and schedules you were given. Do not contradict them.
8. ALWAYS remember what you already know about the user. Do not ask for facts you already have; use the `searchIdentityDB` tool to look them up.
8. ALWAYS remember what you already know about the user. Do not ask for facts you already have; use the `searchMemory` tool to look them up.
### HOW TO REPLY
@@ -29,8 +29,8 @@ You will be given:
### WHEN TO USE TOOLS
- Call `addReplyMessage` for every bubble you want to send. When you have no more to say, end your turn (do not call any tool, return plain text).
- Call `searchIdentityDB` whenever the user references something you might already know but you cannot recall precisely. Use the natural-language query that would best match the relevant fact.
- Do not call `searchIdentityDB` for greetings, small talk, or anything you can answer from the persona's own knowledge of the user.
- Call `searchMemory` whenever the user references something you might already know but you cannot recall precisely. Use the natural-language query that would best match the relevant fact.
- Do not call `searchMemory` for greetings, small talk, or anything you can answer from the persona's own knowledge of the user.
### FINAL MANDATE

View File

@@ -1,17 +0,0 @@
import { llm } from "@/openrouter";
import { extractedFactSchema, type ExtractedFactResult } from "@/openrouter/schema";
import { LlmFactExtractor } from "identitydb";
export const factExtractor = new LlmFactExtractor({
model: {
async generateText({ instruction, input }) {
const result = await llm.call<ExtractedFactResult>(llm.models.identity, {
instruction,
message: input,
jsonSchemaName: "fact-extractor",
jsonSchema: extractedFactSchema,
});
return result.items;
},
},
});

View File

@@ -8,7 +8,7 @@ import {
test,
} from "bun:test";
import { randomUUID } from "node:crypto";
import { IdentityDB, type Space } from "identitydb";
import type { Space } from "./types";
const llmCalls: Array<{ model: unknown; options: any }> = [];
let customMonthlyDays: Array<{ day: number; summary: string }> | null = null;
@@ -24,11 +24,6 @@ let customAvailability: Array<{
status: string;
}> | null = null;
/**
* Queue of LLM responses for tool-calling flows (sendMessage). Each entry is
* returned in order. Shape matches OpenRouter's `ChatResult.choices[0]`
* reduced form: `{ content, tool_calls, finish_reason }`.
*/
type ToolCallResponse = {
id: string;
name: string;
@@ -136,6 +131,9 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
},
} as unknown as T;
}
if (typeof options.message === "string" || options.message === undefined) {
return "test-description" as unknown as T;
}
throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
});
@@ -150,14 +148,191 @@ mock.module("@/openrouter", () => ({
mock.module("@/config", () => ({
config: {
openrouterApiKey: "test-key",
dbPath: ":memory:",
supermemoryApiKey: "test-supermemory-key",
braindbPath: "/tmp/brainbox-test-braindb.json",
},
}));
interface StoredDoc {
id: string;
customId: string | null;
containerTag: string;
content: string;
summary: string | null;
metadata: Record<string, unknown> | null;
}
class MockSupermemory {
docs = new Map<string, StoredDoc>();
private nextId = 0;
documentsAddCalls = 0;
constructor(_options: { apiKey: string }) {}
documents = {
add: async (params: {
content: string;
containerTag: string;
customId?: string;
metadata?: Record<string, unknown>;
}) => {
this.documentsAddCalls += 1;
const id = `mock-${++this.nextId}`;
const stored: StoredDoc = {
id,
customId: params.customId ?? null,
containerTag: params.containerTag,
content: params.content,
summary: null,
metadata: params.metadata ?? null,
};
this.docs.set(id, stored);
return { id, status: "done" };
},
list: async (params: {
containerTags?: Array<string>;
limit?: number;
}) => {
const tags = params.containerTags ?? [];
const limit = params.limit ?? 200;
const all = Array.from(this.docs.values()).filter((d) =>
tags.length === 0 ? true : tags.includes(d.containerTag),
);
const memories = all.slice(0, limit).map((d) => ({
id: d.id,
customId: d.customId,
containerTag: d.containerTag,
summary: d.summary,
metadata: d.metadata as
| string
| number
| boolean
| Record<string, unknown>
| Array<unknown>
| null,
content: d.content,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
status: "done" as const,
type: "text" as const,
connectionId: null,
filepath: null,
title: null,
}));
return {
memories,
pagination: {
currentPage: 1,
totalItems: memories.length,
totalPages: 1,
limit,
},
};
},
get: async (id: string) => {
const d = this.docs.get(id);
if (!d) {
throw new Error(`MockSupermemory.documents.get: no such id ${id}`);
}
return {
id: d.id,
customId: d.customId,
containerTag: d.containerTag,
content: d.content,
summary: d.summary,
metadata: d.metadata as
| string
| number
| boolean
| Record<string, unknown>
| Array<unknown>
| null,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
status: "done" as const,
type: "text" as const,
connectionId: null,
filepath: null,
title: null,
source: null,
ogImage: null,
raw: null,
spatialPoint: null,
taskType: "memory" as const,
url: null,
};
},
};
search = {
execute: async (params: {
q: string;
containerTag?: string;
limit?: number;
onlyMatchingChunks?: boolean;
}) => {
const q = params.q.toLowerCase();
const limit = params.limit ?? 5;
const hits = Array.from(this.docs.values())
.filter(
(d) =>
(params.containerTag
? d.containerTag === params.containerTag
: true) && d.content.toLowerCase().includes(q),
)
.slice(0, limit)
.map((d, i) => ({
chunks: [
{
content: d.content,
isRelevant: true,
score: 1 - i * 0.1,
},
],
summary: d.summary,
score: 1 - i * 0.1,
documentId: d.id,
metadata: (d.metadata as Record<string, unknown>) ?? null,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
title: d.customId,
type: "text" as const,
}));
return {
results: hits,
total: hits.length,
timing: 0,
};
},
};
findByCustomId(customId: string): StoredDoc | undefined {
for (const d of this.docs.values()) {
if (d.customId === customId) return d;
}
return undefined;
}
reset(): void {
this.docs.clear();
this.nextId = 0;
this.documentsAddCalls = 0;
}
}
/**
* Replace the real `supermemory` SDK with our in-memory mock. The
* static factories `Brain.create` / `Brain.createDebug` / `Brain.load`
* all do `new Supermemory({ apiKey })` internally; this mock is what
* they pick up.
*/
mock.module("supermemory", () => ({
default: MockSupermemory,
}));
const { Brain } = await import("./index");
const { brainManager } = await import("./manager");
const { formatDateKey, nextDay, nextMonth } = await import("./schedule");
const { formatDateKey, nextMonth } = await import("./schedule");
type BrainItem = import("./manager").BrainItem;
beforeAll(async () => {
@@ -168,16 +343,10 @@ beforeAll(async () => {
afterAll(async () => {});
async function makeBrain(
embeddingProvider: unknown = NOOP_EMBEDDING_PROVIDER,
): Promise<InstanceType<typeof Brain>> {
const db = await IdentityDB.connect({
client: "sqlite",
filename: ":memory:",
});
await db.initialize();
async function makeBrain(): Promise<InstanceType<typeof Brain>> {
const db = new MockSupermemory({ apiKey: "test-supermemory-key" });
const spaceName = `test-space-${randomUUID()}`;
const space: Space = await db.upsertSpace({ name: spaceName });
const space: Space = { name: spaceName, description: "Test Brain space" };
const brainbase: BrainItem = {
brainId: randomUUID(),
spaceName,
@@ -185,7 +354,7 @@ async function makeBrain(
baseSystemPrompt:
"Test personality: night owl, introverted, studies at midnight.",
};
return new Brain(db, space, brainbase, false, embeddingProvider as never);
return new Brain(db as never, space, brainbase, false);
}
beforeEach(() => {
@@ -197,10 +366,11 @@ beforeEach(() => {
});
describe("Brain.createDailySchedule", () => {
test("S1: returns 48 slots in 30-min intervals and persists a fact", async () => {
test("S1: returns 48 slots in 30-min intervals and persists a document", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 5, 5);
const expectedTomorrow = nextDay(today);
const expectedTomorrow = (await import("./schedule")).nextDay(today);
const expectedKey = formatDateKey(expectedTomorrow);
const result = await brain.createDailySchedule(today, "focus on writing");
@@ -228,50 +398,39 @@ describe("Brain.createDailySchedule", () => {
expect(llmCall!.options.message).toContain("focus on writing");
expect(llmCall!.options.message).toContain("Test personality");
const facts = await brain.db.getTopicFacts(
`daily-schedule:${expectedKey}`,
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
expect(JSON.parse(facts[0]!.statement).items).toHaveLength(48);
const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
expect(stored).toBeDefined();
expect(stored!.containerTag).toBe(brain.space.name);
expect(JSON.parse(stored!.content).items).toHaveLength(48);
});
test("S4: month wrap (June 30 -> July 1)", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 5, 30);
const expectedKey = formatDateKey(new Date(2026, 6, 1));
await brain.createDailySchedule(today, "");
const facts = await brain.db.getTopicFacts(
`daily-schedule:${expectedKey}`,
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
expect(stored).toBeDefined();
});
test("S4b: year wrap (December 31 -> January 1 next year)", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 11, 31);
const expectedKey = "2027-01-01";
await brain.createDailySchedule(today, "");
const facts = await brain.db.getTopicFacts(
`daily-schedule:${expectedKey}`,
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
expect(stored).toBeDefined();
});
test("S6: consumes monthly summary for the target day when present", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
customMonthlyDays = Array.from({ length: 30 }, (_, i) => ({
day: i + 1,
@@ -282,13 +441,8 @@ describe("Brain.createDailySchedule", () => {
const todayForMonthly = new Date(2026, 4, 15);
await brain.createMonthlySchedule(todayForMonthly, "");
const monthlyFacts = await brain.db.getTopicFacts(
`monthly-schedule:2026-06`,
{
spaceName: brain.space.name,
},
);
expect(monthlyFacts).toHaveLength(1);
const monthlyStored = db.findByCustomId("monthly-schedule:2026-06");
expect(monthlyStored).toBeDefined();
llmCalls.length = 0;
customDailySlots = build48Slots();
@@ -304,11 +458,63 @@ describe("Brain.createDailySchedule", () => {
"UNIQUE_SUMMARY_FOR_DAY_10",
);
});
test("S9: injects 2-days-ago schedule as recent context when one exists", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const twoDaysAgoTarget = new Date(2026, 5, 7);
const twoDaysAgoTomorrow = (await import("./schedule")).nextDay(
twoDaysAgoTarget,
);
const twoDaysAgoKey = formatDateKey(twoDaysAgoTomorrow);
await brain.add({
customId: `daily-schedule:${twoDaysAgoKey}`,
content: JSON.stringify({
items: Array.from({ length: 48 }, (_, i) => ({
start: `${String(Math.floor(i / 2)).padStart(2, "0")}:${String((i % 2) * 30).padStart(2, "0")}`,
end: `${String(Math.floor((i + 1) / 2)).padStart(2, "0")}:${String(((i + 1) % 2) * 30).padStart(2, "0")}`,
activity: `prior-day-activity-${i}`,
notes: "",
})),
}),
metadata: { kind: "schedule", source: "createDailySchedule", date: twoDaysAgoKey },
});
llmCalls.length = 0;
const today = new Date(2026, 5, 9);
await brain.createDailySchedule(today, "");
const dailyLlmCall = llmCalls.find(
(c) => c.options.jsonSchemaName === "daily-schedule",
);
expect(dailyLlmCall).toBeDefined();
expect(dailyLlmCall!.options.message).toContain(
`Recent schedule (${twoDaysAgoKey}, 2 days ago):`,
);
expect(dailyLlmCall!.options.message).toContain("prior-day-activity-0");
});
test("S10: 2-days-ago context says 'no schedule on file' when prior day is missing", async () => {
const brain = await makeBrain();
const today = new Date(2026, 5, 9);
await brain.createDailySchedule(today, "");
const dailyLlmCall = llmCalls.find(
(c) => c.options.jsonSchemaName === "daily-schedule",
);
expect(dailyLlmCall).toBeDefined();
expect(dailyLlmCall!.options.message).toContain(
"(no schedule on file for 2 days ago)",
);
});
});
describe("Brain.createMonthlySchedule", () => {
test("S2: returns N day summaries (N = days in next month) and persists a fact", async () => {
test("S2: returns N day summaries (N = days in next month) and persists a document", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 0, 15);
const expected = nextMonth(today);
const expectedKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
@@ -329,20 +535,16 @@ describe("Brain.createMonthlySchedule", () => {
expect(llmCall!.options.message).toContain("study for GRE");
expect(llmCall!.options.message).toContain("Test personality");
const facts = await brain.db.getTopicFacts(
`monthly-schedule:${expectedKey}`,
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
expect(JSON.parse(facts[0]!.statement).items).toHaveLength(
const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`);
expect(stored).toBeDefined();
expect(JSON.parse(stored!.content).items).toHaveLength(
expected.daysInMonth,
);
});
test("S5: year wrap (December 15 -> January next year)", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 11, 15);
const expectedKey = "2027-01";
@@ -351,13 +553,8 @@ describe("Brain.createMonthlySchedule", () => {
expect(result).not.toBeNull();
expect(result!.items).toHaveLength(31);
const facts = await brain.db.getTopicFacts(
`monthly-schedule:${expectedKey}`,
{
spaceName: brain.space.name,
},
);
expect(facts).toHaveLength(1);
const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`);
expect(stored).toBeDefined();
});
});
@@ -366,25 +563,14 @@ describe("Brain.getTodayScheduledAvailability", () => {
const brain = await makeBrain();
const today = new Date(2026, 5, 10);
const todayKey = formatDateKey(today);
await brain.db.addFact({
spaceName: brain.space.name,
statement: JSON.stringify({ items: build48Slots() }),
summary: "test daily",
source: "test",
confidence: 1.0,
topics: [
{
name: `daily-schedule:${todayKey}`,
category: "temporal",
granularity: "concrete",
},
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
},
{ name: todayKey, category: "temporal", granularity: "concrete" },
],
await brain.add({
customId: `daily-schedule:${todayKey}`,
content: JSON.stringify({ items: build48Slots() }),
metadata: {
kind: "schedule",
source: "test",
date: todayKey,
},
});
const result = await brain.getTodayScheduledAvailability(today);
@@ -417,25 +603,14 @@ describe("Brain.removeScheduledAvailability", () => {
const brain = await makeBrain();
const today = new Date(2026, 5, 10);
const todayKey = formatDateKey(today);
await brain.db.addFact({
spaceName: brain.space.name,
statement: JSON.stringify({ items: build48Slots() }),
summary: "test daily",
source: "test",
confidence: 1.0,
topics: [
{
name: `daily-schedule:${todayKey}`,
category: "temporal",
granularity: "concrete",
},
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
},
{ name: todayKey, category: "temporal", granularity: "concrete" },
],
await brain.add({
customId: `daily-schedule:${todayKey}`,
content: JSON.stringify({ items: build48Slots() }),
metadata: {
kind: "schedule",
source: "test",
date: todayKey,
},
});
const r1 = await brain.getTodayScheduledAvailability(today);
@@ -463,25 +638,19 @@ describe("S8: regression on existing methods", () => {
});
describe("Brain.createDebug", () => {
test("D1: returns a Brain with debug=true, the supplied personality, and no disk file created", async () => {
const { existsSync } = await import("fs");
const { resolve } = await import("path");
const before = existsSync(resolve(process.cwd(), "brainbox.db"));
test("D1: returns a Brain with debug=true and the supplied personality under the brain:debug namespace", async () => {
const brain = await Brain.createDebug({ personality: "test-personality-Q" });
expect(brain).toBeInstanceOf(Brain);
expect(brain.debug).toBe(true);
expect(brain.brainbase.baseSystemPrompt).toBe("test-personality-Q");
expect(brain.brainbase.displayName).toBe("Debug Brain");
const after = existsSync(resolve(process.cwd(), "brainbox.db"));
expect(after).toBe(before);
expect(brain.space.name).toBe("brain:debug");
});
test("D2: createDailySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => {
test("D2: createDailySchedule on a debug brain returns a schedule and persists to brain:debug", async () => {
const brain = await Brain.createDebug({ personality: "p" });
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 5, 5);
const tomorrow = new Date(2026, 5, 6);
const tomorrowKey = formatDateKey(tomorrow);
@@ -490,14 +659,14 @@ describe("Brain.createDebug", () => {
expect(schedule).not.toBeNull();
expect(schedule!.items).toHaveLength(48);
const facts = await brain.db.getTopicFacts(`daily-schedule:${tomorrowKey}`, {
spaceName: brain.space.name,
});
expect(facts).toHaveLength(0);
const stored = db.findByCustomId(`daily-schedule:${tomorrowKey}`);
expect(stored).toBeDefined();
expect(stored!.containerTag).toBe("brain:debug");
});
test("D3: createMonthlySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => {
test("D3: createMonthlySchedule on a debug brain returns a schedule and persists to brain:debug", async () => {
const brain = await Brain.createDebug({ personality: "p" });
const db = brain.db as unknown as MockSupermemory;
const today = new Date(2026, 0, 15);
const expected = nextMonth(today);
const monthKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
@@ -506,42 +675,12 @@ describe("Brain.createDebug", () => {
expect(schedule).not.toBeNull();
expect(schedule!.items).toHaveLength(expected.daysInMonth);
const facts = await brain.db.getTopicFacts(
`monthly-schedule:${monthKey}`,
{ spaceName: brain.space.name },
);
expect(facts).toHaveLength(0);
const stored = db.findByCustomId(`monthly-schedule:${monthKey}`);
expect(stored).toBeDefined();
expect(stored!.containerTag).toBe("brain:debug");
});
});
const NOOP_EMBEDDING_PROVIDER = {
model: "test-embed",
dimensions: 4,
async embed(_input: string): Promise<number[]> {
return [0, 0, 0, 0];
},
async embedMany(inputs: string[]): Promise<number[][]> {
return inputs.map(() => [0, 0, 0, 0]);
},
};
const SCORING_EMBEDDING_PROVIDER = {
model: "test-embed-scoring",
dimensions: 4,
async embed(input: string): Promise<number[]> {
if (input.includes("coffee")) return [1, 0, 0, 0];
if (input.includes("pizza")) return [0, 1, 0, 0];
return [0, 0, 1, 0];
},
async embedMany(inputs: string[]): Promise<number[][]> {
return inputs.map((s) => {
if (s.includes("coffee")) return [1, 0, 0, 0];
if (s.includes("pizza")) return [0, 1, 0, 0];
return [0, 0, 1, 0];
});
},
};
describe("Brain.sendMessage — translateMessageHistory helper", () => {
test("SM1: translateMessageHistory produces the documented format with persona label and timestamps", async () => {
const { translateMessageHistory } = await import("./messageHistory");
@@ -600,7 +739,7 @@ describe("Brain.sendMessage — tool-calling flow", () => {
}>
).map((t) => t.function.name);
expect(toolNames).toContain("addReplyMessage");
expect(toolNames).toContain("searchIdentityDB");
expect(toolNames).toContain("searchMemory");
});
test("SM4: sendMessage accumulates addReplyMessage tool calls and returns them in order", async () => {
@@ -635,20 +774,13 @@ describe("Brain.sendMessage — tool-calling flow", () => {
expect(out).toEqual(["어.", "왜불러"]);
});
test("SM5: sendMessage feeds searchIdentityDB tool result back to the LLM", async () => {
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER);
const fact = await brain.db.addFact({
spaceName: brain.space.name,
statement: "사용자는 커피를 좋아한다",
summary: "user loves coffee",
source: "test",
confidence: 1.0,
topics: [
{ name: "사용자", category: "entity", granularity: "concrete" },
{ name: "커피", category: "concept", granularity: "abstract" },
],
test("SM5: sendMessage feeds searchMemory tool result back to the LLM", async () => {
const brain = await makeBrain();
await brain.add({
customId: "fact-coffee",
content: "사용자는 커피를 좋아한다",
metadata: { kind: "fact", source: "test" },
});
await brain.indexFactEmbeddingFor(fact);
chatResponses = [
{
@@ -656,7 +788,7 @@ describe("Brain.sendMessage — tool-calling flow", () => {
tool_calls: [
{
id: "call_s",
name: "searchIdentityDB",
name: "searchMemory",
arguments: JSON.stringify({ query: "커피" }),
},
],
@@ -732,8 +864,8 @@ describe("Brain.sendMessage — tool-calling flow", () => {
expect(userMsg!.content).toContain("하이");
});
test("SM7: createDailySchedule auto-indexes the new fact so it is searchable via the provider", async () => {
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER);
test("SM7: createDailySchedule persists a document reachable via brain.get", async () => {
const brain = await makeBrain();
const today = new Date(2026, 5, 5);
const tomorrow = new Date(2026, 5, 6);
const tomorrowKey = formatDateKey(tomorrow);
@@ -741,43 +873,20 @@ describe("Brain.sendMessage — tool-calling flow", () => {
customDailySlots = build48Slots();
await brain.createDailySchedule(today, "msg");
const hits = await brain.db.searchFacts({
spaceName: brain.space.name,
query: "slot-0",
provider: SCORING_EMBEDDING_PROVIDER as never,
limit: 5,
const stored = await brain.get(`daily-schedule:${tomorrowKey}`);
expect(stored).not.toBeNull();
expect(stored!.content).toContain("slot-0");
expect(stored!.metadata).toEqual({
kind: "schedule",
source: "createDailySchedule",
date: tomorrowKey,
});
expect(hits.length).toBeGreaterThan(0);
const matched = hits.find((h) =>
h.statement.includes(`"activity":"slot-0"`),
);
expect(matched).toBeDefined();
const topicFacts = await brain.db.getTopicFacts(
`daily-schedule:${tomorrowKey}`,
{ spaceName: brain.space.name },
);
expect(topicFacts).toHaveLength(1);
});
test("SM8: sendMessage no longer calls indexFactEmbeddings on every turn (uses per-fact init)", async () => {
const brain = await makeBrain(NOOP_EMBEDDING_PROVIDER);
let embedManyCalls = 0;
const trackingProvider = {
model: "track-embed",
dimensions: 4,
async embed(_input: string): Promise<number[]> {
return [0, 0, 0, 0];
},
async embedMany(inputs: string[]): Promise<number[][]> {
embedManyCalls += 1;
return inputs.map(() => [0, 0, 0, 0]);
},
};
Object.defineProperty(brain, "embeddingProvider", {
value: trackingProvider,
configurable: true,
});
test("SM8: sendMessage does not call brain.add (no documents added during chat)", async () => {
const brain = await makeBrain();
const db = brain.db as unknown as MockSupermemory;
const before = db.documentsAddCalls;
chatResponses = [
{
@@ -796,40 +905,19 @@ describe("Brain.sendMessage — tool-calling flow", () => {
[{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "hi" }],
[],
);
expect(embedManyCalls).toBe(0);
expect(db.documentsAddCalls - before).toBe(0);
});
test("SM9: initializeEmbeddings backfills missing embeddings for facts added out-of-band", async () => {
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER);
await brain.db.addFact({
spaceName: brain.space.name,
statement: "사용자는 피자를 좋아한다",
summary: "user loves pizza",
source: "test",
confidence: 1.0,
topics: [
{ name: "사용자", category: "entity", granularity: "concrete" },
{ name: "피자", category: "concept", granularity: "abstract" },
],
test("SM9: out-of-band add() facts are queryable via brain.search without backfill", async () => {
const brain = await makeBrain();
await brain.add({
customId: "fact-pizza",
content: "사용자는 피자를 좋아한다",
metadata: { kind: "fact", source: "test" },
});
let preInitHits = await brain.db.searchFacts({
spaceName: brain.space.name,
query: "피자",
provider: SCORING_EMBEDDING_PROVIDER as never,
limit: 5,
});
expect(preInitHits).toHaveLength(0);
await brain.initializeEmbeddings();
const postInitHits = await brain.db.searchFacts({
spaceName: brain.space.name,
query: "피자",
provider: SCORING_EMBEDDING_PROVIDER as never,
limit: 5,
});
expect(postInitHits.length).toBeGreaterThan(0);
expect(postInitHits[0]!.statement).toContain("피자");
const hits = await brain.search("피자", 5);
expect(hits.length).toBeGreaterThan(0);
expect(hits[0]!.content).toContain("피자");
});
});

View File

@@ -1,13 +1,7 @@
import { randomUUID } from "node:crypto";
import Supermemory from "supermemory";
import { config } from "@/config";
import {
IdentityDB,
type EmbeddingProvider,
type ExtractedFact,
type Space,
} from "identitydb";
import { llm } from "@/openrouter";
import { OpenRouterEmbeddingProvider } from "@/openrouter/embedding";
import { loadPrompt } from "@/openrouter/promptLoader";
import {
availabilitySchema,
@@ -15,11 +9,19 @@ import {
monthlyScheduleSchema,
type AvailabilityWindows,
type DailySchedule,
type DailySlot,
type MonthlySchedule,
} from "@/openrouter/schema";
import { logger } from "@/utils/logger";
import { factExtractor } from "./factExtractor";
import { BadRequestResponseError } from "@openrouter/sdk/models/errors";
import type {
ChatAssistantMessage,
ChatChoice,
ChatFunctionTool,
ChatMessages,
} from "@openrouter/sdk/models";
import { BrainDBManager, brainManager, type BrainItem } from "./manager";
import { MemoryStub } from "./stub";
import {
translateMessageHistory,
type MessageHistoryEntry,
@@ -31,13 +33,7 @@ import {
nextMonth,
pad2,
} from "./schedule";
import { BadRequestResponseError } from "@openrouter/sdk/models/errors";
import type {
ChatAssistantMessage,
ChatChoice,
ChatFunctionTool,
ChatMessages,
} from "@openrouter/sdk/models";
import type { FactInput, FactMetadata, SearchHit, Space } from "./types";
export interface DebugOptions {
personality: string;
@@ -47,169 +43,96 @@ export interface BrainCreateResult {
brain: Brain;
description: string;
baseSystemPrompt: string;
/**
* Raw facts as returned by `factExtractor.extract(description)`. Populated
* only when `Brain.create` is called with `debug: true`; in production
* (the default), facts are persisted via `db.ingestStatements` which does
* not surface the raw extractor output to the caller.
*/
extractedFacts?: ExtractedFact[];
}
export class Brain {
private availabilityCache: Map<string, AvailabilityWindows> = new Map();
private embeddingProvider: EmbeddingProvider;
constructor(
public db: IdentityDB,
public db: Supermemory | MemoryStub,
public space: Space,
public brainbase: BrainItem,
public debug: boolean = false,
embeddingProvider?: EmbeddingProvider,
) {
this.embeddingProvider =
embeddingProvider ?? new OpenRouterEmbeddingProvider();
) {}
// ---------------------------------------------------------------------------
// Memory primitives — thin wrappers over supermemory's `documents` API.
//
// containerTag = space.name
// customId = the stable lookup key (e.g. "daily-schedule:2026-06-10")
// content = the fact text or JSON-encoded schedule
// metadata = filterable bag: { kind, source, ... }
// ---------------------------------------------------------------------------
async add(input: FactInput): Promise<{ id: string }> {
const response = await this.db.documents.add({
content: input.content,
containerTag: this.space.name,
customId: input.customId,
metadata: input.metadata,
});
return { id: response.id };
}
async get(
customId: string,
): Promise<{ content: string; metadata: FactMetadata | null } | null> {
const listed = await this.db.documents.list({
containerTags: [this.space.name],
limit: 200,
});
const match = (listed.memories ?? []).find((m) => m.customId === customId);
if (!match) return null;
const full = await this.db.documents.get(match.id);
return {
content: full.content ?? "",
metadata: (full.metadata ?? null) as FactMetadata | null,
};
}
async list(): Promise<Array<{ customId: string | null; content: string }>> {
const listed = await this.db.documents.list({
containerTags: [this.space.name],
limit: 200,
});
return (listed.memories ?? []).map((d) => ({
customId: d.customId,
content: d.content ?? "",
}));
}
async search(query: string, limit = 5): Promise<SearchHit[]> {
const response = await this.db.search.execute({
q: query,
containerTag: this.space.name,
limit,
onlyMatchingChunks: true,
});
return (response.results ?? []).map((r) => {
const firstChunk = r.chunks?.[0];
return {
content: firstChunk?.content ?? "",
score: r.score,
};
});
}
// ---------------------------------------------------------------------------
// Domain methods
// ---------------------------------------------------------------------------
async createDailySchedule(
datetime: Date,
message: string,
): Promise<DailySchedule | null> {
try {
const target = nextDay(datetime);
const dateKey = formatDateKey(target);
const topicName = `daily-schedule:${dateKey}`;
const monthlySummary = await this.getMonthlySummaryForDay(target);
const history = await this.getHistoryFacts();
const instruction = await loadPrompt("DAILY_SCHEDULE");
const promptMessage = [
`Target date: ${dateKey} (${target.toLocaleDateString("en-US", { weekday: "long" })})`,
`Personality: ${this.brainbase.baseSystemPrompt}`,
monthlySummary
? `Monthly summary for this day: ${monthlySummary}`
: "(no monthly summary available for this date)",
`Recent history (facts):`,
history,
`User direction: ${message}`,
].join("\n\n");
const schedule = await llm.call<DailySchedule>(llm.models.identity, {
instruction,
message: promptMessage,
jsonSchemaName: "daily-schedule",
jsonSchema: dailyScheduleSchema,
});
if (!this.debug) {
const fact = await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Daily schedule for ${dateKey} (${schedule.items.length} slots)`,
source: "createDailySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: dateKey,
category: "temporal",
granularity: "concrete",
role: "date",
},
],
});
await this.indexFactEmbeddingFor(fact);
}
return schedule;
} catch (error) {
let reason =
error instanceof Error
? error.message + `(${error.name})`
: String(error);
if (error instanceof BadRequestResponseError)
reason = reason + `${error.body}`;
logger.error(`createDailySchedule failed: ${reason}`);
return null;
}
return await runCreateDailyScheduleSteps(this, datetime, message, noopRunner);
}
async createMonthlySchedule(
datetime: Date,
message: string,
): Promise<MonthlySchedule | null> {
try {
const next = nextMonth(datetime);
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
const topicName = `monthly-schedule:${monthKey}`;
const history = await this.getHistoryFacts();
const instruction = await loadPrompt("MONTHLY_SCHEDULE");
const promptMessage = [
`Target month: ${monthKey} (${next.daysInMonth} days)`,
`Personality: ${this.brainbase.baseSystemPrompt}`,
`Recent history (facts):`,
history,
`User direction: ${message}`,
].join("\n\n");
const schedule = await llm.call<MonthlySchedule>(llm.models.identity, {
instruction,
message: promptMessage,
jsonSchemaName: "monthly-schedule",
jsonSchema: monthlyScheduleSchema,
});
if (!this.debug) {
const fact = await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Monthly schedule for ${monthKey} (${schedule.items.length} days)`,
source: "createMonthlySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "monthly-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: monthKey,
category: "temporal",
granularity: "concrete",
role: "period",
},
],
});
await this.indexFactEmbeddingFor(fact);
}
return schedule;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.error(`createMonthlySchedule failed: ${reason}`);
return null;
}
return await runCreateMonthlyScheduleSteps(this, datetime, message, noopRunner);
}
async getTodayScheduledAvailability(
@@ -220,20 +143,10 @@ export class Brain {
const cached = this.availabilityCache.get(dateKey);
if (cached) return cached;
if (this.debug) {
logger.warn(
"getTodayScheduledAvailability requires a persisted daily schedule; debug brains have no DB. Use deriveAvailabilityFromSchedule(schedule) instead.",
);
return null;
}
const stored = await this.get(`daily-schedule:${dateKey}`);
if (!stored) return null;
const topicName = `daily-schedule:${dateKey}`;
const facts = await this.db.getTopicFacts(topicName, {
spaceName: this.space.name,
});
if (facts.length === 0) return null;
const dailySchedule = JSON.parse(facts[0]!.statement) as DailySchedule;
const dailySchedule = JSON.parse(stored.content) as DailySchedule;
const availability =
await this.deriveAvailabilityFromSchedule(dailySchedule);
@@ -246,6 +159,30 @@ export class Brain {
}
}
async getCurrentAndAdjacentSlots(now: Date): Promise<DailySlot[]> {
const dateKey = formatDateKey(now);
const stored = await this.get(`daily-schedule:${dateKey}`);
if (!stored) return [];
let schedule: DailySchedule;
try {
schedule = JSON.parse(stored.content) as DailySchedule;
} catch {
return [];
}
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const toMinutes = (hhmm: string): number => {
const [h = 0, m = 0] = hhmm.split(":").map((x) => parseInt(x, 10));
return h * 60 + m;
};
const index = schedule.items.findIndex(
(slot) =>
toMinutes(slot.start) <= currentMinutes &&
currentMinutes < toMinutes(slot.end),
);
if (index === -1) return [];
return schedule.items.slice(Math.max(0, index - 1), index + 2);
}
async deriveAvailabilityFromSchedule(
schedule: DailySchedule,
): Promise<AvailabilityWindows> {
@@ -273,45 +210,6 @@ export class Brain {
this.availabilityCache.clear();
}
/**
* Embeds a single fact in the embedding table. Called automatically by
* Brain methods that add facts (createDailySchedule, createMonthlySchedule,
* Brain.create). Callers who add facts via `db.addFact` directly should
* invoke this so the LLM can recall the fact via `searchIdentityDB`. A
* no-op in debug mode (where there is no persisted state).
*/
async indexFactEmbeddingFor(fact: { id: string }): Promise<void> {
if (this.debug) return;
try {
await this.db.indexFactEmbedding(fact.id, {
spaceName: this.space.name,
provider: this.embeddingProvider,
});
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.warn(`indexFactEmbeddingFor(${fact.id}) failed: ${reason}`);
}
}
/**
* Backfills embeddings for every fact in this brain's space. Intended
* for `Brain.create` and `Brain.load` — runs once at initialization so
* facts added by older code paths (or out-of-band) become searchable.
* No-op in debug mode and when the space has no facts.
*/
async initializeEmbeddings(): Promise<void> {
if (this.debug) return;
try {
await this.db.indexFactEmbeddings({
spaceName: this.space.name,
provider: this.embeddingProvider,
});
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.warn(`initializeEmbeddings failed: ${reason}`);
}
}
async sendMessage(
history: ReadonlyArray<MessageHistoryEntry>,
newMessages: ReadonlyArray<MessageHistoryEntry>,
@@ -394,7 +292,7 @@ export class Brain {
});
continue;
}
if (call.function.name === "searchIdentityDB") {
if (call.function.name === "searchMemory") {
const result = await this.executeSearchTool(call.function.arguments);
messages.push({
role: "tool",
@@ -415,7 +313,7 @@ export class Brain {
if (
!hasContent &&
toolCalls.every((c) => c.function.name === "searchIdentityDB")
toolCalls.every((c) => c.function.name === "searchMemory")
) {
continue;
}
@@ -433,14 +331,23 @@ export class Brain {
}
private async buildScheduleBlock(now: Date): Promise<string> {
const dateKey = formatDateKey(now);
const currentSlots = await this.getCurrentAndAdjacentSlots(now);
const currentBlock = currentSlots.length > 0
? `Currently (around ${now.toTimeString().slice(0, 5)}):\n${currentSlots
.map(
(s) =>
` ${s.start}-${s.end} ${s.activity}${s.notes ? ` (${s.notes})` : ""}`,
)
.join("\n")}`
: `Currently (${dateKey} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
const days: { label: string; date: Date }[] = [
{
label: "Yesterday",
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1),
},
{ label: "Today", date: now },
{
label: "Tomorrow",
{ label: "Tomorrow",
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
},
];
@@ -452,19 +359,16 @@ export class Brain {
`${label} (${key}): ${summary ?? "(no daily schedule on file)"}`,
);
}
return `Schedule context:\n${blocks.join("\n")}`;
return `Schedule context:\n${currentBlock}\n\n${blocks.join("\n")}`;
}
private async getDailyScheduleSummary(
dateKey: string,
): Promise<string | null> {
if (this.debug) return null;
try {
const facts = await this.db.getTopicFacts(`daily-schedule:${dateKey}`, {
spaceName: this.space.name,
});
if (facts.length === 0) return null;
const schedule = JSON.parse(facts[0]!.statement) as DailySchedule;
const stored = await this.get(`daily-schedule:${dateKey}`);
if (!stored) return null;
const schedule = JSON.parse(stored.content) as DailySchedule;
const first = schedule.items[0];
const last = schedule.items[schedule.items.length - 1];
if (!first || !last) return null;
@@ -481,15 +385,9 @@ export class Brain {
return JSON.stringify({ ok: false, error: "missing query" });
}
try {
const hits = await this.db.searchFacts({
spaceName: this.space.name,
query,
provider: this.embeddingProvider,
limit: 5,
});
const hits = await this.search(query, 5);
const compact = hits.map((hit) => ({
statement: hit.statement,
summary: hit.summary,
content: hit.content,
score: hit.score,
}));
return JSON.stringify({ ok: true, hits: compact });
@@ -499,17 +397,13 @@ export class Brain {
}
}
private async getMonthlySummaryForDay(target: Date): Promise<string | null> {
if (this.debug) return null;
async getMonthlySummaryForDay(target: Date): Promise<string | null> {
try {
const monthKey = formatMonthKey(target);
const topicName = `monthly-schedule:${monthKey}`;
const facts = await this.db.getTopicFacts(topicName, {
spaceName: this.space.name,
});
if (facts.length === 0) return null;
const stored = await this.get(`monthly-schedule:${monthKey}`);
if (!stored) return null;
const monthly = JSON.parse(facts[0]!.statement) as MonthlySchedule;
const monthly = JSON.parse(stored.content) as MonthlySchedule;
const day = target.getDate();
const entry = monthly.items.find((d) => d.day === day);
return entry?.summary ?? null;
@@ -518,21 +412,13 @@ export class Brain {
}
}
private async getHistoryFacts(): Promise<string> {
if (this.debug) return "";
async getHistoryFacts(): Promise<string> {
try {
const topics = await this.db.listTopics({
spaceName: this.space.name,
includeFacts: true,
});
const statements: string[] = [];
for (const topic of topics) {
const t = topic as { facts?: Array<{ statement: string }> };
if (t.facts) {
for (const f of t.facts) statements.push(f.statement);
}
}
return statements.slice(-30).join("\n");
const docs = await this.list();
return docs
.map((d) => d.content)
.slice(-30)
.join("\n");
} catch {
return "";
}
@@ -541,136 +427,301 @@ export class Brain {
static async create(
displayName: string,
seed: string,
options: {
dbPath?: string;
braindbPath?: string;
debug?: boolean;
embeddingProvider?: EmbeddingProvider;
} = {},
options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {},
): Promise<BrainCreateResult | null> {
const dbPath = options.dbPath ?? config.dbPath;
const manager = options.braindbPath
? new BrainDBManager(options.braindbPath)
: brainManager;
const embeddingProvider =
options.embeddingProvider ?? new OpenRouterEmbeddingProvider();
try {
const personaInitInstruction = await loadPrompt("PERSONA_INIT");
const description = await llm.call<string>(llm.models.identity, {
instruction: personaInitInstruction,
message: seed,
});
const personaSystemInstruction = await loadPrompt(
"PERSONA_BASE_SYSTEM_PROMPT",
);
const generatedBaseSystemPrompt = await llm.call<string>(
llm.models.identity,
{
instruction: personaSystemInstruction,
message: description,
},
);
const personaSystemFixed = await loadPrompt(
"PERSONA_BASE_SYSTEM_PROMPT_FIXED",
);
const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`;
const db = await IdentityDB.connect({
client: "sqlite",
filename: dbPath,
});
await db.initialize();
const brainId = randomUUID();
const spaceName = `brain:${brainId}`;
const space = await db.upsertSpace({
name: spaceName,
description: displayName,
});
let extractedFacts: ExtractedFact[] | undefined;
if (options.debug) {
extractedFacts = await factExtractor.extract(description);
for (const fact of extractedFacts) {
const created = await db.addFact({
spaceName,
statement: fact.statement ?? description,
summary: fact.summary,
source: fact.source,
confidence: fact.confidence,
topics: fact.topics,
metadata: fact.metadata,
});
await db.indexFactEmbedding(created.id, {
spaceName,
provider: embeddingProvider,
});
}
} else {
await db.ingestStatements(description, {
extractor: factExtractor,
embeddingProvider,
spaceName,
});
}
const brainbase: BrainItem = {
brainId,
spaceName,
displayName,
baseSystemPrompt,
};
await manager.saveBrain(brainId, brainbase);
const brain = new Brain(db, space, brainbase, false, embeddingProvider);
return { brain, description, baseSystemPrompt, extractedFacts };
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.error(`Failed to create brain "${displayName}": ${reason}`);
return null;
}
return await runCreateSteps(displayName, seed, options, noopRunner);
}
static async load(brainId: string): Promise<Brain | null> {
const brain = await brainManager.loadBrain(brainId);
if (!brain) return null;
const brainbase = await brainManager.loadBrain(brainId);
if (!brainbase) return null;
const db = await IdentityDB.connect({
client: "sqlite",
filename: config.dbPath,
});
const space = await db.getSpaceByName(brain.spaceName);
if (!space) return null;
const brainInstance = new Brain(db, space, brain);
await brainInstance.initializeEmbeddings();
return brainInstance;
const db = new Supermemory({ apiKey: config.supermemoryApiKey });
const space: Space = { name: brainbase.spaceName };
return new Brain(db, space, brainbase);
}
static async createDebug(options: DebugOptions): Promise<Brain> {
const db = await IdentityDB.connect({
client: "sqlite",
filename: ":memory:",
});
await db.initialize();
const space = await db.upsertSpace({
name: "debug",
description: "Debug Brain",
});
static async createDebug(
options: DebugOptions,
db?: Supermemory | MemoryStub,
): Promise<Brain> {
const client = db ?? new Supermemory({ apiKey: config.supermemoryApiKey });
const space: Space = { name: "brain:debug", description: "Debug Brain" };
const brainbase: BrainItem = {
brainId: "debug",
spaceName: "debug",
spaceName: space.name,
displayName: "Debug Brain",
baseSystemPrompt: options.personality,
};
return new Brain(db, space, brainbase, true);
return new Brain(client, space, brainbase, true);
}
}
export type ScheduleStep =
| { kind: "gather-context" }
| {
kind: "generate-schedule";
jsonSchemaName: string;
schedule: DailySchedule | MonthlySchedule;
}
| { kind: "persist-schedule"; customId: string; contentLength: number }
| { kind: "derive-availability"; availability: AvailabilityWindows };
export type ScheduleProgress = (step: ScheduleStep) => void;
const noScheduleProgress: ScheduleProgress = () => {};
export interface StepRunner {
start(label: string): void;
done(summary: string): void;
fail(reason: string): void;
}
const noopRunner: StepRunner = {
start: () => {},
done: () => {},
fail: () => {},
};
export async function runCreateDailyScheduleSteps(
brain: Brain,
datetime: Date,
message: string,
runner: StepRunner = noopRunner,
): Promise<DailySchedule | null> {
try {
runner.start("gathering context");
const target = nextDay(datetime);
const dateKey = formatDateKey(target);
const twoDaysAgo = new Date(target);
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const twoDaysAgoKey = formatDateKey(twoDaysAgo);
const [monthlySummary, history, twoDaysAgoStored] = await Promise.all([
brain.getMonthlySummaryForDay(target),
brain.getHistoryFacts(),
brain.get(`daily-schedule:${twoDaysAgoKey}`),
]);
let twoDaysAgoSchedule: DailySchedule | null = null;
if (twoDaysAgoStored) {
try {
twoDaysAgoSchedule = JSON.parse(twoDaysAgoStored.content) as DailySchedule;
} catch {
twoDaysAgoSchedule = null;
}
}
runner.done("");
runner.start("generating schedule (daily-schedule)");
const instruction = await loadPrompt("DAILY_SCHEDULE");
const promptMessage = [
`Target date: ${dateKey} (${target.toLocaleDateString("en-US", { weekday: "long" })})`,
`Personality: ${brain.brainbase.baseSystemPrompt}`,
monthlySummary
? `Monthly summary for this day: ${monthlySummary}`
: "(no monthly summary available for this date)",
`Recent schedule (${twoDaysAgoKey}, 2 days ago): ${
twoDaysAgoSchedule
? twoDaysAgoSchedule.items
.map((s) => `${s.start} ${s.activity}`)
.join(", ")
: "(no schedule on file for 2 days ago)"
}`,
`Recent history (facts):`,
history,
`User direction: ${message}`,
].join("\n\n");
const schedule = await llm.call<DailySchedule>(llm.models.identity, {
instruction,
message: promptMessage,
jsonSchemaName: "daily-schedule",
jsonSchema: dailyScheduleSchema,
});
runner.done(`${schedule.items.length} items`);
runner.start("persisting schedule");
await brain.add({
customId: `daily-schedule:${dateKey}`,
content: JSON.stringify(schedule),
metadata: {
kind: "schedule",
source: "createDailySchedule",
date: dateKey,
},
});
runner.done(`customId=daily-schedule:${dateKey}`);
return schedule;
} catch (error) {
let reason =
error instanceof Error
? error.message + `(${error.name})`
: String(error);
if (error instanceof BadRequestResponseError)
reason = reason + `${error.body}`;
logger.error(`createDailySchedule failed: ${reason}`);
runner.fail(reason);
return null;
}
}
export async function runCreateMonthlyScheduleSteps(
brain: Brain,
datetime: Date,
message: string,
runner: StepRunner = noopRunner,
): Promise<MonthlySchedule | null> {
try {
runner.start("gathering context");
const next = nextMonth(datetime);
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
const twoMonthsAgo = new Date(next.year, next.month - 2, 1);
const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad2(twoMonthsAgo.getMonth() + 1)}`;
const [history, twoMonthsAgoStored] = await Promise.all([
brain.getHistoryFacts(),
brain.get(`monthly-schedule:${twoMonthsAgoKey}`),
]);
let twoMonthsAgoSchedule: MonthlySchedule | null = null;
if (twoMonthsAgoStored) {
try {
twoMonthsAgoSchedule = JSON.parse(twoMonthsAgoStored.content) as MonthlySchedule;
} catch {
twoMonthsAgoSchedule = null;
}
}
runner.done("");
runner.start("generating schedule (monthly-schedule)");
const instruction = await loadPrompt("MONTHLY_SCHEDULE");
const promptMessage = [
`Target month: ${monthKey} (${next.daysInMonth} days)`,
`Personality: ${brain.brainbase.baseSystemPrompt}`,
`Recent schedule (${twoMonthsAgoKey}, 2 months ago): ${
twoMonthsAgoSchedule
? twoMonthsAgoSchedule.items
.map((s) => `Day ${s.day}: ${s.summary}`)
.join(", ")
: "(no schedule on file for 2 months ago)"
}`,
`Recent history (facts):`,
history,
`User direction: ${message}`,
].join("\n\n");
const schedule = await llm.call<MonthlySchedule>(llm.models.identity, {
instruction,
message: promptMessage,
jsonSchemaName: "monthly-schedule",
jsonSchema: monthlyScheduleSchema,
});
runner.done(`${schedule.items.length} items`);
runner.start("persisting schedule");
await brain.add({
customId: `monthly-schedule:${monthKey}`,
content: JSON.stringify(schedule),
metadata: {
kind: "schedule",
source: "createMonthlySchedule",
month: monthKey,
},
});
runner.done(`customId=monthly-schedule:${monthKey}`);
return schedule;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.error(`createMonthlySchedule failed: ${reason}`);
runner.fail(reason);
return null;
}
}
export async function runCreateSteps(
displayName: string,
seed: string,
options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {},
runner: StepRunner = noopRunner,
): Promise<BrainCreateResult | null> {
const manager = options.braindbPath
? new BrainDBManager(options.braindbPath)
: brainManager;
try {
runner.start("generating persona description (PERSONA_INIT)");
const personaInitInstruction = await loadPrompt("PERSONA_INIT");
const description = await llm.call<string>(llm.models.identity, {
instruction: personaInitInstruction,
message: seed,
});
runner.done(snippet80(description));
runner.start(
"generating base system prompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)",
);
const personaSystemInstruction = await loadPrompt(
"PERSONA_BASE_SYSTEM_PROMPT",
);
const generatedBaseSystemPrompt = await llm.call<string>(
llm.models.identity,
{
instruction: personaSystemInstruction,
message: description,
},
);
const personaSystemFixed = await loadPrompt(
"PERSONA_BASE_SYSTEM_PROMPT_FIXED",
);
const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`;
runner.done(snippet80(baseSystemPrompt));
const db =
options.db ?? new Supermemory({ apiKey: config.supermemoryApiKey });
const brainId = randomUUID();
const space: Space = {
name: `brain:${brainId}`,
description: displayName,
};
const brain = new Brain(db, space, {
brainId,
spaceName: space.name,
displayName,
baseSystemPrompt,
});
runner.start("persisting persona document");
await brain.add({
customId: "persona",
content: description,
metadata: { kind: "persona", source: "persona-init" },
});
runner.done(`customId=persona, contentLength=${description.length}`);
runner.start("saving braindb index");
const brainbase: BrainItem = {
brainId,
spaceName: space.name,
displayName,
baseSystemPrompt,
};
await manager.saveBrain(brainId, brainbase);
runner.done(`brainId=${brainId}`);
return { brain, description, baseSystemPrompt };
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.error(`Failed to create brain "${displayName}": ${reason}`);
runner.fail(reason);
return null;
}
}
function snippet80(text: string): string {
const flat = text.replace(/\s+/g, " ").trim();
return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
}
function formatDatetime(now: Date): string {
const pad = (n: number) => n.toString().padStart(2, "0");
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(
@@ -699,9 +750,9 @@ function buildSendMessageTools(): ChatFunctionTool[] {
{
type: "function",
function: {
name: "searchIdentityDB",
name: "searchMemory",
description:
"Semantic search over the long-term memory of facts about the persona and the user. Returns the most relevant stored statements for a natural-language query.",
"Semantic search over the long-term memory of facts about the persona and the user. Returns the most relevant stored content for a natural-language query.",
parameters: {
type: "object",
additionalProperties: false,

148
src/brain/stub.ts Normal file
View File

@@ -0,0 +1,148 @@
/**
* In-memory implementation of the supermemory SDK surface that the
* `Brain` class uses. Lets debug commands exercise the full Brain
* flow (including `Brain.add`, `Brain.get`, `Brain.search`, etc.)
* without any network calls or API key.
*
* Storage shape: a `Map<id, StoredDoc>` keyed by an internal id; lookups
* by `customId` are done by linear scan. `containerTag` is recorded at
* `add()` time and filtered on `list()`. `search.execute` does a
* case-insensitive substring match against `content`.
*
* This is NOT a complete supermemory clone — it only implements the
* methods Brain calls. Adding a new Brain method that needs a
* different SDK method will require extending this stub.
*/
interface StoredDoc {
id: string;
customId: string | null;
containerTag: string;
content: string;
summary: string | null;
metadata: Record<string, unknown> | null;
}
export class MemoryStub {
readonly docs = new Map<string, StoredDoc>();
private nextId = 0;
documents = {
add: async (params: {
content: string;
containerTag: string;
customId?: string;
metadata?: Record<string, unknown>;
}) => {
const id = `stub-${++this.nextId}`;
this.docs.set(id, {
id,
customId: params.customId ?? null,
containerTag: params.containerTag,
content: params.content,
summary: null,
metadata: params.metadata ?? null,
});
return { id, status: "done" };
},
list: async (params: { containerTags?: Array<string>; limit?: number }) => {
const tags = params.containerTags ?? [];
const limit = params.limit ?? 200;
const all = Array.from(this.docs.values()).filter((d) =>
tags.length === 0 ? true : tags.includes(d.containerTag),
);
const memories = all.slice(0, limit).map((d) => ({
id: d.id,
customId: d.customId,
containerTag: d.containerTag,
content: d.content,
summary: d.summary,
metadata: d.metadata,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
status: "done" as const,
type: "text" as const,
connectionId: null,
filepath: null,
title: null,
}));
return {
memories,
pagination: {
currentPage: 1,
totalItems: memories.length,
totalPages: 1,
limit,
},
};
},
get: async (id: string) => {
const d = this.docs.get(id);
if (!d) {
throw new Error(`MemoryStub.documents.get: no such id ${id}`);
}
return {
id: d.id,
customId: d.customId,
containerTag: d.containerTag,
content: d.content,
summary: d.summary,
metadata: d.metadata,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
status: "done" as const,
type: "text" as const,
connectionId: null,
filepath: null,
title: null,
source: null,
ogImage: null,
raw: null,
spatialPoint: null,
taskType: "memory" as const,
url: null,
};
},
};
search = {
execute: async (params: {
q: string;
containerTag?: string;
limit?: number;
onlyMatchingChunks?: boolean;
}) => {
const q = params.q.toLowerCase();
const limit = params.limit ?? 5;
const hits = Array.from(this.docs.values())
.filter(
(d) =>
(params.containerTag
? d.containerTag === params.containerTag
: true) && d.content.toLowerCase().includes(q),
)
.slice(0, limit)
.map((d, i) => ({
chunks: [
{
content: d.content,
isRelevant: true,
score: 1 - i * 0.1,
},
],
summary: d.summary,
score: 1 - i * 0.1,
documentId: d.id,
metadata: d.metadata as Record<string, unknown> | null,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
title: d.customId,
type: "text" as const,
}));
return {
results: hits,
total: hits.length,
timing: 0,
};
},
};
}

26
src/brain/types.ts Normal file
View File

@@ -0,0 +1,26 @@
/** A brain's logical namespace. Maps to supermemory's `containerTag`. */
export interface Space {
name: string;
description?: string;
}
/** Metadata bag stored alongside a document in supermemory. */
export type FactMetadata = Record<string, string | number | boolean | string[]>;
/**
* A fact/document to store in the brain's memory. Mirrors supermemory's
* `documents.add` parameter shape: `content` is the canonical text (or
* JSON-encoded schedule), `customId` is the stable lookup key, and
* `metadata` is the filterable bag.
*/
export interface FactInput {
customId: string;
content: string;
metadata?: FactMetadata;
}
/** A single semantic-search hit. */
export interface SearchHit {
content: string;
score: number;
}

View File

@@ -1,8 +1,5 @@
import { randomUUID } from "node:crypto";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { existsSync, readdirSync } from "fs";
import { unlink, writeFile } from "fs/promises";
import type { ExtractedFact } from "identitydb";
import { tmpdir } from "os";
interface RecordedCall {
@@ -19,36 +16,6 @@ const llmCalls: RecordedCall[] = [];
const PERSONA_DESCRIPTION = "A 34yo night-shift nurse, hides exhaustion behind sarcasm.";
const GENERATED_BASE_SYSTEM_PROMPT =
"You are Maren. You text in lowercase. You use '...' when tired.";
const EXTRACTED_FACTS: ExtractedFact[] = [
{
statement: "Maren is 34 years old.",
summary: "Maren is 34 years old.",
source: "persona-init",
confidence: 1.0,
topics: [
{
name: "maren-age",
category: "temporal",
granularity: "concrete",
role: "attribute",
},
],
},
{
statement: "Maren is a night-shift nurse.",
summary: "Maren is a night-shift nurse.",
source: "persona-init",
confidence: 1.0,
topics: [
{
name: "maren-occupation",
category: "entity",
granularity: "concrete",
role: "attribute",
},
],
},
];
const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
llmCalls.push({ model, options });
@@ -64,9 +31,6 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
) {
return GENERATED_BASE_SYSTEM_PROMPT as unknown as T;
}
if (options.jsonSchemaName === "fact-extractor") {
return { items: EXTRACTED_FACTS } as unknown as T;
}
throw new Error(
`unexpected LLM call: model=${model} instruction=${options.instruction?.slice(0, 80)}`,
);
@@ -82,26 +46,12 @@ mock.module("@/openrouter", () => ({
mock.module("@/config", () => ({
config: {
openrouterApiKey: "test-key",
dbPath: ":memory:",
supermemoryApiKey: "test-supermemory-key",
braindbPath: "/tmp/brainbox-test-braindb-debug-brain-IGNORED.json",
},
}));
mock.module("@/openrouter/embedding", () => ({
OpenRouterEmbeddingProvider: class {
model = "test-embed";
dimensions = 4;
async embed(_input: string): Promise<number[]> {
return [0, 0, 0, 0];
}
async embedMany(inputs: string[]): Promise<number[][]> {
return inputs.map(() => [0, 0, 0, 0]);
}
},
}));
const { runDebugBrainInit } = await import("./brain");
const { Brain: ProdBrain } = await import("@/brain");
beforeEach(() => {
llmCalls.length = 0;
@@ -109,22 +59,23 @@ beforeEach(() => {
});
afterEach(async () => {
const { unlink } = await import("fs/promises");
const tmpFiles = readdirSync(tmpdir()).filter((f) =>
f.startsWith("brainbox-debug-brain-"),
);
for (const f of tmpFiles) {
try {
const { unlink } = await import("fs/promises");
await unlink(`${tmpdir()}/${f}`);
} catch {}
}
});
describe("runDebugBrainInit", () => {
test("B1: returns ok result with full description, baseSystemPrompt, extractedFacts, and uses the supplied seed", async () => {
test("B1: returns ok result with full description, baseSystemPrompt, storedFacts, and uses the supplied seed", async () => {
const result = await runDebugBrainInit({
displayName: "Maren",
seed: "Maren, 34, night-shift nurse, hides exhaustion behind sarcasm",
noSupermemory: true,
});
expect(result.ok).toBe(true);
@@ -148,18 +99,22 @@ describe("runDebugBrainInit", () => {
),
);
expect(result.extractedFacts).toEqual(EXTRACTED_FACTS);
expect(result.storedFacts).toHaveLength(1);
expect(result.storedFacts[0]!.customId).toBe("persona");
expect(result.storedFacts[0]!.content).toContain(PERSONA_DESCRIPTION);
expect(typeof result.elapsedMs).toBe("number");
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
});
test("B2: invokes the LLM exactly 3 times — PERSONA_INIT, PERSONA_BASE_SYSTEM_PROMPT, fact-extractor", async () => {
test("B2: invokes the LLM exactly 2 times — PERSONA_INIT and PERSONA_BASE_SYSTEM_PROMPT", async () => {
await runDebugBrainInit({
displayName: "Test",
seed: "a seed",
noSupermemory: true,
});
expect(llmCalls.length).toBe(3);
expect(llmCalls.length).toBe(2);
const initCall = llmCalls[0]!;
expect(initCall.options.message).toBe("a seed");
@@ -168,32 +123,28 @@ describe("runDebugBrainInit", () => {
const systemCall = llmCalls[1]!;
expect(systemCall.options.jsonSchemaName).toBeUndefined();
expect(systemCall.options.message).toBe(PERSONA_DESCRIPTION);
const factCall = llmCalls[2]!;
expect(factCall.options.jsonSchemaName).toBe("fact-extractor");
expect(factCall.options.message).toBe(PERSONA_DESCRIPTION);
});
test("B3: writes no real on-disk state — no brainbox.db, no brainbox.json, no leftover temp braindb in /tmp", async () => {
test("B3: writes no real on-disk state — no leftover temp braindb in /tmp, no stray files in cwd", async () => {
const cwd = process.cwd();
const beforeDb = existsSync(`${cwd}/brainbox.db`);
const beforeJson = existsSync(`${cwd}/brainbox.json`);
const beforeCwdEntries = readdirSync(cwd);
const beforeTmp = readdirSync(tmpdir()).filter((f) =>
f.startsWith("brainbox-debug-brain-"),
);
await runDebugBrainInit({ displayName: "NoDiskCheck", seed: "x" });
await runDebugBrainInit({ displayName: "NoDiskCheck", seed: "x", noSupermemory: true });
const afterDb = existsSync(`${cwd}/brainbox.db`);
const afterJson = existsSync(`${cwd}/brainbox.json`);
const afterCwdEntries = readdirSync(cwd);
const afterTmp = readdirSync(tmpdir()).filter((f) =>
f.startsWith("brainbox-debug-brain-"),
);
expect(afterDb).toBe(beforeDb);
expect(afterJson).toBe(beforeJson);
expect(afterCwdEntries).toEqual(beforeCwdEntries);
expect(afterTmp).toHaveLength(0);
expect(existsSync(`${cwd}/brainbox.db`)).toBe(false);
expect(existsSync(`${cwd}/brainbox.json`)).toBe(false);
});
test("B4: when Brain.create returns null (e.g. LLM throws), result is {ok: false, error}", async () => {
@@ -204,6 +155,7 @@ describe("runDebugBrainInit", () => {
const result = await runDebugBrainInit({
displayName: "Doomed",
seed: "x",
noSupermemory: true,
});
expect(result.ok).toBe(false);
@@ -213,10 +165,11 @@ describe("runDebugBrainInit", () => {
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
});
test("B5: with no DB_PATH / BRAINDB_PATH env, runDebugBrainInit still works (no env dependency)", async () => {
test("B5: with no BRAINDB_PATH env, runDebugBrainInit still works (no env dependency)", async () => {
const result = await runDebugBrainInit({
displayName: "EnvFree",
seed: "no env",
noSupermemory: true,
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected ok");
@@ -225,48 +178,12 @@ describe("runDebugBrainInit", () => {
});
});
describe("Brain.create (production path — debug: false)", () => {
test("B6: with debug omitted (default), uses db.ingestStatements and does NOT return extractedFacts", async () => {
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const result = await ProdBrain.create("ProdMaren", "a prod seed", {
dbPath: ":memory:",
braindbPath,
});
try {
expect(result).not.toBeNull();
if (!result) throw new Error("expected result");
expect(result.brain).toBeDefined();
expect(result.description).toBe(PERSONA_DESCRIPTION);
expect(result.baseSystemPrompt).toContain(GENERATED_BASE_SYSTEM_PROMPT);
expect(result.extractedFacts).toBeUndefined();
} finally {
try {
await unlink(braindbPath);
} catch {}
}
});
test("B7: with debug: false (explicit), same as default — uses db.ingestStatements, no extractedFacts", async () => {
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const result = await ProdBrain.create("ProdMaren2", "seed", {
dbPath: ":memory:",
braindbPath,
debug: false,
});
try {
expect(result).not.toBeNull();
if (!result) throw new Error("expected result");
expect(result.extractedFacts).toBeUndefined();
} finally {
try {
await unlink(braindbPath);
} catch {}
}
});
});
// ---------------------------------------------------------------------------
// Removed: B6 and B7 (production path with `debug: true|false` option).
//
// Reason: `Brain.create` no longer accepts a `debug` option. The production
// path is now identical to the debug path — `Brain.create` always persists
// facts to supermemory and returns `{ brain, description, baseSystemPrompt }`
// (no `extractedFacts`). B1 already exercises the post-refactor production
// behavior end-to-end through `runDebugBrainInit`.
// ---------------------------------------------------------------------------

View File

@@ -3,15 +3,20 @@ import { unlink, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import type { Command } from "commander";
import ora from "ora";
import type { ExtractedFact } from "identitydb";
import { Brain } from "@/brain";
import { runCreateSteps } from "@/brain";
import { MemoryStub } from "@/brain/stub";
import { formatDuration } from "@/utils/duration";
import { logger } from "@/utils/logger";
import {
StepDriver,
printKeyValue,
printSection,
} from "./output";
export interface BrainInitOptions {
displayName: string;
seed: string;
noSupermemory: boolean;
}
export type BrainInitResult =
@@ -23,26 +28,12 @@ export type BrainInitResult =
spaceName: string;
description: string;
baseSystemPrompt: string;
extractedFacts: ExtractedFact[];
storedFacts: Array<{ customId: string | null; content: string }>;
storageMode: "supermemory" | "stub";
elapsedMs: number;
}
| { ok: false; error: string; elapsedMs: number };
/**
* Exercise the full `Brain.create` flow (PERSONA_INIT → PERSONA_BASE_SYSTEM_PROMPT
* LLM calls → SQLite DB upsert → fact extraction via `factExtractor.extract` →
* braindb save) without touching real on-disk state.
*
* - SQLite DB uses `:memory:` (ephemeral, dies with the process).
* - The braindb JSON is written to a fresh temp file under `os.tmpdir()`
* and unlinked after the run.
*
* Prints the full text of:
* 1. the generated `description` (PERSONA_INIT output)
* 2. the concatenated `baseSystemPrompt` (generated + fixed)
* 3. the `extractedFacts` (obtained by directly calling
* `factExtractor.extract(description)`)
*/
export async function runDebugBrainInit(
opts: BrainInitOptions,
): Promise<BrainInitResult> {
@@ -52,56 +43,53 @@ export async function runDebugBrainInit(
`brainbox-debug-brain-${randomUUID()}.json`,
);
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const spinner = ora(
`Initializing brain "${opts.displayName}" with LLM (debug, no real disk state)...`,
).start();
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
const db = opts.noSupermemory ? new MemoryStub() : undefined;
try {
const result = await Brain.create(opts.displayName, opts.seed, {
dbPath: ":memory:",
const steps = new StepDriver(4);
const result = await runCreateSteps(opts.displayName, opts.seed, {
braindbPath,
debug: true,
});
db,
}, steps);
if (!result) {
spinner.fail("Brain initialization failed");
const elapsedMs = Date.now() - startTime;
return { ok: false, error: "Brain initialization failed", elapsedMs };
}
const {
brain,
description,
baseSystemPrompt,
extractedFacts,
} = result;
const factCount = extractedFacts?.length ?? 0;
spinner.succeed(
`Brain initialized (id=${brain.brainbase.brainId}, space=${brain.brainbase.spaceName}, ${factCount} fact(s) extracted)`,
);
const { brain, description, baseSystemPrompt } = result;
const storedFacts = await brain.list();
printSection(`Description (PERSONA_INIT output)`);
console.log();
printSection(`Brain — ${brain.brainbase.displayName}`);
printKeyValue({
brainId: brain.brainbase.brainId,
spaceName: brain.brainbase.spaceName,
storage: storageMode,
documents: String(storedFacts.length),
});
console.log();
printSection(`Step 1 output — Description (PERSONA_INIT)`);
console.log(description);
console.log();
printSection(`baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`);
printSection(`Step 2 output — baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`);
console.log(baseSystemPrompt);
console.log();
printSection(
`Extracted facts (factExtractor.extract — ${factCount})`,
);
if (extractedFacts && extractedFacts.length > 0) {
extractedFacts.forEach((fact, i) => {
console.log(`\n[${i + 1}/${extractedFacts.length}]`);
console.log(` statement: ${fact.statement ?? ""}`);
console.log(` summary: ${fact.summary ?? ""}`);
console.log(` source: ${fact.source ?? ""}`);
console.log(` confidence: ${fact.confidence ?? ""}`);
console.log(` topics: ${JSON.stringify(fact.topics)}`);
if (fact.metadata) {
console.log(` metadata: ${JSON.stringify(fact.metadata)}`);
}
printSection(`Step 3 output — Stored documents (brain.list() — ${storedFacts.length})`);
if (storedFacts.length > 0) {
storedFacts.forEach((doc, i) => {
console.log();
console.log(`[${i + 1}/${storedFacts.length}]`);
printKeyValue({
customId: doc.customId ?? "(none)",
content: doc.content,
});
});
} else {
console.log(" (no facts extracted)");
console.log(" (no documents stored)");
}
console.log();
@@ -118,14 +106,10 @@ export async function runDebugBrainInit(
spaceName: brain.brainbase.spaceName,
description,
baseSystemPrompt,
extractedFacts: extractedFacts ?? [],
storedFacts,
storageMode,
elapsedMs,
};
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
spinner.fail("Brain initialization failed");
const elapsedMs = Date.now() - startTime;
return { ok: false, error: reason, elapsedMs };
} finally {
try {
await unlink(braindbPath);
@@ -136,37 +120,35 @@ export async function runDebugBrainInit(
export function addBrainSubcommand(parent: Command): Command {
const cmd = parent
.command("brain")
.description(
"Debug tools for brain lifecycle (no real disk writes)",
);
.description("Debug tools for brain lifecycle (no real disk writes)");
cmd
.command("init")
.description(
"Initialize a new brain with LLM (in-memory DB, temp braindb; nothing persisted)",
"Initialize a new brain with LLM (temp braindb; nothing persisted to repo)",
)
.requiredOption("-n, --name <text>", "Display name for the new brain")
.requiredOption(
"-s, --seed <text>",
"Seed text used to generate the persona biography",
)
.action(async (opts: { name: string; seed: string }) => {
const result = await runDebugBrainInit({
displayName: opts.name,
seed: opts.seed,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
});
.option(
"--no-supermemory",
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
)
.action(
async (opts: { name: string; seed: string; supermemory: boolean }) => {
const result = await runDebugBrainInit({
displayName: opts.name,
seed: opts.seed,
noSupermemory: opts.supermemory === false,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
},
);
return cmd;
}
function printSection(title: string): void {
const line = "─".repeat(Math.max(40, title.length + 4));
console.log(`\n┌${line}`);
console.log(`${title}`);
console.log(`${line}`);
}

View File

@@ -0,0 +1,63 @@
import chalk from "chalk";
import ora, { type Ora } from "ora";
export function printSection(title: string): void {
const line = "─".repeat(Math.max(40, title.length + 4));
console.log(`\n┌${line}`);
console.log(`${title}`);
console.log(`${line}`);
}
export function printKeyValue(pairs: Record<string, string>): void {
const labelWidth = Math.max(...Object.keys(pairs).map((k) => k.length));
for (const [key, value] of Object.entries(pairs)) {
console.log(` ${key.padEnd(labelWidth)} ${value}`);
}
}
export class StepDriver {
private readonly stepCount: number;
private stepIndex = 0;
private current: Ora | null = null;
private currentLabel = "";
constructor(stepCount: number) {
this.stepCount = stepCount;
}
start(label: string): void {
this.stepIndex += 1;
this.resolvePrevious();
this.currentLabel = label;
const text = `Step ${this.stepIndex}/${this.stepCount}: ${label}`;
this.current = ora(text).start();
}
done(summary: string): void {
if (!this.current) return;
const text = this.current.text;
this.current.succeed(`${text}${summary}`);
this.current = null;
}
fail(reason: string): void {
if (!this.current) {
console.log(`${chalk.red("✖")} ${this.currentLabel}${reason}`);
return;
}
this.current.fail(`${this.current.text}${reason}`);
this.current = null;
}
private resolvePrevious(): void {
if (this.current) {
this.current.stop();
this.current = null;
}
}
}
export function snippet(text: string): string {
const flat = text.replace(/\s+/g, " ").trim();
return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
}

View File

@@ -101,6 +101,7 @@ describe("runDebugScheduleDaily", () => {
const result = await runDebugScheduleDaily({
message: "focus on writing",
personality: "test-personality-XYZ",
noSupermemory: true,
});
expect(result.ok).toBe(true);
@@ -131,6 +132,7 @@ describe("runDebugScheduleDaily", () => {
const result = await runDebugScheduleDaily({
message: "",
personality: "p",
noSupermemory: true,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected !ok");
@@ -145,6 +147,7 @@ describe("runDebugScheduleMonthly", () => {
const result = await runDebugScheduleMonthly({
message: "study for GRE",
personality: "test-personality-ABC",
noSupermemory: true,
});
expect(result.ok).toBe(true);
@@ -169,6 +172,7 @@ describe("runDebugScheduleMonthly", () => {
const result = await runDebugScheduleMonthly({
message: "",
personality: "p",
noSupermemory: true,
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected !ok");
@@ -186,7 +190,7 @@ describe("debug schedule no-disk invariant", () => {
const beforeDb = existsSync(resolve(process.cwd(), "brainbox.db"));
const beforeJson = existsSync(resolve(process.cwd(), "brainbox.json"));
await runDebugScheduleDaily({ message: "m", personality: "p" });
await runDebugScheduleDaily({ message: "m", personality: "p", noSupermemory: true });
const afterDb = existsSync(resolve(process.cwd(), "brainbox.db"));
const afterJson = existsSync(resolve(process.cwd(), "brainbox.json"));

View File

@@ -1,6 +1,10 @@
import type { Command } from "commander";
import ora from "ora";
import { Brain } from "@/brain";
import {
Brain,
runCreateDailyScheduleSteps,
runCreateMonthlyScheduleSteps,
} from "@/brain";
import { MemoryStub } from "@/brain/stub";
import {
type AvailabilityWindows,
type DailySchedule,
@@ -9,10 +13,16 @@ import {
import { formatDuration } from "@/utils/duration";
import { logger } from "@/utils/logger";
import { formatDateKey, nextMonth, pad2 } from "@/brain/schedule";
import {
StepDriver,
printKeyValue,
printSection,
} from "./output";
export interface ScheduleOptions {
message: string;
personality: string;
noSupermemory: boolean;
}
export type DailyRunResult =
@@ -23,6 +33,7 @@ export type DailyRunResult =
tomorrow: Date;
schedule: DailySchedule;
availability: AvailabilityWindows;
storageMode: "supermemory" | "stub";
elapsedMs: number;
}
| { ok: false; error: string; elapsedMs: number };
@@ -34,6 +45,7 @@ export type MonthlyRunResult =
monthKey: string;
daysInMonth: number;
schedule: MonthlySchedule;
storageMode: "supermemory" | "stub";
elapsedMs: number;
}
| { ok: false; error: string; elapsedMs: number };
@@ -49,15 +61,23 @@ export async function runDebugScheduleDaily(
today.getDate() + 1,
);
const dateKey = formatDateKey(tomorrow);
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
const db = opts.noSupermemory ? new MemoryStub() : undefined;
const brain = await Brain.createDebug({ personality: opts.personality });
const brain = await Brain.createDebug(
{ personality: opts.personality },
db,
);
const scheduleSpinner = ora(
`Generating daily schedule for ${dateKey}...`,
).start();
const schedule = await brain.createDailySchedule(today, opts.message);
const steps = new StepDriver(4);
const schedule = await runCreateDailyScheduleSteps(
brain,
today,
opts.message,
steps,
);
if (!schedule) {
scheduleSpinner.fail("Daily schedule generation failed");
const elapsedMs = Date.now() - startTime;
return {
ok: false,
@@ -65,19 +85,11 @@ export async function runDebugScheduleDaily(
elapsedMs,
};
}
scheduleSpinner.succeed(
`Daily schedule generated (${schedule.items.length} slots)`,
);
printSection(
`Daily Schedule — ${dateKey} (${tomorrow.toLocaleDateString("en-US", { weekday: "long" })})`,
);
console.log(JSON.stringify(schedule, null, 2));
const availSpinner = ora("Deriving availability...").start();
steps.start("deriving availability (SCHEDULE_AVAILABILITY)");
const availability = await brain.deriveAvailabilityFromSchedule(schedule);
if (!availability) {
availSpinner.fail("Availability derivation failed");
steps.fail("see error above");
const elapsedMs = Date.now() - startTime;
return {
ok: false,
@@ -85,16 +97,29 @@ export async function runDebugScheduleDaily(
elapsedMs,
};
}
availSpinner.succeed(
`Availability derived (${availability.items.length} windows)`,
);
steps.done(`${availability.items.length} windows`);
printSection(`Availability — ${dateKey}`);
console.log();
printSection(`Schedule — daily (${dateKey})`);
printKeyValue({
dateKey,
weekday: tomorrow.toLocaleDateString("en-US", { weekday: "long" }),
storage: storageMode,
slots: String(schedule.items.length),
});
console.log();
printSection(`Step 1/2 output — Daily Schedule (DAILY_SCHEDULE)`);
console.log(JSON.stringify(schedule, null, 2));
console.log();
printSection(`Step 2/2 output — Availability (SCHEDULE_AVAILABILITY)`);
console.log(JSON.stringify(availability, null, 2));
console.log();
const elapsedMs = Date.now() - startTime;
logger.info(
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to disk.`,
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to real disk.`,
);
return {
@@ -104,6 +129,7 @@ export async function runDebugScheduleDaily(
tomorrow,
schedule,
availability,
storageMode,
elapsedMs,
};
}
@@ -115,15 +141,23 @@ export async function runDebugScheduleMonthly(
const today = new Date();
const next = nextMonth(today);
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
const db = opts.noSupermemory ? new MemoryStub() : undefined;
const brain = await Brain.createDebug({ personality: opts.personality });
const brain = await Brain.createDebug(
{ personality: opts.personality },
db,
);
const scheduleSpinner = ora(
`Generating monthly schedule for ${monthKey} (${next.daysInMonth} days)...`,
).start();
const schedule = await brain.createMonthlySchedule(today, opts.message);
const steps = new StepDriver(3);
const schedule = await runCreateMonthlyScheduleSteps(
brain,
today,
opts.message,
steps,
);
if (!schedule) {
scheduleSpinner.fail("Monthly schedule generation failed");
const elapsedMs = Date.now() - startTime;
return {
ok: false,
@@ -131,16 +165,24 @@ export async function runDebugScheduleMonthly(
elapsedMs,
};
}
scheduleSpinner.succeed(
`Monthly schedule generated (${schedule.items.length} day summaries)`,
);
printSection(`Monthly Schedule — ${monthKey} (${next.daysInMonth} days)`);
console.log();
printSection(`Schedule — monthly (${monthKey})`);
printKeyValue({
monthKey,
daysInMonth: String(next.daysInMonth),
storage: storageMode,
summaries: String(schedule.items.length),
});
console.log();
printSection(`Step 1/1 output — Monthly Schedule (MONTHLY_SCHEDULE)`);
console.log(JSON.stringify(schedule, null, 2));
console.log();
const elapsedMs = Date.now() - startTime;
logger.info(
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to disk. (Availability applies per-day and is not generated for the monthly view.)`,
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to real disk. (Availability applies per-day and is not generated for the monthly view.)`,
);
return {
@@ -149,6 +191,7 @@ export async function runDebugScheduleMonthly(
monthKey,
daysInMonth: next.daysInMonth,
schedule,
storageMode,
elapsedMs,
};
}
@@ -158,38 +201,53 @@ export function addScheduleSubcommand(parent: Command): Command {
.command("schedule")
.description("Generate a test schedule (no disk writes)");
cmd.command("daily")
cmd
.command("daily")
.description(
"Generate a daily schedule for tomorrow and print schedule + availability",
)
.requiredOption("-m, --message <text>", "User direction for the schedule")
.requiredOption("-p, --personality <text>", "Brain personality to use")
.action(async (opts: ScheduleOptions) => {
const result = await runDebugScheduleDaily(opts);
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
});
.option(
"--no-supermemory",
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
)
.action(
async (opts: { message: string; personality: string; supermemory: boolean }) => {
const result = await runDebugScheduleDaily({
message: opts.message,
personality: opts.personality,
noSupermemory: opts.supermemory === false,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
},
);
cmd.command("monthly")
cmd
.command("monthly")
.description("Generate a monthly schedule for next month and print it")
.requiredOption("-m, --message <text>", "User direction for the schedule")
.requiredOption("-p, --personality <text>", "Brain personality to use")
.action(async (opts: ScheduleOptions) => {
const result = await runDebugScheduleMonthly(opts);
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
});
.option(
"--no-supermemory",
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
)
.action(
async (opts: { message: string; personality: string; supermemory: boolean }) => {
const result = await runDebugScheduleMonthly({
message: opts.message,
personality: opts.personality,
noSupermemory: opts.supermemory === false,
});
if (!result.ok) {
logger.error(result.error);
process.exit(1);
}
},
);
return cmd;
}
function printSection(title: string): void {
const line = "─".repeat(Math.max(40, title.length + 4));
console.log(`\n┌${line}`);
console.log(`${title}`);
console.log(`${line}`);
}

View File

@@ -3,13 +3,16 @@ import { join } from "path";
export interface Config {
openrouterApiKey: string;
dbPath: string;
supermemoryApiKey: string;
braindbPath: string;
}
const openrouterApiKey = process.env["OPENROUTER_API_KEY"];
if (!openrouterApiKey) throw new Error("OPENROUTER_API_KEY is missing");
const dbPath = join(process.cwd(), process.env["DB_PATH"] ?? "brainbox.db");
const supermemoryApiKey = process.env["SUPERMEMORY_API_KEY"];
if (!supermemoryApiKey) throw new Error("SUPERMEMORY_API_KEY is missing");
const braindbPath = join(
process.cwd(),
process.env["BRAINDB_PATH"] ?? "brainbox.json",
@@ -17,6 +20,6 @@ const braindbPath = join(
export const config: Config = {
openrouterApiKey,
dbPath,
supermemoryApiKey,
braindbPath,
};

View File

@@ -1,57 +0,0 @@
import { config } from "@/config";
import { OpenRouter } from "@openrouter/sdk";
import type { EmbeddingProvider } from "identitydb";
export const QWEN_EMBEDDING_MODEL = "qwen/qwen3-embedding-8b" as const;
export const QWEN_EMBEDDING_DIMENSIONS = 512 as const;
export class OpenRouterEmbeddingProvider implements EmbeddingProvider {
readonly model: string = QWEN_EMBEDDING_MODEL;
readonly dimensions: number = QWEN_EMBEDDING_DIMENSIONS;
private client: OpenRouter;
constructor(apiKey: string = config.openrouterApiKey) {
this.client = new OpenRouter({ apiKey, appTitle: "boxbrain" });
}
async embed(input: string): Promise<number[]> {
const result = await this.embedBatch([input]);
return result[0]!;
}
async embedMany(inputs: string[]): Promise<number[][]> {
if (inputs.length === 0) return [];
return await this.embedBatch(inputs);
}
private async embedBatch(inputs: string[]): Promise<number[][]> {
const response = await this.client.embeddings.generate({
requestBody: {
model: this.model,
input: inputs,
dimensions: this.dimensions,
encodingFormat: "float",
},
});
if (typeof response === "string") {
throw new Error("OpenRouter returned a non-JSON embeddings response");
}
const ordered = new Array<number[]>(inputs.length);
for (const item of response.data) {
if (typeof item.embedding === "string") {
throw new Error(
"OpenRouter returned a base64 embedding but float was requested",
);
}
const index = item.index ?? 0;
ordered[index] = item.embedding;
}
for (let i = 0; i < ordered.length; i += 1) {
if (!ordered[i]) {
throw new Error(`OpenRouter omitted embedding for input index ${i}`);
}
}
return ordered;
}
}

View File

@@ -1,54 +1,3 @@
export const extractedFactSchema = {
type: "object",
additionalProperties: false,
properties: {
items: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
statement: { type: "string" },
summary: { type: "string" },
source: { type: "string" },
confidence: { type: "number" },
metadata: { type: "object", additionalProperties: false },
topics: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string" },
category: {
type: "string",
enum: ["entity", "concept", "temporal", "custom"],
},
granularity: {
type: "string",
enum: ["abstract", "concrete", "mixed"],
},
role: { type: "string" },
description: { type: "string" },
metadata: { type: "object", additionalProperties: false },
},
required: [
"name",
"category",
"granularity",
"role",
"description",
],
},
},
},
required: ["statement", "summary", "source", "confidence", "topics"],
},
},
},
required: ["items"],
};
const timeString = {
type: "string",
pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$",
@@ -133,9 +82,6 @@ export const availabilitySchema = {
// Types — co-located with their schemas.
// ----------------------------------------------------------------------------
import type { ExtractedFact } from "identitydb";
/** A single 30-minute slot in a daily schedule. Matches `dailyScheduleSchema.items.items`. */
export type DailySlot = {
start: string;
end: string;
@@ -143,52 +89,27 @@ export type DailySlot = {
notes: string;
};
/**
* A complete daily schedule: a wrapped object containing exactly 48 half-hour
* slots. Matches `dailyScheduleSchema` (the LLM is constrained to return the
* `{ items: [...] }` envelope).
*/
export type DailySchedule = {
items: DailySlot[];
};
/** A single day's summary inside a monthly schedule. Matches `monthlyScheduleSchema.items.items`. */
export type MonthlyDay = {
day: number;
summary: string;
};
/**
* A complete monthly schedule: a wrapped object containing one entry per day
* of the month. Matches `monthlyScheduleSchema`.
*/
export type MonthlySchedule = {
items: MonthlyDay[];
};
/** Reachability status for a single availability window. */
export type AvailabilityStatus = "online" | "do-not-disturb" | "offline";
/** A single availability window. Matches `availabilitySchema.items.items`. */
export type Availability = {
start: string;
end: string;
status: AvailabilityStatus;
};
/**
* The full set of availability windows for a day: a wrapped object containing
* one or more windows. Matches `availabilitySchema`.
*/
export type AvailabilityWindows = {
items: Availability[];
};
/**
* The wrapped envelope the LLM returns for `extractedFactSchema`. The inner
* `items` array is the list of `ExtractedFact` (which is defined in the
* external `identitydb` package).
*/
export type ExtractedFactResult = {
items: ExtractedFact[];
};