Skip to content

NMOS v0.3.0

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 05 Oct 13:33
· 207 commits to main since this release
0ace76c

The second milestone release (docs/ROADMAP-1.0.md): Stage 6, verification and repair, is complete (canon sources,
owner repairs that survive a new extractor, export and restore), with the recall and extraction work since 0.2.0
(Phase 14 step 6, Phases 15–31; the corrections of AGE-24). The owner asked for this release on 2026-10-05, after the Phase 31 reduced live gate met every
bar: on the long synthetic chat, questions about now at turn 240 went from 18–19 to 23 of 25, the full-history set from
19 to 23, and the lent-book set from 5 to 6 of 6 (docs/perf/phase31-live-gate.md).

What changes, in short:

  • Memory knows what changed (packet-v12): an older excerpt of a replaced value (where someone lives, what someone
    is called) and an ended role stay out of a question about now; a question about the past keeps them.
  • Roles and names (extract-v16): a role the story ends is ended; a character written in full and by part of the
    name is one person.
  • A chat connected for the first time is read in story order, so each turn sees what came before.
  • Recall finds more of the answer: keyword recall, "how it started" questions, given and romanized names, excerpts
    that land on the answer and grow with a 4,000-token default budget.
  • Run without Docker: bundles for Windows x64, macOS arm64 and Linux x64/arm64.
  • Move or back up memory with NMOS Archive (export and restore).
  • Repair with a preview, and repairs that survive a new extractor generation (or are listed when their item is gone).
  • See the cost: model-call usage per chat, /dashboard with the version and recent job errors.
  • Google Vertex AI with the key file alone.

