Skip to content

Usage

What novamem does, when to use each tool, what the gates and decay loops mean for you. For the underlying mechanics see architecture.md. For per-tool field reference see skills/novamem/references/ or the OpenAPI spec.

Mental model

A novamem instance holds memory entries. Every entry belongs to a single user. An entry can additionally belong to a project (a sub-brain) which can be shared with other users. Each entry lives in a tier — warm or cold — and each search runs three signals (keyword, vector, graph) in parallel.

A user-global entry is invisible to anyone else, period. A project entry is visible to every member of that project — that's the whole point of sharing.

Hybrid search runs keyword (FTS) + vector (cosine) + graph (neighbour) in parallel and fuses the three with weighted scoring:

Default weightSignalBest for
0.6vectorconcept-level questions, paraphrases
0.3keywordliteral symbol / id / file / hash matches
0.1graph"what's adjacent to X?" once you have a seed

Override weights when a default doesn't fit the question:

Needweights
Exact id / file path / commit hash{ keyword: 1, vector: 0 }
Pure semantic ("things like X" with no shared tokens){ vector: 1, keyword: 0 }
Neighbour-driven recall around a known entry{ graph: 1 }

Hits below ~0.4 are misses — treat them as "nothing relevant" rather than chasing low-confidence matches.

By default a search runs in one namespace (default "default") and one project (or user-global if no project arg). Two ways to widen:

  • includeNamespaces: ["a", "b"] — union across namespaces (capped at 16)
  • includeProjects: ["a", "b"] — union user-global with each listed project (capped at 16)

Or set an active project once and memory_* defaults to it.

Remembering (memory_remember)

Save things that will still matter next session:

  • Decisions with reasoning — "chose drizzle over knex because we want compile-time-checked SQL"
  • User preferences that recur — tools, formatting, review style
  • Hidden constraints — legal, deadlines, dep pins
  • Bug post-mortems — root cause + fix, not just the fix
  • Architecture invariants the codebase wouldn't reveal on its own

Don't save:

  • Conversational context that ends with the task
  • Facts trivially derivable from the current code
  • Anything the user said is private/secret
  • Verbatim error stack traces — extract the diagnosis instead

The worthiness gate

Every remember call passes through a hard-rule gate before insertion:

  • Length floor — trim then reject if < 12 chars
  • Filler regex — single-word canned replies (thanks?, ok(ay)?, sure, got it, noted, …)

A rejected request returns { id: null, rejected: <reason> } with HTTP 200 — no error, just a signal. Pass force: true to bypass when the user explicitly asked.

The exact-duplicate fast-path

Every entry stores a sha256 of its trimmed content. If you remember something already present in the same (user, project) scope, the response is { id: <existingId>, deduplicated: true } — the existing row's hit count is bumped and its decay clock is refreshed. Treat this as success.

This runs even when force: true, so a duplicate of yourself just returns the existing id.

Provenance

Set these on remember (and update) when known:

FieldVocabularyPurpose
sourceTypechat / email / code-review / doc / inference / observation / system / manualfilter by where the fact came from
capturedFromfree textagent name, conversation id, channel ref
confidence0..1, default 1.0lower for inferred facts; usable as a future filter

Updating (memory_update)

Facts evolve. When the user says "I now live in Singapore", search for the existing "lives in" memory and call memory_update instead of forget + remember. Update preserves the entry's id, hit count, and graph edges; it re-embeds when content changes. Skip the embedder by omitting content if you only need to bump metadata, provenance, or confidence.

Forgetting (memory_forget)

Hard delete: removes warm row, FTS shadow, cold vector, and graph edges. There is no undo. Use when:

  • The user explicitly asks to forget
  • An entry is wrong and not just outdated (use update for outdated)
  • You wrote a memory that the user vetoed in the same turn

Time-windowed recall

memory_recent returns newest-first within a namespace, with an optional since ISO-8601 lower bound. memory_today is sugar for recent with since = now - 24h. Use these when the user is anchoring on time, not relevance ("what did we work on yesterday?").

Surfacing a cold entry via recent does not auto-promote it (only search does); recall is non-mutating.

Graph traversal (memory_neighbors)

