Skip to content

NMOS v0.2.0

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Sep 14:42
· 303 commits to main since this release
b8f5347

The first milestone release (docs/ROADMAP-1.0.md): Stage 4, knowledge and secrets (Phase 10) is complete, and
main has since added Stage 5, the narrative engine (Phases 11–12), and most of Stage 6, verification and
repair
(Phase 13 and Phase 14 steps 1–5). The owner asked for this release on 2026-09-28. On the owner's real scenes,
48 replies of Opus 5.5 and Gemini 3.1 Pro with the new memory voiced no secret to a character it is kept from, and the
holders remembered it (docs/perf/secrets-eval.md).

What changes, in short:

  • Secrets: what is kept from whom, who found it out and when; a Private section; strict and first-person
    narrator modes per chat.
  • Open business and causes: promises, goals, questions, threats and debts are open until the story ends them;
    the stated cause of a feeling or an event.
  • Relationships: a relationship is one pair, both ways.
  • Summaries: scenes and the story so far are summarized in <Story>; each scene character's state goes in
    <Cast>.
  • The owner repairs memory: close or reopen a thread, retract or correct a fact, mark a secret found out, split
    two names, lock a fact; each with undo.
  • Canon: the card, the lorebooks, the persona and the author's note become sources of each chat. Lorebook keys
    give given names; the extraction model reads canon facts, which the story supersedes.

It also has the fixes of the 2026-09-26 and 2026-09-27 audits and a new text normalizer. The generations and schema
that change are:

  • extractor generations extract-v11 to extract-v13;
  • the text normalizer clean-v3;
  • the default packet policy packet-v8;
  • migrations 0021–0026.

