This is the canonical packaged 0.7.5 reference path for chatbox/copilot-style
integration. Registry commands require publication; source verification uses
the local tarball path below.
Published install:
npm install goodmemory@0.7.5Bun install:
bun add goodmemory@0.7.5Tarball verification of the same release artifact before publish:
npm install ./goodmemory-0.7.5.tgzFor a normal Node HTTP service, start with the canonical 15-minute guide:
docs/GoodMemory-15-Minute-App-Integration.md. It shows the current app loop
with memory.runtime.*, recall, buildContext,
memory.jobs.enqueueRemember(), targeted reviseMemory(), and traceSink.
- Install the package with npm or Bun and keep the default public runtime entrypoint:
createGoodMemory({}) - Use the Express or Fastify examples when you want a minimal Node route around the current memory loop.
- Accept normal
ModelMessage[]input plus a scopeduserId/workspaceId/sessionId. - Use
createGoodMemoryAISDK(...)when your server already standardizes on AI SDK streaming. - For repo-local comparison only, run the reference example:
bun run example:ai-sdk-server
import { createGoodMemory } from "goodmemory";
import type { GoodMemoryStreamTextInput } from "goodmemory/ai-sdk";
import { createGoodMemoryAISDK } from "goodmemory/ai-sdk";
const memory = createGoodMemory({});
const aiSDK = createGoodMemoryAISDK({
memory,
});
type MemoryChatRequest = Pick<
GoodMemoryStreamTextInput,
"messages" | "query" | "scope" | "system"
>;
function isMemoryChatRequest(value: unknown): value is MemoryChatRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const candidate = value as Record<string, unknown>;
const scope = candidate.scope;
return Array.isArray(candidate.messages)
&& !!scope
&& typeof scope === "object"
&& !Array.isArray(scope)
&& typeof (scope as { userId?: unknown }).userId === "string"
&& (scope as { userId: string }).userId.trim().length > 0;
}
export async function handleMemoryChat(request: Request): Promise<Response> {
const body: unknown = await request.json();
if (!isMemoryChatRequest(body)) {
return new Response(
JSON.stringify({
error: "Expected a request body with a messages array and scope.userId.",
}),
{
headers: {
"content-type": "application/json; charset=utf-8",
},
status: 400,
},
);
}
const result = aiSDK.streamText({
messages: body.messages,
query: body.query,
scope: body.scope,
system: body.system,
model: {} as never,
});
return result.toTextStreamResponse();
}Next.js mapping:
export async function POST(request: Request)can delegate directly to the same handler body.- For ordinary Node HTTP services, prefer the thin route examples:
examples/express-chat-server.tsandexamples/fastify-chat-server.ts. - The repo-local AI SDK example is
examples/plain-ai-sdk-server.ts. examples/vercel-ai-chat.tsremains as the lower-level wrapper-first example.- The HTTP boundary should reject malformed
scopeinput instead of silently soft-failing memory semantics.
Hosts using goodmemory/runtime-kit directly should carry the anchor returned
by beforeModelCall into the matching afterModelCall. The runtime kit does
not associate turns by scope because multiple model calls in one scope can run
concurrently.
import { createGoodMemoryRuntimeKit } from "goodmemory/runtime-kit";
const runtimeKit = createGoodMemoryRuntimeKit({ memory });
const before = await runtimeKit.beforeModelCall({
scope,
messages,
timezone: "America/New_York",
});
const answer = await callModel({
messages,
system: before.context.content,
});
await runtimeKit.afterModelCall({
scope,
messages,
assistantText: answer,
referenceTime: before.referenceTime,
timezone: before.timezone,
writeback: { mode: "observe" },
});Only the latest user message receives the returned referenceTime as an
observedAt fallback. Historical messages are never backfilled; callers may
set message-level observedAt and timezone when those values are already
known.
goodmemory,goodmemory/ai-sdk,goodmemory/host, andgoodmemory/httpnow resolve through compiled package artifacts on both Node and Bun.- Domain-specific writes should use
createGoodMemory({ remember: ... })with public profiles, rules, custom extractors, and annotations. testing.extractoris not a product integration surface; it remains available for tests.rememberprofiles differ fromretrievalProfile: remember profiles control what gets written, while retrieval profiles control recall routing and context assembly.- Domain rules differ from
policyhooks: rules generate normal candidates before classification, evidence, conflict handling, vector writes, and rollback; policy hooks remain governance gates after candidates exist. - The canonical deterministic path uses the accepted Phase 26 local-first runtime and the accepted Phase 28 supported local acceleration behavior.
- Bun keeps the local SQLite default runtime path; Node zero-config runtime currently falls back to in-memory when the built-in local SQLite adapter is unavailable.
- No embedding environment variables means the runtime stays
rules-only. - Tarball and registry installability are both valid package-boundary paths for this package line.
- The reference path should use only:
goodmemorygoodmemory/ai-sdkgoodmemory/hostgoodmemory/http
- public imports are enough for the reference integration path
- the default runtime can stand up a working memory loop without repo-internal imports
- the canonical public path is a plain AI SDK server that returns
Request -> ResponsethroughtoTextStreamResponse() - the AI SDK wrapper can augment recall and remember on the public surface
- the same public surface is installable from the packed release artifact or from registry install, not only from a repo checkout
A server-side agent can declare domain write behavior without forking the core extractor. OneLife and life-coach agents are motivating examples, but they are not built-in presets.
import { createGoodMemory, rememberRules } from "goodmemory";
const memory = createGoodMemory({
remember: {
profiles: [
{
id: "life-coach",
when: { agentId: "life-coach" },
rules: [
rememberRules.fact(/my top priority this quarter is (.+)/i, {
id: "life-goal-priority",
category: "goal",
tags: ["life_coach", "long_term_goal"],
attributes: { horizon: "quarter" },
content: ({ match }) => match[1] ?? "",
}),
rememberRules.preference(/please coach me with (.+)/i, {
id: "life-coaching-style",
category: "coaching_style",
value: ({ match }) => match[1] ?? "",
}),
],
assistantOutputs: { mode: "confirmed_or_verified_only" },
},
],
},
});Host annotations should be used for explicit write intent:
remember: "never"suppresses the annotated message before deterministic, custom, or assisted extraction.remember: "always"can raise a valid low-confidence candidate through normal classification and policy; it does not bypass redaction or policy hooks.metadataPatch,kindHint,confirmed,verified, andreasonare preserved in remember traces so audit output can explain why a write changed.
Custom domain extractors should use the named profile form when trace stability matters:
extractors: [
{
id: "life-coach-values-extractor",
extractor: {
async extract(input) {
return {
candidates: [],
ignoredMessageCount: 0,
};
},
},
},
],The raw MemoryExtractor array form remains supported for compatibility; the
named form keeps extractorIds stable across profile reordering and replayed
evals. Named extractor ids are audit identities: they must be non-blank and
unique in the resolved profile, and they cannot use the generated
${profileId}:extractor-N raw-extractor namespace.
Remember events carry the resolved profileId and presetId for default
preset, rules, custom extractor, assisted-only, and annotation-derived writes.
Storage guidance stays deployment-dependent:
- in-memory storage is for tests and short-lived demos
- SQLite is acceptable for local and single-writer deployments
- Postgres is recommended for multi-instance production
For Python backends or Expo clients, keep GoodMemory on the server side as a
Node/Bun sidecar or service. The mobile/client app should call the server API;
it does not need GoodMemory bundled into the client runtime. Python backends can
use the packaged goodmemory-http-bridge server or import the TypeScript
bridge API from goodmemory/http in a Node/Bun sidecar.