npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synthryn/sypi-memory

v0.6.0-beta.20260816.6a00fcae

Published

Optional extension: Save notes locally, retrieve them by meaning, and auto-inject a bounded set of relevant memories into each turn. Use it when useful project or personal context should survive sessions without leaving the machine. Primary capability: th

Readme

sypi-memory

Local vector memory. Notes survive across sessions, and retrieval never leaves the machine after the embedding model is on disk.

Surface

One memory tool with four ops:

  • op:add saves a note. scope is project-local by default, or global across every project.
  • op:search recalls notes by meaning.
  • op:forget deletes a note by id.
  • op:why shows where a recalled note came from: session, timestamp, capture trigger.

A context hook auto-retrieves the top-k relevant notes each turn and prepends them to the conversation. It is bounded to top-k, never the whole store.

Memory aggressiveness

Set memory.aggressiveness to choose the amount of automatic recall and maintenance. The default preserves the behavior described below. Explicit reSearch, autoCapture, and dream switches override the selected level.

  • low: inject one high-confidence note; keep capture, re-search, and dream off.
  • default: inject up to three notes above the 0.35 score floor; failure memory stays on, while durable capture and maintenance remain opt-in.
  • high: inject up to five notes, enable boundary capture, re-search after compaction, and gated dream consolidation.
  • max: inject up to eight notes from a wider candidate pool, use a lower relevance floor, and allow dream consolidation after one hour and one session.

Every level remains bounded by the host's optional context-append budget and keeps validation, deduplication, namespace boundaries, failure-safe hooks, and the injection-time prompt-injection scan. The level does not change explicit memory tool searches or manual writes.

A human-facing /memory command opens a centered board for inspecting and pruning stored notes. Bare /memory shows the most recent notes, compact and newest first; /memory <query> runs the same semantic search as the tool. Use a or the add memory row to save a note from the board. /memory forget <id> deletes one as a direct fast path. The command and the tool share one store closure, so they never diverge.

Where notes live

Notes are embedded with bge-small-en-v1.5 (384-dim) via fastembed and stored in a single sqlite-vec file at ~/.sypi/memory/memory.db, opened through Node's built-in node:sqlite and keyed by a per-project namespace plus a shared global bucket. Nothing is ever written into the project repository.

How a note is ranked

Retrieval does not use raw cosine alone. A vector candidate pool is reranked by a fully local hybrid score: 0.7·vector + 0.3·BM25. BM25 runs in memory over the candidate set and adds no dependency. The product is multiplied by a per-source weight, so project notes edge out the shared global bucket, and by a session-scoped recency decay with a 7-day half-life on project notes. global facts are long-lived and undecayed.

On top of that sits a write-time importance nudge, + IMPORTANCE_BONUS·(importance - 0.5). The term is additive and centered on the neutral default, so a fact rated above neutral is lifted, one below is demoted, and an unrated fact contributes exactly zero and keeps its prior order. Importance is rated once, at write time, inside the same cheap-LLM capture pass, so it costs no extra call and retrieval stays fully local.

Injection keeps only notes above a 0.35 floor. A growing store therefore yields fewer, more-relevant memories per turn, at zero per-turn token cost. An opt-in MMR diversity rerank (λ=0.7, off by default) drops near-duplicates when requested.

Post-compaction re-search (opt-in memory.reSearch)

A lossy compaction summary can drop facts that remain in the durable store. When enabled, a session_compact handler runs the same local retrieve() against the fresh summary. The summary becomes the leading context. The handler injects recovered notes through pi.sendMessage for the next turn.

It fires only at compaction time, never per turn, reuses the existing retrieve machinery, and adds nothing to the default per-turn path. This half lives entirely in sypi-memory. It never imports sypi-compaction, which owns the separate segments half.

Boundary auto-capture (opt-in memory.autoCapture)

The store previously grew only when the model called op:add. Boundary capture adds the other write path.