The Phase 14 evaluation of canon facts on the measured chats (their model calls, M0, the secret gate, latency with a
large lorebook) and its real-host smoke are still to come (step 6).

  • Fixes from the 2026-09-27 audit (docs/proposals/ORIGINAL-VISION-TO-STABLE-2026-09-27.md, ADR 0033
    amendment 2):

    • A reveal no longer carries over to a different secret after the secret's turn is edited (G1). A character
      who found out one plan counted as knowing whatever plan that turn was edited into. A reveal now links by turn
      only while that turn reads as it did; otherwise it must match by content, or it is shown as unrevealed.
    • "Extract all history" now recovers a reveal missed when a chat was first connected (K29, G2): it extracts
      again the turns extracted before an earlier turn's secret, oldest first. Before, it skipped them.
    • A model answer without an assertions list fails the job (retried, then counted failed) instead of
      counting as a turn with nothing to extract (G3; in every release since v0.1.0-beta.1).
  • Phase 12 (Stage 5, part 2) is complete (docs/phases/PHASE-12.md, 2026-09-28):

    • M0 has 12 more owner-confirmed cases whose answers lie outside the prompt's own messages: 2 of 12 on Phase 11.
    • Scene summaries and the story so far (ADR 0042, migration 0023), written in the background by the extraction
      model for every 8-turn window and kept current through edits, deletes and swipes; the Inspector shows them. The
      prompt lists the secrets still kept from someone and says to leave them out; a summary that repeats one is held
      back when it is read.
    • packet-v8 (new default): <Story> and <Cast> (ADR 0043). The story so far and the scene the message is
      about, in at most 30 % of the budget (none for a first-person narrator); each scene character's place, condition,
      feeling toward the persona, what they carry and, when the message names them, open goals. On the owner's chat
      (restored copy) 5 of 12 answers outside the prompt window reach the packet, 2 before. A summary written before a
      secret it should keep is held and written again (ADR 0042 amendment 2); in front of a character a secret is kept
      from, a summary that comes near it is held too (amendment 3); a reworded secret can still pass (K30).
    • Summaries are on by default where extraction is on (NMOS_SUMMARIES=0 or the panel turns them off). On the
      first start every chat's due windows are summarized in the background: for the owner's chats, about 11 scene and
      2 story calls.
    • The Inspector shows why a summary is or is not used: the summary generation, each scene's state (current,
      held back for a secret with the secret named, not used while a character it is kept from is in the scene,
      changed and written again, queued, failed with its error), and a note when summaries are off or a chat has a
      narrator. A character's page starts with Current state, the lines <Cast> gives them and their open goals.
    • A long append no longer leaves scenes unsummarized (ADR 0042 amendment 4). A sync that made two or more
      windows due at once queued only the newest, and the story so far, which waits for every window, was never
      written. In play a sync adds one turn; it took a chat continued while the sidecar was down or the plugin off.
    • <Story> in a long chat costs a request about 10 ms instead of 180 ms (10,000 messages, 624 scenes). A
      request read every story the chat ever had and checked every scene against every secret; it now finds the
      current scenes and one story in one query and checks only what it would offer. The choice is the same.
    • Fixes from the review of #137 and #139: the Inspector shows the job that would replace a summary still in use
      (queued, writing, or failed with its error), such as a story behind the newest scene; and a request keeps the
      scene trigrams it read, so another request clearing the shared cache cannot drop a scene from its choice.
    • From the first cross-model review (Codex, AGENTS.md §14): after the turns of a whole window were deleted, a later
      scene summary, current now for an earlier window, was judged older than the prompt by the turns it was written
      at, so a scene the prompt no longer held could be left out. The request now uses the window it is current for,
      as before the step 7 rewrite.
  • Phase 14 (Stage 6, part 2): canon sources (docs/phases/PHASE-14.md, approved 2026-09-28; steps 1–5): the card,
    the lorebooks, the persona and the author's note as sources of each chat, host evidence first. Export and restore
    are Phase 15.

    • Step 2: canon host evidence on PocketRisu v1.13.0 (docs/HOST-FACTS.md, H19) and a count-only inventory of the
      two measured chats' canon (docs/perf/canon.md).
    • Step 3 (ADR 0045, migration 0025; new plugin build): each chat's canon is kept as the host shows it, with every
      version. The Inspector lists it in a "Canon" section, with how often a request's prompt held each text. The
      plugin sends it in the background when it changes and reads the card off the request path. Canon changes no
      packet yet.
    • Step 4 (ADR 0046): a lorebook entry's keys count as names of the one character they name. So a character called by
      a given name the lorebook lists is a mention (K31). On sample 2 the given-name probes found their fact 2 of 3,
      against 0 before.
    • Step 5 (ADR 0047, migration 0026; new plugin build): facts from canon. The extraction model reads the card,
      the persona, the author's note, and each lorebook entry once a prompt held it, in the background, each text once
      (NMOS_CANON_FACTS=0 or the panel's new switch turns it off). Canon facts are how things stand before turn 0: the
      story supersedes them from the turn it says something new. When the story changes who someone is or how two
      characters stand, "Needs attention" lists it with the choices: keep canon's (a lock, a new owner repair that
      keeps a canon fact or a correction current against later statements, with undo), keep the story's, or leave it.
      A canon fact whose text the prompt already holds is not sent again; a locked one is, when the story contradicts
      it. The plugin now recognises a card that uses {{char}} in the prompt, so it no longer reads the card again on
      every request.
  • Phase 13 (Stage 6, part 1): the owner repairs memory (docs/phases/PHASE-13.md, approved and complete 2026-09-28).

    • Step 2: the owner confirmed NMOS's lists for the longest chat (50 of 59 open threads ended, 6 secrets found out);
      on the previous main every M0 packet carried a thread the owner closed (docs/perf/repair.md).
    • Step 3 (ADR 0044, migration 0024): the owner can close a thread with an outcome or reopen one the story closed,
      and mark a secret found out by a character or keep one the story ended by mistake, through
      POST /v1/conversations/{id}/repairs. A repair is owner input: it survives
      rebuilds and new extractor generations, finds its target by what it says, and is listed in the Inspector, with
      undo. A repair whose target's turn was edited matches nothing and says so.
    • Step 4: the owner can retract a fact (the version before it is current again), correct one (a new object or value,
      at its own turn or from a later one, until the story says otherwise), and split two names the story joined (K8),
      with the names that still join them when a split cannot separate them.
    • Step 5 (new plugin build): the repairs are buttons in the panel's Inspector, on the line they fix: close a thread
      with an outcome (or tick several and close them together), reopen, mark a secret found out or still kept per
      character, retract or correct a fact (its object or its value, from its own turn or a later one), undo; split
      two names on an entity's page. Each chat's page starts with
      "Needs attention": threads open for 30 turns without a restatement, ends that matched no thread, disputed
      whereabouts, repairs that match nothing now, splits still joined through another name, ambiguous names.
    • Step 6 (docs/perf/repair.md): with the owner's decisions made as repairs, no closed thread reaches a packet on
      either measured chat, M0 gains a case and loses none, and <Story> is back in every secret-gate scene. A memory
      read with many fact repairs no longer searches every assertion for each (it cost +66 ms at 10,000 messages with
      100 repairs; now +7.0 ms over Phase 12, +1.7 ms with none; accepted by the owner). A thread with no counterpart
      reads "하나: …" in the Inspector, not "하나 → ?: …". tools/eval_secret_gate.py takes a case's words by secret.
  • Cross-model review (.ai/, AGENTS.md §14; #138): fixed after its review, the script refuses every .env* name
    (.envrc passed before) and gives the reviewer CLI only the system basics of the environment and its own login
    variables, never NMOS_*, DATABASE_URL or PG* (it passed the whole environment).

    • The default memory budget is 2,000 tokens (was 800); a value saved in the plugin stays. Lower PocketRisu's max
      context accordingly.
  • Phase 11 (Stage 5, part 1) is complete (docs/phases/PHASE-11.md, 2026-09-28):

    • M0, an evaluation on a restored copy of the owner's chats (tools/eval_rp.py, numbers only in
      docs/perf/m0-baseline.md): 13 of 28 owner-confirmed cases on the Phase 10 code, 15 after step 3.
    • A relationship has one history per pair (ADR 0038, K24). A change the extraction records in the other
      direction ("카이토 → 유이: 연인" after "유이 → 카이토: 같은 반 친구") replaces the old one when either is symmetric
      (friends, lovers, classmates…); a directed pair ("엄마", "자녀") keeps both. Feelings and speech levels stay per
      direction.
    • The persona's full name is the persona (resolve-v5): "아오키 타쿠미" for the persona "타쿠미". One chat was two
      people, so old speech levels stayed current and promises made to both never closed.
    • packet-v5 (new default) names what a relationship, feeling or speech level replaced, with its turn
      ("…: 연인; before, turn 1: …: 같은 반 친구"), and how it started when that differs, so "how did they stand before"
      has an answer. NMOS_PACKET_POLICY
      keeps the earlier policies.
    • extract-v13: goals, questions, threats and debts end (ADR 0039, migration 0022). They are threads like
      promises; the prompt lists the open ones and resolved ends one (achieved, abandoned, failed, answered, averted,
      paid). A craving or the next thing someone is about to do is not a goal. A cause the story states is kept
      (because). Re-extracts each chat's recent window. A promise or goal kept from someone who then finds it out is
      no longer shown hidden from them as a thread. A goal the story never mentions again stays open (K23): on the
      owner's longest chat 9 of 48 goals ended and 37 stayed open.
    • packet-v6 (new default): the cause the story states ("…: 화남; because: 약속을 잊어서"), on facts and claims,
      and facts with a cause come first when the message asks why (ADR 0040). The Inspector shows each cause and the
      event it names when one nearby clearly matches.
    • M0 counts answers the prompt's own last messages hold: of 28 cases, 9 need memory; 5 of them before Phase 11, 7 now.
    • Inspector: a Relationships section with each pair on one row (relationship, each direction's feeling and
      speech level, with cause, what it replaced and how it started), on the chat and character pages; open threads
      counted by kind.
    • packet-v7 (new default): one numbering for turn (ADR 0041). A fact, claim or thread says the
      turn index, but an excerpt's turn and a state item's as_of_turn said the message's position, so one packet
      could show a goal at "turn 26" next to an excerpt of the next message at "turn 56". Under packet-v7 they carry
      the turn of their message; excerpts stay in story order. Same outcomes on the memory evaluation and on M0
      (26 of 28). The Inspector's state table ("As of turn") now shows the turn index; it showed the position.
  • The compose files no longer pin a packet policy. Both passed NMOS_PACKET_POLICY=packet-v4 unless .env set
    it, so a stack started from them on main kept packet-v4 after packet-v5 and packet-v6 became the default.
    They now pass it empty, which means the sidecar's own default. No release was affected (v0.1.0-beta.21 pins
    packet-v1, its default).

  • A saved API key is sent only to the host it was saved for (review of an external analysis, 2026-09-27).
    /v1/config/test and /v1/config/models sent the saved key to whatever URL the request named, and saving an
    endpoint on another host kept the key for it, so anyone who could reach the settings API could have the key sent
    to their own server. Now a test or model list for another host goes without the saved key (its error says why),
    and saving an endpoint on another host drops the saved key: enter it again. A key from .env applies again
    when the endpoint is back on the .env host.

  • The plugin's host reads count against the request deadline. Only the sidecar calls had a hard deadline: a
    settings or chat read the host never answered held the generation. The memory budget and deadline set in
    PocketRisu's own plugin argument fields are capped like the panel's; a budget over 20,000 tokens, which the
    sidecar refuses, made every request go without memory (the panel did not cap the budget either).

  • The worker waits for the sidecar's migrations. Compose starts both at once, and a worker that claimed jobs
    against the previous schema failed them and used up their attempts.

  • A recall reads the chat as its request had it. A sync of the same chat from another tab or device (K28)
    that landed during a recall added its messages to what the recall read and recorded.

  • Tests: the plugin's DOM code (the Inspector sanitizer's tree walk, the settings panel) runs under happy-dom,
    a new dev dependency (owner approval 2026-09-27).

  • The sidecar's access log no longer shows the auth token (audit A-11). The Inspector opened in a browser
    tab passes the token as ?token= on every link, and uvicorn logged each request line with it. The log now
    shows token=***. The token still stays in that browser's history (K21); the panel's Inspector tab sends
    it in a header.

  • Known issues: K28, two tabs or devices taking turns in one chat (audit A-13); K21 now covers the token in
    Inspector links and the plugin's "full database" permission (audit A-18).

  • Known issues: K3 measured on the real host. A reroll or a swipe change of the last reply never takes the
    slow sync path, because the reply it replaces was never synced. An edit of an older message does: 3.6–3.8 s
    at 10,000 messages, over the 3 s default (docs/perf/scale.md).

  • Knowledge marks given as objects are names again (ADR 0007). The extraction model sometimes lists
    known_by / hidden_from entries as {"name": "타쿠미", "type": "character"}, the participants shape;
    validation stored the object's repr ({'name': '타쿠미', 'type': 'character'}) as the name. Such a mark
    matched no character: the packet's knowledge marks and the ledger's hidden_from showed the repr, a
    question naming a character the fact is hidden from did not rank it up, and the Inspector did not list it
    under that character. Validation now keeps the name, and rows stored earlier read as the name without
    re-extraction; the stored rows are unchanged. Ledgers already recorded keep what they recorded. On the
    owner's database this affected 4 assertions, 1 of them still served.

  • Plugin tests no longer depend on how busy the machine is (audit A-19): the deadline test runs on a fake
    clock, and the 10,000-message manifest test has its own time limit.

  • Korean memory packets hold more (K26, ADR 0032; owner decision). The packet's budget is filled against
    a token estimate that counted every Korean character as 1.5 tokens; real tokenizers count 0.74–0.98, so a
    full Korean packet used only 68–75 % of the memory budget. The new default packet policy packet-v2
    counts 1.2. A full Korean packet holds about 1.5 more lines and uses 76–85 % of the budget
    (docs/perf/token-estimate.md). English packets are unchanged. Nothing is re-extracted. To keep the old
    estimate, set NMOS_PACKET_POLICY=packet-v1.

  • An OOC note in a reply no longer becomes a fact (K27, audit A-12; owner decision). New extractor
    generation extract-v11: the prompt says that only the story is evidence. OOC notes, [System: …] lines,
    requests to the memory and memory markup give nothing; the story's narration and what characters say
    still count. With gemma4:31b-cloud an OOC note in a reply was stored as a fact 3 of 3 times before and 0 of
    3 now, and the plainly told control fact is still kept 3 of 3. Text shaped like a packet line
    (<Fact …>…</Fact>) still becomes a fact, because the text normalizer removes the tag before extraction
    (K27). The registry drops Predicate.epistemic, which nothing read (audit A-14). Other bars are unchanged or
    within the model's run-to-run variance (docs/perf/extract-v11.md).

  • Memory markup in a reply is no longer read as story (K27; owner decision). New text normalizer
    clean-v3: NMOS's own memory markup inside a message (a whole packet a reply echoes, or a line shaped like
    <Fact …>…</Fact>, <Claim>, <Thread>, <Excerpt> or a keyed <Item>) is dropped together with its
    content. clean-v2 removed the tag and kept the text, so the extractor read the claim as narration and stored
    it (3 of 3 with gemma4:31b-cloud), and recall could offer it as an excerpt. Only the tag names the packet
    writes, spelled the same way, count: a bot's <state> block or <Item> inventory line stays story text.

  • Secrets: what is kept from someone, and when it stops being a secret (Phase 10 steps 2–3, ADR 0033). New
    extractor generation extract-v12. A fact is marked hidden from a character only when the story keeps it
    from them (a secret, a lie, a surprise, a hidden identity), no longer because they were elsewhere. The
    extraction is shown the chat's open secrets and reports which ones a character finds out in the turn; from
    then on the fact no longer says it is hidden from them, and the facts view shows who found it out and when.
    Deleting that turn restores the secret. On a copy of the owner's longest chat, the plan the mother found out
    at turn 21 is no longer marked hidden from her afterwards (five copies of it end there), a surprise ends
    when it is given, and the daughter's hug promise gets the mark it lacked (docs/perf/extract-v12.md).

  • A generation's backfill (after a model or prompt change, and "Extract all history") is now extracted oldest
    turn first; new chats and new turns still go newest first. The previous generation keeps serving the turns
    meanwhile, and a later turn's promises and secrets then refer to what this generation extracted.

  • Facts only some characters in the scene know go in a Private section (Phase 10 step 4, ADR 0034). The new
    default packet policy packet-v3 works out who is in the scene (characters of the last two turns, anyone
    named now, the user's character) and moves facts and promises that someone present is not shown to know into
    <Private>, with a rule: only the holders know it, others must not act on it, and the holders keep it from
    those it is hidden from. A fact hidden from someone in the scene also ranks a little higher. The rule costs
    about 47 tokens of the memory budget when the section opens; with the default 600 a fact line can drop out,
    so raise 기억 예산(토큰) / Memory budget (tokens) for chats with many secrets (docs/perf/packet-v3.md).
    NMOS_PACKET_POLICY=packet-v2 keeps the previous layout; both compose files now default to packet-v3.

  • Per-chat memory mode (Phase 10 step 5, ADR 0035). A chat's page in the Inspector has a 기억 모드 / Memory
    mode
    card. Strict gives the model only what everyone in the scene knows: a fact only some of them know
    becomes "something known to A, not known to B", its content withheld (fewer leaks, but the holder forgets it
    too). First-person narrator (none, your character, or a character of the chat) leaves out what the
    narrator is not shown to know. Excerpts of the chat that say what the mode left out are left out too. Both
    are off by default, and each request records the mode it used.

  • A character's claim that only some of the scene know now goes to the Private section like a fact, with who
    knows it; before, strict mode could still carry a secret word for word as a <Claim> (ADR 0035).

  • The default memory budget is 800 tokens (was 600; owner decision). On the owner's recorded requests a
    600-token packet placed 66% of the memory lines retrieval found, 800 places 89%, and 1000 all of them
    (docs/perf/memory-mode.md). If you left 기억 예산(토큰) / Memory budget (tokens) empty, lower PocketRisu's
    max context by 200 more.

  • The promise your message is about is remembered first (ADR 0019 amendment 1). Promises were chosen by who
    is named, newest first, three at most, so newer promises could push out the one the scene is about (asking a
    child to go and hug her mother dropped her old promise to do just that). Under the default packet-v4 a
    promise whose words your message repeats now comes first.

  • A character who found out a secret now counts as knowing it (ADR 0033 amendment 1). Before, with them in
    the scene the fact still went to the Private section with its rule telling them not to act on it, strict
    mode withheld it from them, and as a first-person narrator they were not given it. Found by the Phase 10
    evaluation, which adds seven synthetic secret cases to CI (docs/perf/eval-baseline.md).

  • CI also upgrades a database that 0.1.0-beta.21 wrote, and checks that an upgraded chat has the default
    memory mode.

  • Known issues: K29, a reveal among the turns first extracted together (connecting an existing chat) can be
    missed; run Extract all history once after connecting a chat with secrets.

  • The sidecar says whether your plugin is the right one (ADR 0037, K19). The plugin sends its build id with
    every sync, and the sidecar compares it with the plugin file of its own version. The Inspector's first page
    says "the sidecar's build", or warns that the plugin in use differs (an older plugin shows as "no build id"),
    or that another tab or device still runs an older one and needs a reload. It links the matching plugin file,
    which the sidecar serves at /v1/plugin/nmos-pocketrisu.js. The Status tab of a plugin with the check says
    the same. /v1/health reports it too.

  • The Inspector shows a chat's secrets (Phase 10 step 6). A chat's page has a 비밀 / Secrets section: each
    fact kept from someone, who knows it, whom it is kept from, the turn, and for each of them whether and when
    they found out. Reports of someone finding out that matched no secret are listed apart, and a character's
    page lists the secrets they found out. The last packet's section names the scene's characters and the
    memory mode the request used. Sidecar only.

  • The Status tab says when memory did not fit (ADR 0036). When the last reply's memory budget left memory
    out, a card says how many lines of how many, and the budget that holds them all (in steps of 100, up to 2000),
    with a button that sets it. Lower PocketRisu's max context by the same amount. Inspector → Retrievals shows it
    too ("all at N"). With the facts extract-v12 writes, the owner's chat needs 1000–1100 (docs/perf/budget.md).

  • A line that says an earlier line again is left out (ADR 0036). The new default packet policy packet-v4
    keeps one of two lines with the same subject, relation and content (a fact extracted twice, a character's
    claim of what the narration already says), which frees room for others. NMOS_PACKET_POLICY=packet-v3 keeps
    the previous layout; both compose files now default to packet-v4.

Upgrading from 0.1.0-beta.21

Back up the database first (README, "Upgrade, backup and rollback"): migrations 0021–0026 run when the new sidecar
starts, and a rollback needs the backup.

  1. Pull the new image and restart the sidecar and the worker. At startup:
    • the normalized text is rewritten (clean-v3);
    • migrations 0021–0026 add memory modes, thread outcomes and causes, summaries, owner repairs and canon.
  2. Model calls at the provider's cost (with an LLM configured):
    • extract-v13 becomes active, and each chat's latest NMOS_EXTRACT_BACKFILL turns (default 100) are extracted
      again once. Older turns keep their earlier facts until Extract all history, which also finds reveals a first
      extraction missed (K29).
    • Scenes are summarized (one call per 8 turns and a story call; NMOS_SUMMARIES=0 or the panel turns this off).
    • Canon facts are read once the new plugin sends a chat's canon: the card, the persona and the note at once, and
      each lorebook entry once a prompt has held it (NMOS_CANON_FACTS=0 or the panel turns this off).
    • With embeddings on, every message is embedded again.
  3. Replace the plugin file (the host offers the update: //@version 0.2.0) and reload PocketRisu. The sidecar
    serves it at /v1/plugin/nmos-pocketrisu.js, and the Inspector says whether the plugin in use matches (ADR 0037).
    • The new plugin brings the memory mode, the repair buttons, the canon sync, the switches and the default budget
      of 2,000 tokens.
    • With the old plugin memory still works, but without canon, repairs or the new default budget.

Known limitations

  • Canon facts are not yet measured on the owner's chats (Phase 14 step 6), and neither are their model calls or
    the latency with a large lorebook. A story that changes who someone is, or how two characters stand, is listed in
    "Needs attention" against canon until the owner chooses.
  • Secrets: a secret is kept by instruction, not isolation (K11). A summary can say a secret in other words (K30).
  • Threads stay open until the story ends them in words extraction recognizes, or the owner closes them (K23).
  • Names: a given name alone is a mention only when the lorebook lists it (K31). A persona narrated in the third
    person does not bring its own facts (K32).
  • Long chats: beyond about 10,000 messages memory needs a higher deadline (K1).
  • The full list is docs/KNOWN-ISSUES.md.