Generations and schema that change:

  • the extractor extract-v16 (extract-v14 and extract-v15 were never released);

  • the default packet policy packet-v12 (from packet-v8: packet-v9 to packet-v12);

  • canon facts are a new generation (read again once);

  • migrations 0027–0028.

  • Recall knows what changed: packet-v12 is the default (ADR 0066, D75; Phase 31, AGE-24, AGE-37). An older
    excerpt that states a value the story has since replaced (where someone lived, what someone was called) is left out
    of a question about now, and so is a role the story ended; a question that asks about the past ("전에", "예전에",
    "before", "used to") keeps both. The excerpt of a long message now centres on the sentence that also holds the
    question's one-letter words (책, 집, 돈), so "이안이 빌려준 책 제목이 뭐였지?" reaches the title (K43). Measured on
    the live gate's long synthetic chat: the questions about now at turn 240 went from 18–19 to 23 of 25. Nothing is
    extracted again; NMOS_PACKET_POLICY=packet-v11 keeps the previous packet.

  • Keyword recall finds its words again on a live chat (ADR 0052). A keyword's lookup has 25 ms; Postgres often
    planned it from the chat's messages, checking each one, instead of from the trigram index: after a reroll, an edit or
    a deletion, because its statistics did not know the new version of the chat yet, and on a connection that had run the
    lookup five times, because a prepared statement is planned without its word. That took longer than 25 ms, so the word was dropped as if it were
    too common (in the measurement runs, most requests' keyword route ended as too_broad), and a replay could find what
    its request had not (ADR 0027). The lookup now always starts from the trigram index. A word whose own matches take
    longer than 25 ms to check (long messages) is still dropped, as before.

  • extract-v16 is the default extractor (ADR 0064 accepted, D73; Phase 28 step 3, AGE-24). It shows the model the
    roles in force and closes the one the story ends, and links a character the story writes in full and by part of the
    name, each confirmed once more before it is stored (Phases 28 and 29). Updating re-extracts every chat's recent turns
    once (a new generation, ADR 0014), oldest first; until a turn is re-extracted, the previous extractor's facts still
    serve it. NMOS_EXTRACT_COMPILER=extract-v15 keeps the earlier extractor. extract-v15 was never released, so 0.3.0
    users re-extract once either way. Each of its rules comes only with the list it is about (ADR 0064 item 5): on
    turns with no role or name pair to check, its prompt is close to extract-v15's, which kept new roles and first
    events it had been missing.

  • A chat connected for the first time is read in story order (ADR 0065, D74; Phase 30, NMO-36). NMOS used to
    extract a new chat's recent turns newest first, so each turn was read before the turns that came before it: it could
    not see who was already known, which promises and secrets were open, or which roles held, and a later turn's ending
    or second name for someone was missed. The turns are now read oldest first, and a turn you write meanwhile waits
    behind them. The newest turns are still in the chat's own prompt, and their messages are searchable at once.
    Nothing is extracted again; chats NMOS already knows are unaffected. A secret found out within those turns is now
    matched when they are first read, so K29 (a missed reveal) no longer needs "Extract all history" there.

  • Google Vertex AI: pick the key file, and the model list works (ADR 0022 amendment 1). The fact-extraction LLM
    section shows Load key file for a Vertex endpoint: pick the service-account JSON key file instead of pasting it,
    and the endpoint's project is filled and the model list loads at once. Load models now lists the Gemini models
    Vertex serves (google/…) from Vertex's publisher-model catalog, since its OpenAI-compatible endpoint has no
    /models. Pasting the key works as before.

  • extract-v16, an extractor to measure, off by default (Phases 28 and 29, ADR 0064 proposed, D73; AGE-24, NMO-35).
    The owner's live run found a role kept as current after the story ended it, and one character split in two by its
    full name and its given name. NMOS_EXTRACT_COMPILER=extract-v16 shows the extractor the roles in force and asks it
    which one the story ends; each ending is asked once more about that one role before it is applied, and an ending the
    model calls planned, or the other direction of an ending, is held for you rather than applied. It links a character
    the story writes in full and by part of the name (윤하나 and 하나; Elena Vance and Elena) unless the two could be
    different people, and an alias whose two names are both in the turn is asked once more too, so a nickname one
    character calls another no longer becomes the speaker's. A name counts as written only as a word of its own (람이 is
    not found inside 하람이). The Inspector's "Needs attention" lists the role endings it applied recently and the ones
    it held, and the aliases it held, each with one action (undo, apply, or link the two names as one person).
    Selecting it re-extracts every chat once; extract-v15 stays the default, and without the setting nothing changes.

  • A model reply with a trailing comma is read (#251). A reply such as {"a": [1, 2,],} was rejected and its turn
    retried; at temperature 0 the retry wrote the same comma, so the turn was never extracted. A reply that fails to
    parse is now read again without a comma that ends a list or an object (outside strings); every other reply parses
    exactly as before. This applies whichever extractor you use.

  • The query is embedded while recall reads, so a slower embedder still gives vectors (ADR 0061, D70; K34, AGE-24).
    A request used to give the embedding of your message 300 ms on its own, after lexical recall and before the facts
    were read: an embedder behind a proxy, or a busy one, missed it, and 70 % of the owner's production requests found
    memory by shared words only. The embedding call now starts when recall starts and runs while the state, facts,
    threads, cast and summaries are read; the request waits for it at most 300 ms after those reads. The embedder so
    has the reads' time as well (≈100–300 ms on a long chat), a request whose embedder answered during the reads is
    faster than before, and none waits longer for the embedding than before (a request that now finds vectors spends
    what a request with vectors always spent: the search and a fuller packet). And since the plugin syncs your message before it asks for the packet,
    the sidecar now starts the embedding of that message the moment the sync delivers it and hands it to the request
    that follows: on a long chat the sync alone outlasts a slow embedder's call, so that request waits for nothing. The timeout's meaning for you is unchanged (NMOS_EMBED_TIMEOUT_MS:
    what a request may wait for the embedding); the call itself may run twice that long and ends on its own. Fail-open
    and the Status tab's notice are as before; the trace now records embed_wait. Recorded requests replay as they were.
    A cold model (an unloaded Ollama) still gives no vectors to the request that wakes it. Measured:
    docs/perf/query-embedding.md.

  • packet-v11, the excerpt that lands on the answer — the default (Phase 27, ADR 0063, D72; AGE-31). A message
    found by your words and by meaning is excerpted from the part the meaning found, instead of around the sentence
    with most of your words, and a why or contents question gets the whole of a short-sentence explanation (up to 320
    characters) instead of four sentences; the sentence an excerpt starts from is chosen by your question's keywords
    alone. Measured on the owner's chats by replay (docs/perf/answer-span.md): the main chat's questions that need
    memory 16 → 19 of 23, forbidden phrases down over twelve sets, no set worse by more than one case; packets at most
    0.11 % larger on one set and smaller on another. NMOS_PACKET_POLICY=packet-v10 keeps the previous policy. Traces record a new recall
    option, excerpt_anchor; recorded requests replay as they were.

  • The chunk cap is a setting (ADR 0062, D71; K13). Semantic recall embeds at most the first 5,600 normalized
    characters of a message (8 chunks of 700). The cap is now NMOS_EMBED_MAX_CHUNKS (default 8, as before: nothing is
    re-embedded on upgrade) and belongs to the projection: a changed cap is a new projection, under which every chat is
    embedded again once (local, in the background; the recent messages first). Raise it for chats whose messages are
    longer than that once normalized. The owner's long chats are not — their messages normalize to under 5,500
    characters, and a cap of 24 compiled the same packets on the M0 evaluation copy — so nothing changes for them.
    Nothing else about recall changes.

  • NMOS without Docker (Phase 23, ADR 0060; AGE-29). The release carries a bundle for each system PocketRisu ships
    a portable package for: NMOS-v<version>-win-x64.zip (double-click NMOS.exe; NMOS sits in the notification area),
    -macos-arm64.dmg (a menu-bar NMOS.app, signed ad hoc: allow it once with Open Anyway), -linux-x64.tar.gz and
    -linux-arm64.tar.gz (start.sh). Each holds PostgreSQL 16 with pg_trgm and pgvector, Python, the sidecar and
    the plugin; nothing else is installed, and the plugin's URL is the same as with Docker. The tray and the menu bar
    show the status, copy the URL, open the dashboard and the plugin and log folders, start NMOS at login, and quit
    (stopping the database too). To update on Windows/Linux, unpack the new version, move data into it and copy the
    existing .env too (macOS: replace the app; data and .env stay in Application Support; README "Without Docker").
    The Docker install is unchanged.

  • /dashboard: the Inspector's first page, now with the NMOS version and the last failed or retrying background
    jobs with their errors (Phase 23 step 5). Read-only, behind the sidecar's token like the Inspector.

  • A role between two people is remembered as such: extract-v15 (Phase 25, ADR 0059, D69,
    docs/perf/extract-v15.md; AGE-27). Who rents from whom, works for, teaches or looks after whom is now its own fact,
    role_toward, one per direction, beside the pair's relationship, which now means a personal tie (kin, romance,
    rivalry, friendship). A turn that says where someone lives and the role they hold there gives both. The role counts
    as how two characters stand: it comes first among facts of equal mention, opens the packet's facts and names what
    it replaced; canon may state it; the Inspector's Relationships section has a Role column. On the owner's main M0
    chat the question about a tenant, answered before only by a lucky excerpt, now finds the fact (one case more).
    A new extractor generation: each chat's recent turns are extracted again once and its canon sources read again
    once (provider cost); it replaces the unreleased extract-v14, so 0.3.0 moves from extract-v13 to
    extract-v15. An extraction reads about 3.5 % more input and writes 5.5 % more output than with extract-v14.
    Recorded requests replay as they were.

  • A story fact extracted just before a request is served on every system. A live request reads the story's facts
    up to its own message with no time to read at, and took "now" from the sidecar's clock while extractions are
    stamped by the database's: where the sidecar's clock lags (Python 3.12 on Windows, the Docker-free Windows bundle of
    Phase 23), a fact extracted moments before was left out, and the request's replay held it. It now follows the
    database's clock, as canon facts do since the previous fix; a canon change with no observation time from the plugin
    is stamped by the database too. Recorded requests replay as they were.

  • A character called by the given name alone, or in Hangul when the story spells the name in Latin letters, now
    brings their memory
    (Phase 24, ADR 0058, D68; AGE-28). "이안은 어디 있지?" now counts as a mention of 백이안
    (a three-syllable name with a common family name), and "하진은?" of a character the story calls Hajin (a fixed
    Hangul-to-Latin spelling key, with the usual family-name spellings such as Lee and Park). The persona gains no such
    name, and a name another character holds or two would share counts for no one. Where a secret is kept from a
    character addressed that way, it is still ranked and kept private as when the full name is used. The scene's
    character groups now list the characters your message names first. Recorded requests replay as they were.
    Phase 24 complete (docs/perf/name-variants.md): on a synthetic chat three more "at first" questions asked by the
    given name pass (8 of 15), and on a copy of production more questions in Hangul about characters the story names in
    Latin letters find their memory (10 of 24 and 2 of 6, from 8 and 0); every other set unchanged; no measurable
    latency.

  • A canon fact read just before a request is served on every system. A request that does not replay an earlier
    one took "now" from the sidecar's clock to choose the canon facts in force, while those facts are stamped by the
    database's. Where the sidecar's clock lags (Python 3.12 on Windows ticks every 15.6 ms, as in the Docker-free
    Windows bundle of Phase 23), a fact extracted moments before was left out, and the request's replay then did not
    reproduce it. The choice now follows the database's clock, as summaries already did; the name-join preview stamps
    its link as the join would (the database's now()). Recorded requests replay as they were.

  • A relationship's or feeling's earlier version kept from someone is no longer shown as known to all (ADR 0038
    amendment 1, K42). Since packet-v5 a relationship, feeling or form of address line also names what it replaced
    and how it started, under the current version's knowledge marks; a version only some characters knew read as known
    to everyone. Such a version is now named only when the current one has the same marks. The Inspector still shows
    every version; recorded requests replay as they were.

  • Phase 21 complete: "at first" — how it started, when you ask (ADR 0056, D67, docs/perf/first-cue.md; AGE-26). A message that asks how
    something started (처음, 맨 처음, 최초, 예전, 옛날, 원래, 초반, 첫 만남, "at first", "originally", …) now gets the oldest of
    several similar events of the characters it names, not the newest, also small ones; and a pair's first form of
    address or relationship even when the chat window holds the current one. Other messages are unchanged, and recorded
    requests replay as they were. On the owner's M0 chat one more case that needs memory passes (7 of 10), and on a
    copy of production 7 of 8 "at first" questions (6 before); no measurable latency.

  • Phase 22 complete: a re-extraction that keeps what it found (ADR 0057 and its amendment 1, ADR 0044 amendment 3, D66,
    docs/perf/reextract-loss.md; AGE-25). "Extract all history" no longer extracts again a turn that was extracted
    before an earlier turn's secret (K29): it keeps the turn's facts and asks one short model call whether a character
    found the secret out there. On the owner's M0 chat the 68 turns one press re-extracted cost 727k input and 85k output
    tokens and dropped the answer to "what does A do?"; their checks took 324k and 1.5k, discarded nothing, and the
    answer stayed. Facts a re-extraction dropped (the same model, extracting a turn again, did not state them again
    and the chat holds them nowhere else) are listed under "Needs attention" with Restore, and counted on the
    panel's chat card; Restore puts the fact back as yours at its turn, Undo takes it back. Nothing is restored on its
    own. New API: GET /v1/conversations/{id}/dropped, repair kind fact_restore. Migration 0028; a new plugin build.

  • An owner's repair survives a new extractor generation (ADR 0044 amendment 2, docs/perf/repair.md). A repair now
    also stores its item's quote of the chat, and finds a re-extracted item by that quote when the new generation words
    it differently or states it at another turn. On a copy of the owner's chats re-extracted with extract-v14, 40 of
    68 repairs made on extract-v13 found their item again (29 before); none whose item the new generation still quotes
    was lost. Repairs made before this change have no quote and match as before; one that matches nothing is still
    listed under "Needs attention".

  • Phase 20 complete: a name join shown before it is made (ADR 0055, D65, docs/perf/join-preview.md). Joining
    two names, splitting them, or undoing either now shows first what changes in the chat's memory: which current
    fact replaces which (the story's order decides), facts that merge, a relationship, promise or thread of a character
    with themself, a secret kept from someone who holds it, repairs whose match changes, conflicts, the persona and
    canon names; "nothing changes" when nothing does. The action is made from the preview and refused (a fresh preview)
    when memory changed since. Undoing a join offers to re-extract the turns extracted while it held, off by default,
    one extraction call a turn. Also fixed: a rebuild's or re-extraction's job that the model was still answering no
    longer stores its old result. New API: …/entity-links/preview, …/entity-links/{id}/remove/preview,
    …/entity-links/{id}/reextract, …/repairs/preview, …/repairs/{id}/remove/preview, and expect on the join,
    a split and both removes. A new plugin build.

  • Phase 19 complete: extract-v14 (ADR 0054, D64, docs/perf/extract-v14.md), a new extractor generation. The
    model sees each previous turn's messages up to 1,000 characters instead of 2,000, so an extraction reads about a sixth
    fewer input tokens; on sampled turns it found more of a synthetic chat's facts (115 against 106–109 of 167). The
    prompt's examples are synthetic. A fact whose quoted evidence (12 characters or more) is not in its own turn is kept
    as pending, "evidence not in the turn", and never injected (0.2 % of deepseek-v4.1-flash's facts and 2.7 % of
    gemma4:31b's when the evaluation chats were extracted again). Upgrading re-extracts each chat's recent turns
    (NMOS_EXTRACT_BACKFILL, default 100) once at your provider's cost, and reads each chat's canon sources once more;
    older turns keep their facts until Extract all history. Known issue K41: re-extracted with deepseek-v4.1-flash,
    the owner's evaluation chats passed two fewer memory cases on three of four runs (gemma4:31b within one).
    tools/eval_extract_sample.py compares extractor prompts on sampled turns.

  • Phase 18 complete (docs/perf/lexical-recall.md "Step 5"). Replayed on the owner's recorded requests (read-only,
    counts only), packet-v10 with keywords placed excerpts in 10 of 34 requests that had none without vectors, and no
    secret or thread that packet-v9 would not. Known issues K39 (a grown excerpt can carry a value the story has since
    replaced) and K40 (a keyword of two or three syllables is not found where a particle is attached to it).

  • Phase 18 step 4: packet-v10, excerpts that grow (ADR 0053, D63), the new default. An excerpt starts from the
    sentence holding most of your message's keywords and adds its neighbouring sentences, after then before, up to four
    and within the length the memory budget gives it; before, it was always two sentences (about 70 characters) whatever
    the budget. On the owner's chats without vectors it answered 6 more questions needing memory, and with vectors placed
    2 forbidden phrases instead of 6. On the synthetic chat, whose facts change often, it places values the story has
    since replaced more often than packet-v9 (17 against 12 with vectors, 12 against 5 without; none a secret), though
    far less often than growing to the whole length (29). Requests recorded before replay with packet-v9.

  • Phase 18 step 3: keyword lexical recall (ADR 0052, D62). Lexical recall also looks up the message's keywords one
    by one (up to four; Korean particles and question endings taken off, names kept whole, question and stop words
    left out), so a natural question finds the message that says what it names even when the question as a whole is
    too unlike it, and without vectors. A keyword in most of the chat is ignored. An excerpt only this route found is
    left out when it repeats a secret still kept from someone (owner, 2026-09-30). Requests recorded before this version
    replay without it. The trace shows keyword_mode, each candidate's keyword_score and keyword_withheld.

  • Phase 17 (what NMOS's own model calls cost, and fallbacks told from failures) is complete
    (docs/phases/PHASE-17.md, docs/perf/model-usage.md): the recorded tokens equal the provider's on a hosted model
    and on local chat and embedding models; generation latency is unchanged; archives from before carry over, their
    usage "not recorded".

  • Phase 18 step 2: recall measured by what lexical recall finds and how long excerpts are
    (docs/perf/lexical-recall.md). The evaluation tool says per case whether lexical recall found anything and how long
    each excerpt was, and marks runs without vectors. Fixed in the tool: an older request's prompt window read empty once
    the chat had moved on (so answers the prompt already held counted as memory's), and a probe could not replay after
    the request's own message was deleted. Nothing changes in what a request recalls.

  • Phase 17 step 4: the progress display says how memory was served and what background work made. "✓ Memory
    injected (… chars) · lexical only" when recall had to go without vectors (the embedding model did not answer in
    time, K34), "· reused" when a reroll reused the packet already built; neither is shown as a warning. When
    background work finishes it says what it added ("✓ facts +3 · summaries +1"), or "✓ Processing done" as before. A
    new plugin build.

  • Phase 17 step 3: what a chat's memory cost, shown. A conversation's Inspector page has a Model usage
    section: per generation (fact extraction, canon reads, summaries, embeddings) the model calls NMOS made and the
    input, output and cached input tokens the provider reported, with how many calls reported them. The panel's
    "This chat" card says the chat's totals in one line. No prices. A new plugin build.

  • Phase 17 step 2: what NMOS's own model calls used (ADR 0051, D61, migration 0027). Each extraction, canon read,
    summary and embedded chunk now keeps the tokens its model call used, exactly as the provider reported them (input,
    output, cached input and reasoning tokens, when reported), with the call's duration and model. Nothing is estimated:
    a provider that reports nothing is recorded as such. Rows from before this version show nothing. Shown in the
    Inspector and the Status tab in a later step.

  • Fixed: a reroll could reuse a packet built for another sidecar or with old canon (Codex security review of
    Phase 16 step 3). The plugin keeps a packet for 10 minutes for the same chat state; it was reused after
    sidecar_url, the route, the token or the memory budget changed (the new sidecar was never asked), after a lorebook
    entry, the card, the persona or the author's note changed or the chat was bound to another persona without a new
    message, and for a prompt of the same length holding other messages. These are now part of the key (the token
    hashed), and a packet built with other canon (its manifest or what the prompt held) is not reused. A panel action (a
    delete, a repair, a settings save) now empties the cache also when its answer failed or came late, since the sidecar
    may have applied it, and a request that was fetching memory meanwhile goes without it rather than cache or inject a
    packet from before. A new plugin build; nothing changes in the sidecar.

  • Phase 16 (Stage 6, part 3: export and restore) is complete (docs/phases/PHASE-16.md, docs/perf/archive.md):
    on copies of the owner's two measured chats an archive restored into a fresh database (migrating it one level on
    the way) held every table the same, compiled every recorded request the same, and gave the same M0 and secret-gate
    results and the same rebuild. A production-sized install exports in about 3 s (15 MB with embeddings, 2 MB without)
    and restores in about 3 s. Both Export buttons were smoke-tested on PocketRisu v1.13.0. On an iPhone, Export everything saved its file,
    but PocketRisu may then show its own "server has been updated" alert, which is safe to dismiss (K38).

  • Phase 16 step 4: restore (ADR 0050 amendment 1): python -m nmos_sidecar.archive restore FILE (or - for
    stdin; --check to verify only), with the sidecar and worker stopped. Every file's size and hash is checked before
    anything is written; an archive from a newer NMOS, or a chat this install already holds, is refused (nothing is
    merged); an archive from an older NMOS is upgraded as the upgrade would. Ids and timestamps are kept, so recorded
    requests replay as before (with embeddings in the archive when they used vectors). Settings already set here stay.

  • Phase 16 step 3: export (NMOS Archive, ADR 0050, D60). Export everything in the panel's Settings tab and
    Export this chat on a conversation's Inspector page save a .nmos.zip: every conversation's history, canon,
    your repairs and links, the recorded requests, and by default the model's extractions and summaries (embeddings when
    asked); the whole install's archive also holds the settings, never an API key or the token. The same from
    GET /v1/archive and python -m nmos_sidecar.archive export. NMOS refuses to write an archive in which it finds a
    key or the token. A new plugin build; no migration. Restoring comes in step 4.

  • Phase 16 step 2: the plugin's frame can save a file (H21, docs/HOST-FACTS.md): on PocketRisu v1.13.0 a Blob
    saves from the panel (Chromium, Firefox) and nativeFetch carries a binary body whole on both routes.

  • Fixed (security): a narrator's name could break out of the packet's Note (ADR 0035). With a first-person
    narrator set for a chat, the narrator's display name went into the packet's <Note> unescaped, so a name holding
    markup (</Note><Fact>…) could add elements of its own to the system-role packet. The Note's text is now escaped
    like every other line. A name without <, > or & gives the same packet as before, so recorded requests still
    replay. No migration, no plugin change.

  • Fixed: canon facts from a branch the host never shows (ADR 0047 amendment 2, H20). A card or lorebook text
    with {{#if …}} / {{#when …}} blocks was read with every branch, and its facts were sent as memory even when
    the chat's variables hid them (a language or a display switch, for example). A canon fact quoted only inside such
    a block is no longer served; nothing is re-extracted. No migration, no plugin change.

  • Phase 15 (a packet that fills its budget) is complete (docs/phases/PHASE-15.md, docs/perf/packet-fill.md):
    on the owner's two chats with both extraction models, packet-v9 at 4,000 answered 6 more cases needing memory than
    packet-v8 at 2,000, with 2 more forbidden phrases (one old excerpt); on 38 recorded requests it added only facts,
    claims and excerpts; retrieve latency unchanged at 10,000 messages. Fixed: after saving a budget above 8,000 the
    panel kept showing the typed value. tools/bench_story.py takes BENCH_BUDGET and BENCH_RECALL=wide.

  • Phase 15 step 4: memory recalled without vectors is no longer silent (K34). On the owner's production 70 % of
    recalls went without vectors because the query's embedding took longer than 300 ms. The retrieve answer now says
    whether vectors ran (vectors: on, off, fallback), and the panel's Status tab says so after a request that fell
    back, with what to do. NMOS_EMBED_TIMEOUT_MS is documented. A new plugin build.

  • Phase 15 step 3: a packet that fills its budget (packet-v9, ADR 0049, D59), the new default policy: above
    2,000 tokens the excerpt count, each excerpt's length and the fact limit grow with the memory budget, from your own
    settings (up to 4× excerpts and 2× facts at 8,000; the added fact slots only for facts kept from no one); threads,
    events, secrets, <Private>, <Cast> and <Story> do not. At 2,000
    and below it is packet-v8; recorded packet-v8 requests replay as they were. The plugin's default budget is
    4,000 (was 2,000): lower PocketRisu's max context by 2,000 more if you never set the budget. The panel saves up
    to 8,000 and the budget advice suggests up to 8,000; a larger stored budget still works and recalls as 8,000. A new plugin build; no migration, nothing re-extracted.

  • Phase 15 step 2: evaluations that cannot drop vectors silently (PHASE-15 Q6): tools/eval_rp.py --projection
    searches a named embedding projection (its recorded endpoint only when local, else --embed-url), warms the
    embedder, and reports per case whether vectors ran; a run that asked for vectors and lacked them exits with status 2.
    A replay (/v1/trace/{id}/replay, audit.replay) now says vectors: on, off or why it fell back. The packet-v8
    baseline with vectors is in docs/perf/packet-fill.md.

  • Phase 14 (Stage 6, part 2: canon sources) is complete (docs/phases/PHASE-14.md, 2026-09-29):

    • On both measured chats with their canon, M0 and the secret gate are unchanged case by case, and canon took 12 and
      66 model calls (26 on the owner's production chat), as the inventory predicted (docs/perf/canon.md).
    • Only relationships are listed as conflicts with canon (ADR 0047 amendment 1, owner decision): on real chats
      the listed identities were the same one in English and Korean, or two true descriptions. A story's identity
      supersedes canon's quietly; the owner can still lock a canon fact from its line.
    • The canon-facts query is prepared once per connection. Latency with a 200-entry lorebook read whole misses the
      +5 ms criterion; the owner accepted it (K36).
    • Fixed: the Inspector marked every canon fact "older generation".
    • Tools: tools/eval_rp.py and tools/eval_secret_gate.py take --canon, tools/bench_story.py BENCH_CANON=N;
      an upgrade fixture from Phase 13 main. No migration, no plugin change.
  • NMOS off for one chat (ADR 0048, D58): the panel's Status tab starts with a This chat card that turns NMOS
    off or back on for the chat open now, and the chat input's ☰ menu has NMOS: this chat off/on. A chat that is
    off sends nothing to the sidecar and gets no memory; what NMOS keeps of it stays. The list is the new plugin arg
    disabled_chats. The panel also opens from NMOS's icon in the sidebar's ☰ menu. A new plugin build; no migration.

  • A new icon: the 🧠 emoji is replaced by NMOS's own line icon (an N with a memory node) in Settings, the chat
    input's ☰ menu and the sidebar's ☰ menu. It takes the host's text colour. In the sidebar, which shows no name, the
    icon carries it for screen readers and as a hover tooltip. The progress display shows it before
    "Recalling memory…" only. A new plugin build.

Upgrading from 0.2.0

Back up the database first (README, "Upgrade, backup and rollback"; for a bundle, quit NMOS and back up its data
folder): migrations 0027–0028 run when the new sidecar starts, and a rollback needs the backup. NMOS Archive is for
moving memory, not for a rollback.

  1. Update the sidecar and the worker (docker compose pull, or the new bundle). At startup migrations 0027–0028 add
    model-call usage, reveal checks and dropped-fact restore.
  2. Model calls at the provider's cost (with an LLM configured):
    • extract-v16 becomes active, and each chat's latest NMOS_EXTRACT_BACKFILL turns (default 100) are extracted
      again once, oldest first. Older turns keep their earlier facts until Extract all history. Do not use Rebuild
      memory
      for an upgrade: it discards the chat's facts and extracts the whole chat again.
    • Canon is read again once.
    • Embeddings are kept (the chunk cap stays 8).
  3. Replace the plugin file (the host offers the update: //@version 0.3.0) and reload PocketRisu. The Status tab
    says whether the plugin matches the sidecar.
  4. Memory budget: the default is 4,000 tokens (2,000 before). Leave that much room in the host's context, or set a
    smaller budget in the panel.
  5. After extraction, look at Needs attention: a repair whose item extract-v16 no longer states is listed there.

Known limitations

  • Long chats: beyond about 10,000 messages, or right after editing an old message, memory can miss its deadline and
    the reply goes without it (K1–K4).
  • Names and facts can still be wrong; the owner repairs them. A persona narrated in the third person does not bring
    its own facts (K8, K32). A short Korean keyword with a particle attached is often not found (K40).
  • Replaced values: an excerpt can still carry a value no selected fact is about (K39).
  • Summaries have no input cap; a large lorebook adds latency, and a story that changes who someone is is not flagged
    against canon automatically (K35–K37).
  • iPhone: Export everything saves, but the host may show an alert (K38). Vertex's Load key file was checked in
    desktop Chromium only.
  • macOS bundle is signed ad hoc: allow it once at first launch.
  • The full list is docs/KNOWN-ISSUES.md.