Walks the FalkorDB graph from a seed entry id and returns the same hit shape as search, scored by graph proximity. depth defaults to 1; prefer 1, larger depths are exponential and noisy.

Edges come from two sources:

  • co_occurs — written automatically by remember, linking each new entry to its top-3 vector neighbours at write time
  • co_inferred — written by the dream cycle when two entries share ≥3 common neighbours

When the graph store is offline, the response carries degraded: true and the neighbour set is empty. Tell the user.

Projects (sub-brains)

A project is a named collection of memories the owner can share with other users. Project memories are visible to every member; the owner can add or remove members at any time.

The seven project_* tools cover the full lifecycle:

ToolWhoPurpose
project_listany userlist projects the caller owns or is a member of
project_createany usercreate a new project; the caller becomes its owner. Returns the assigned ULID
project_deleteowner onlydelete the project and every memory entry, vector, and graph node in it. No undo
project_shareowner onlyadd another user as a member by their exact email address. Member can read and write
project_unshareowner onlyremove a member. The owner cannot unshare themselves — project_delete instead
project_activateany userset the caller's active project (server-side per-user state)
project_deactivateany userclear the active project

Active project

project_activate({ project: <id-or-name> }) makes subsequent memory_* calls without an explicit project arg default to it:

  • search / recent / neighbors union the active project with user-global
  • remember / forget / update target the active project directly

Use this when the user signals they're working on a specific project ("let's switch to Phoenix"). The pointer is server-side state per user, so a switch on one device is visible to every other device the user signs in from.

Stats (memory_stats)

Per-caller snapshot of how big the caller's memory is, scoped to the authenticated user. No arguments; non-mutating. Returns:

FieldShapeMeaning
byNamespaceRecord<string, {warm, cold}>Entry counts grouped by namespace, split by tier
totalWarm / totalColdnumberCaller's totals across all namespaces
lastDecayAtISO-8601 or nullWhen the service last ran the decay loop (service-wide context)
uptimeMsnumberService uptime in ms (service-wide context)

Useful for skills that want to surface a "you have N entries across M namespaces" hint without polling the dashboard. Not the dashboard's full Metrics view — that's /v1/me/metrics (per-user counters + rolling rates) or /v1/admin/metrics (the whole service).

Decay and reinforcement

Entries decay if not accessed:

effectiveDays(hits) = 7 · log₂(hits + 1)

An entry idle for longer than its lifespan gets demoted from warm → cold by the decay loop (every 6 h by default). A subsequent search that hits the cold entry re-promotes it to warm.

You don't need to manage this. Just know that frequently-relevant entries don't fall off, and once-and-done facts age out gracefully.

Dream cycle (compaction)

A daily compaction pass — also triggerable on demand via POST /v1/dream-cycle — does two things:

  1. Dedup-merge — for each entry, query Qdrant for top-3 vector neighbours; merge a pair when cosine ≥ 0.97 AND token-set Jaccard ≥ 0.5. Both required so contradictions like "lives in X" / "lives in Y" don't collapse. Picks canonical by hit count (oldest tiebreak), redirects graph edges, sums hits, deletes the loser everywhere.
  2. Edge promotion — when two entries share ≥3 graph neighbours in common, add a direct A→B edge with relation: "co_inferred". Tagged distinctly from co_occurs so search ranking can dial it back.

Response: { walked, merged, edgesPromoted, durationMs }.

Per-host wiring

For agent-host integrations (when to remember, what weights to pick, project scoping conventions), see the Connect guides — each one has the host-specific behaviour rules and a verify snippet.

Errors and signals

ResponseMeaningWhat to do
{ id: null, rejected: <reason> }Worthiness gateSurface the reason; retry with force: true only if the user asked
{ id: <existing>, deduplicated: true }Exact-duplicate fast-pathSuccess — the existing entry was reinforced
degraded: true on search/neighborsGraph store offlineMention to the user; warm + cold paths still work
401Bearer missing / revokedDon't retry; surface to the user
403 not a memberProject exists but caller can't reach itDistinct from 404; the project name is right but you don't have access
404 no such projectProject name/id doesn't resolveCaller should project_list to see what's available