A rolling buffer records the transcript that the context hook assembles each turn, at zero extra token cost. At a session boundary, either session_shutdown or session_before_compact, it extracts the transcript before context is discarded. It then runs one cheap-LLM pass through the host's bounded query seam (ctx.cheapQuery, the cheapest currently-authed provider) to distil a bounded list of durable facts: project conventions, commands, decisions, stable preferences. It skips ephemeral chatter.

The model output is UNTRUSTED. It is fence-stripped, JSON-parsed, Typebox-validated, and hard-capped at 12 facts of at most 240 characters each before anything is stored. Each survivor is embedded once and semantically de-duplicated against the target namespace. A cosine score at or above 0.92 means that the fact is already known, so it is skipped. This also catches intra-batch repeats. The remaining facts are written through the same bge-small and sqlite-vec store with the project or global scope.

Every failure path is a safe no-op. This includes an unavailable authenticated provider, a timeout, malformed output, and an empty transcript. Capture never breaks shutdown or compaction.

Dream consolidation (opt-in memory.dream)

Capture grows the store. Dream curates it as volume increases. A gated session_shutdown pass clusters near-duplicate facts, merges each cluster into one consolidated fact with an optional topic heading, re-embeds the merged text through the same bge-small store, and prunes the originals.

Two gates keep it cheap and rare, and both are required: at least minHours of wall-clock (default 4) since the last run, and at least minSessions sessions (default 3). A lock file under ~/.sypi/memory/ with a 30-minute stale-takeover TTL means two passes never overlap, and a crashed run self-heals.

The clustering call goes through the host's bounded query seam (ctx.cheapQuery). Its output is UNTRUSTED. Before anything is written, it is fence-stripped, JSON-parsed, and Typebox-validated. Cluster and member caps, in-range fact indices, a one-merge limit per fact, and a merged length limit of 240 are enforced.

No data loss is invariant. A merged fact is written forward before its originals are forgotten, so a crash mid-merge leaves a harmless duplicate and never a hole. A "cluster" of fewer than two known facts is dropped, so a lone fact is never deleted. Cross-namespace clusters (project to global) are skipped.

Provenance (op:why)

Every write stamps a source on the fact: a trigger (manual for a tool add, capture:shutdown or capture:before_compact for the boundary capture pass) and the sessionId, which is the session file's basename, alongside the write timestamp. op:why <id> reports that origin before a recalled fact is acted on. It serves ask-never-assume and feeds /doctor introspection.

The importance and source columns are back-compatible. An older DB is migrated with ALTER TABLE ADD COLUMN on open. Older rows read back with no importance, treated as neutral, and unknown provenance. Nothing in the prior schema breaks.

Failure memory (always on)

Alongside durable facts, the extension remembers durable mistakes. A tool_result hook logs each tool call's normalized command and whether it errored (isError). At the same session boundary the capture pass already owns, it deterministically pairs each failed command with the fix that followed (the next successful call of the same tool) and stores a terse lesson keyed by a hash of the failing command in a distinct JSON store (failures.json, project-namespaced). The vector facts are untouched.

When that exact command recurs in a later session, a tool_call hook injects one short Reflexion-style hint through pi.sendMessage: "this repeats X, which failed before; last time Y worked". It is deduped to once per command per session and never blocks the call. A novel command injects nothing.

Standing cost is about zero. Detection is deterministic and uses no LLM. Each call normalizes the command, computes a sha256, and checks a Map. Every path is best-effort. A corrupt store is treated as empty rather than throwing.

Injection-time prompt-injection scan (always on)

Recalled text enters model context, so poisoned memory can act as a prompt-injection vector. Every recalled entry from the per-turn context hook, post-compaction re-search, or op:search is scanned by scan.ts just before rendering, against a small and deliberately tight pattern list: instruction-override, role-impersonation, exfil and C2 shapes such as curl-pipe-sh and secret-carrying URLs, and unicode obfuscation (bidi controls, zero-width smuggling, Latin/Cyrillic homoglyph mixing).

A hit is replaced by a [BLOCKED: <class>] placeholder and the event is appended to ~/.sypi/memory/blocked.log. The on-disk store is never modified, and the human-facing /memory surface still shows the raw note so it can be inspected and forgotten.

The bias is conservative. The patterns may miss an exotic attack to avoid blocking a legitimate memory. Code snippets, URLs, documentation placeholders, and non-Latin prose therefore pass.

Testing and seams

The store and the embedder are dependency-injected, so the whole surface - tool, hook, capture pipeline, dream consolidation, gate and lock lifecycle, real cosine KNN, provenance and migration, failure pairing and recurrence hints - is tested without a model download, using a pure lexical hashEmbedder and a fake cheapQuery. The real model downloads once on first use at runtime.

The package imports only @synthryn/sypi-coding-agent/extension-api and @synthryn/sypi-tui: the host's ctx.cheapQuery bounded-query host, the ctx.readScopedSettings effective-profile reader, the extensionStateDir/projectStateKey state paths, the mountBoard/clampLines/getSelectListTheme UI helpers, and the tool-card helpers. sqlite, vec0, and onnxruntime probing is owned by the package (natives.ts). Runtime deps: fastembed, sqlite-vec.

Optional local embedder + reranker (research + gated scaffold)

Status: off by default, adds no new hard dependency. fastembed is "local" only after it downloads the bge-small ONNX model once from a CDN on first use; that initial fetch is the network dependency. A gated seam lets a fully-network-free local embedder be swapped in without sypi taking on a required dep, and without risking an 8GB machine's RAM. The seam is resolveEmbedder(opts?) in embed.ts: with no opts (the default) it returns the existing fastembed embedder and does zero extra work; with opts it tries a gated local path and falls back to fastembed on any miss. It never throws: an absent peer, an oversized model, a build error, or a wrong vector width all resolve to a clean fallback, never a crash and never a silent wrong-dimension write.

Two hard gates protect the ceiling. (1) RAM footprint: localEmbedderFits() requires the model's estimated resident size to be under an absolute cap (LOCAL_EMBEDDER_MAX_FOOTPRINT_BYTES = 512 MiB, so a large 1B+ embedding model is always rejected) and to have 2x its footprint free right now; the check is deliberately conservative because falling back is always safe. (2) Vector width: the local model must produce the same 384-dim vector as the on-disk store, or it is rejected (a different width would corrupt the sqlite-vec table). Availability is feature-detected: the peer is dynamically import()-ed inside a try/catch and its shape validated before use.

How to opt in (user's choice, not shipped). Set memory.localEmbedder in your profile to true (defaults: peer module sypi-local-embedder, 256 MiB footprint) or to { "module": "<specifier>", "footprintMb": <n> }. The named module must export createEmbedder(): Embedder (a tiny library-agnostic shim you provide, wrapping your chosen runtime). Absent the module, the import fails and the store keeps fastembed. The key is declared in the core profile schema and read defensively at runtime.

Compatible peer options for an 8GB machine (budget ~1-1.5 GB for the model layer):

  • Same model, fully offline: bge-small-en-v1.5 (384-dim) via Transformers.js (@huggingface/transformers + onnxruntime-node). ~150-300 MB resident, 384-dim so the existing store is unchanged and the width gate passes.
  • Higher quality, needs a store migration: nomic-embed-text-v1.5 (768-dim, ~275 MB Q4) or mxbai-embed-large (1024-dim, ~670 MB) via node-llama-cpp (GGUF, Metal-accelerated). Their width isn't 384, so the seam rejects them until the store is rebuilt at the new dim.
  • Reranker: the dependency-free BM25 hybrid (score.ts) is the default. A local cross-encoder (bge-reranker-base, ~280 MB, via Transformers.js) is a real quality gain but roughly doubles resident model memory alongside an embedder, so enable it only behind the same footprint gate and only as a small/int8 variant.

fastembed stays the default. It is already a local ONNX bge-small embedder: the only "network" step is the one-time model fetch on first use, after which it runs fully offline, so it needs no external app and no extra dependency. Ollama is ruled out on purpose: requiring users to install and run a separate app is a line this setup won't cross. The gated resolveEmbedder(opts) scaffold above ships no new dependency: it is strictly opt-in and does nothing unless a user wires their own peer module.