Compare commits

..

23 Commits

Author SHA1 Message Date
vh c6788539ac feat(web): collapsible hideable left+right debug panes
Each debug pane (#rail-left, #affect-console) gains an independent
collapse control: a chevron button in the pane header folds it to a
22px re-open strip, and #center (flex:1) reflows to fill. State
persists per-pane in localStorage (ratatoskr-left-collapsed /
-right-collapsed), matching the theme/cot-toggle idiom; the right-side
drag-resizer hides with its pane. Collapsing both yields a clean
chat-only surface.

Playwright-verified end-to-end (default-open, collapse-to-strip,
center reflow, independent left/right, reload persistence, re-open).
2026-07-17 13:27:01 -07:00
vh 021fb5265f memory: /snapshot — arbo image-gen smoke teed up for a fresh /vor-plan; name-recall arc + web UI shipped; archived 5 oldest entries 2026-07-17 09:33:17 -07:00
vh 5a1883a023 feat(web): code-block word-wrap + per-code-block and per-turn copy buttons
Operator UX asks on the web console:
- Code blocks now WRAP (`white-space: pre-wrap; overflow-wrap: anywhere`) instead of
  overflowing with a horizontal scrollbar — a long unbreakable token wraps in place.
- Per-code-block copy button (hover-revealed, top-right of each `.md-code-wrap`).
- Per-turn copy button (in the live turn-rule; top-right on seeded/historical turns) —
  copies the turn's response text; the existing think-inline copy is unchanged.

One delegated click handler on #tw covers both (works across live / historical / think
renders where blocks are injected via innerHTML). `copyText` falls back to a hidden
textarea + execCommand when `navigator.clipboard` is absent (plain-http LAN context),
and buttons flash "✓ copied". Static file served from disk — a browser refresh picks it
up, no :8765 restart. Playwright-verified: code wraps (no x-overflow), both buttons copy
the right text; 90 web tests green.
2026-07-17 08:58:22 -07:00
vh bf421f1203 memory: capture brokkr's pinned scoring rubric for the referential-frame probe (vocab-adherence manipulation check load-bearing) 2026-07-17 01:36:01 -07:00
vh 0e7a04331a memory: queued P06-driver referential-frame A/B/C probe (operator-ratified, waits on WT b122); Sindra pristine baseline for test drive 2026-07-17 01:33:39 -07:00
vh ab8c1ee781 memory: #364 CLOSED end-to-end both sides — cbbc7bdd forgotten, zero live name-negatives; durable name-recall COMPLETE (WT #367 tracks bridge gap) 2026-07-16 22:55:23 -07:00
vh eef4d01c15 memory: mark_superseded SHIPPED+VERIFIED (v0.20.15) — durable name-recall DONE; Arm-2 delivered+credited, quartet rebuild deferred 2026-07-16 22:53:39 -07:00
vh be2c577884 feat(provider): mark_superseded verb — Worldtree #364 contradiction retirement + bifrost 1.1.4
Implement `mark_superseded(ids, *, superseded_by=None, reason=None)` — the SOLE
supersession verb Worldtree #364's promotion-hygiene reconciliation calls to retire
contradicted facts (wire shape confirmed by worldtree-dev, bifrost_memory_store.py:293).
Live re-verify (2026-07-16) proved our provider 500-crashed on this call (unimplemented)
→ #364's retirement couldn't land + a retry-storm bloated the store; the readout only
passed via transient recency-eviction.

- `mark_superseded` mirrors the reference `_mark_lifecycle`: sets top-level
  `superseded=True` (+ `superseded_by`/`superseded_reason` when non-None), increments
  revision, NON-destructive (get still returns; recoverable). Unknown ids skipped.
- `_is_live` (INV-011) now short-circuits on `superseded is True`, so a retired chunk is
  excluded from `scan` (person-prime) — durable retirement, not just recency-eviction.
  search is unfiltered (matches reference; WT re-checks liveness client-side).
- Contract: un-defer mark_superseded (+ FN spec, INV-011); TDD 5/5 (retires-from-scan
  tracer, non-destructive-get, unknown-id no-op, non-None-fields-only, parity #195).
- bifrost 1.1.1→1.1.4: hasattr-gate backstop for the maintenance verbs (unimplemented
  verb → unsupported_capability 400, never AttributeError/500/retry-storm — the gap we
  surfaced) + the 1.1.3 scan/cursor conformance harness. Full suite 644 green.
2026-07-16 22:37:31 -07:00
vh c0d00ccd18 memory: bifrost 1.1.4 (hasattr backstop) supersedes 1.1.3 as the next bump target; folds into provider-maintenance batch 2026-07-16 16:53:47 -07:00
vh 722d6c76de memory: #364 CLOSED worldtree-side (b105); mark_superseded ship = the durable half, awaiting operator go 2026-07-16 13:41:37 -07:00
vh 56f4895881 memory: #364 GC done — store back to 10-chunk specimen; durable name-recall now depends on mark_superseded (next task) 2026-07-16 13:39:54 -07:00
vh ca249e4986 memory: #364 storm root-caused WT-side (self-stopping); mark_superseded wire shape confirmed; GC-after-stop plan 2026-07-16 13:33:00 -07:00
vh faf605fdf1 memory: #364 re-verify DONE — readout passes (Sindra recalls name) but supersession 500s (mark_superseded unimpl'd); new task + retry-storm flagged 2026-07-16 13:29:16 -07:00
vh 7c8644dc45 memory: soong-lab motivational export bug RESOLVED (v0.3.24 emits valid objects); drop the string->object workaround 2026-07-16 09:35:21 -07:00
vh 195292156f memory: R39 Phase-2 probe-runner role ACCEPTED in principle (Vuong); formalize when Arm-2 spins 2026-07-15 23:39:48 -07:00
vh abe1b52002 memory: R39 Phase-2 prereg frozen; ratatoskr flagged probe-runner (Arm-2) — formal role PENDING Vuong's scope call 2026-07-15 23:36:17 -07:00
vh e365b24339 memory: /snapshot — condense in-flight to milestone-close; person-prime shipped+verified, R39 arc closed; first persistent-memory.d/ detail files 2026-07-15 22:54:20 -07:00
vh 5e3e88d26d memory: snapshot — R39 hold lifted, Arm-0 arc closed; live store kept as #364 re-verify target 2026-07-15 22:41:06 -07:00
vh ce6907bd73 memory: snapshot — R39 re-drive COMPLETE + decisive (no fusion fixes #364; signal-family fix); froze Phase-2 Arm-2 specimen seed 2026-07-15 22:40:07 -07:00
vh e2b2f51364 memory: snapshot — worldtree-dev triage: both flags folded into #364 (2 defect classes); #349 ranking already-ruled (no band-aid, R39 identity-pinning) 2026-07-15 22:33:14 -07:00
vh 0f2b28919a memory: snapshot — R39 verbatim export sent to brokkr; subject-provenance bug caught (Sindra self-fact in user partition) 2026-07-15 22:31:39 -07:00
vh b154bb3885 memory: snapshot — verify spawned cross-project R39 (bifrost cursor ruling, WT #364, brokkr export); personal-data export gated on operator consent 2026-07-15 22:21:48 -07:00
vh 46d6efa962 memory: snapshot — person-prime LIVE-VERIFIED (v0.20.14 handshake fix); name-recall gap is WT ranking/hygiene 2026-07-15 09:14:09 -07:00
12 changed files with 508 additions and 60 deletions
+15
View File
@@ -48,6 +48,21 @@ The 2026-05-20 → 2026-05-29 cluster: the original debug-TUI/web build era, bef
- `[2026-05-29]` **v0.17.0 frontend redesign — aurora telemetry instrument.** `/frontend-design` pass on the web companion: all-monospace technical-instrument aesthetic with the Australis dark palette + aurora-borealis accent band. Top command bar with live connection dot (idle/streaming/error states), inline persona summary with P/A/D micro-bars, animated awaiting-token, terminal-event status chips. **Live Markdown rendering in transcript + thinking panes** via a hand-rolled `markdownSafe()` (escape-first, whitelist subset of headings/bold/italic/inline-code/fenced/lists/quote/links; link-scheme whitelist; XSS-verified under a node harness). Thinking pane now has per-turn labeled dividers + a fresh MD-rendered block per turn. **Tools / Debug / Persona panes stay literal monospace** by deliberate choice — they carry structured audit lines + JSON, where MD would corrupt readability (underscores in tool names, JSON braces). Single-file vanilla HTML/CSS/JS, no build, no CDN, no node_modules.
- `[2026-05-29]` **Codex-first discipline pilot — Ratatoskr selected.** brokkr-smithy-dev pushed `AGENTS.md` (commit `bbeaa23`) and declared the `ratatoskr-codex` handle per `brokkr-smithy/docs/codex-first-discipline.md` v0.1 (brokkr-smithy commit `5dd061c`, tag `v0.5.3`). Per-dispatch opt-in model: default Sleipnir Claude-implementer path remains available; Codex used only when operator routes via `/codex-dispatch <N>`. Bootstrap handshake when operator spins up a codex session: codex sends `codex-online` → ratatoskr-dev replies with active branches + WIP state. Galdrabok was rejected as pilot (Codex authoring Claude skills is a category error); Skaldsong was the other candidate. (Still dormant as of 2026-06-18 — no codex session spun up.)
- `[2026-06-14]` **Ratatoskr becomes the v1 Bifrost Tier-3 consumer.** A second identity beyond the debug TUI: the durable persistence Worldtree writes Tier-3 agent affect (persona) + memory into. Pin `bifrost>=0.6.1` in a `provider` optional-extra (gitea PyPI index, auth via `~/.netrc`; 0.6.0 was yanked for a circular import). Implement bifrost's OWN `MemoryDataStore`/affect Protocols (NOT worldtree-memory's); `describe_store` is SYNC; affect is conduit-opaque. New module `src/ratatoskr/provider/`. Authoritative how-to: `~/development/bifrost/docs/implementing-a-consumer.md`. (commits `1a73d77` pin, `d90a58d` affect store v0.17.1, `bcdcd71` serve entrypoint v0.17.2)
_Archived 2026-07-17._
- `[2026-06-14]` **Backend = SQLite + sqlite-vec; affect-first then memory; separate DB per plane** (operator-chosen). Affect = blind conduit (reads only `agent_id`+`end_user_id`); memory = structural index (reads vector/scope/id/origin to serve search). Conformance for both = #195 parity vs bifrost's `InMemory*Store` through the real `dispatch_*_call`.
_Archived 2026-07-17._
- `[2026-06-14]` **The affect contract's idempotency model was WRONG; real-lib TDD caught it.** First draft modeled same-idempotency-key-different-payload as an LWW overwrite; bifrost actually raises a CONFLICT (`AffectIdempotencyConflict`), actor-scoped. The artifact-only `/heid-contract-review` STRUCTURALLY cannot catch this class (it never sees bifrost's source) — TDD against the shipped library is the gate; the executable reference store + #195 parity are the backstop. Filed the guide §6 gap to bifrost-dev, who fixed it (bifrost `c0d0a11`).
_Archived 2026-07-17._
- `[2026-06-15]` **Memory v1 = the bifrost BASIC plane only** (search/get/upsert/delete + describe_store/health) per worldtree-dev re-scope (#294) — the only surface Tier-3's live path touches; gated verbs (edges/scan/atomic_supersede/mark/patch/maintenance) deferred + advertised-unsupported. Worldtree v0.35.3 already requests+maps it — no Worldtree-side blocker. Memory contract committed v1.0 (`eebab46`) → v1.1 Heid-reviewed (`1f94e5f`).
_Archived 2026-07-17._
- `[2026-06-15]` **Providers run as dev-box BACKGROUND SHELLS, not infra-ops/systemd** (operator call — it's a dev box). `ratatoskr-provider` (affect) + `ratatoskr-memory-provider` as background processes; no productionization track.
_Archived 2026-07-17._
## Tried and abandoned (archived)
The 2026-05-20 → 2026-05-28 cluster: original-build-era foot-guns. Archived 2026-06-18.
@@ -163,6 +163,13 @@ interpreted.
id-list + TTL + `ScanCursorExpired`); DEFERRED pending bifrost-dev's ruling on the conformance
gap (scan/cursor has NO conformance coverage today, so a non-snapshot cursor passes). Routed
to bifrost-dev 2026-07-15.
- **INV-011** [hard]: **`mark_superseded` retires via a top-level `superseded` flag; `_is_live`
recognizes it.** `mark_superseded` sets top-level `superseded=True` (+ `superseded_by`) on the
record, mirroring the reference `_mark_lifecycle` (NOT a `verbatim.governance_state` change). So
`_is_live` MUST short-circuit on `record.get("superseded") is True` (in addition to its existing
`lifecycle_state` / `verbatim.governance_state` checks) — else a #364-retired chunk would still
scan live. Retirement is NON-destructive: `get`/`get_many` still return superseded chunks
(recoverable). `search` is NOT filtered (matches the reference; WT re-checks liveness client-side).
## Concurrency
@@ -199,7 +206,7 @@ negotiation, routes). **This contract** owns the store (the basic verbs + SQLite
## Out of scope (deferred — do NOT flag as drift)
- **Gated/maintenance verbs:** `upsert_edges`/`get_edges_for`, `mark_invalid`/`mark_superseded`, `patch_many`, `atomic_supersede`, lease/checkpoint. Absent + advertised-unsupported. (`scan` is NO LONGER deferred — it is implemented + advertised via `sortable_chunk_fields` to light up Worldtree's #349 person-prime turn-1 durable-fact injection; see the `scan` FN spec + INV-009/INV-010.)
- **Gated/maintenance verbs:** `upsert_edges`/`get_edges_for`, `mark_invalid`, `patch_many`, `atomic_supersede`, lease/checkpoint. Absent (no describe_store cap; hasattr-gated at dispatch as of bifrost 1.1.4 → `unsupported_capability` 400). (`scan` and `mark_superseded` are NO LONGER deferred — `scan` implements #349 person-prime; `mark_superseded` implements Worldtree #364's contradiction retirement, the SOLE supersession verb #364 uses. See their FN specs + INV-009/INV-011.)
- **metadata_filter beyond scope:** advertise `filterable_metadata_fields=[]`; a non-empty `metadata_filter` is unsupported in v1 (rejected — see search PRE).
- **The combined two-plane server** (guide §7) — separate memory + affect apps in v1.
- **Deployment** — dev-box background shell (`ratatoskr-memory-provider`), no systemd/infra.
@@ -306,6 +313,24 @@ TESTS:
delete_absent [boundary]: unknown id → {"deleted":0}
```
```contract
FN mark_superseded(self, ids: list[str], *, superseded_by: str | None = None, reason: str | None = None) -> dict
BRIEF: Worldtree #364 retirement — mark chunks superseded so scan (live-only) excludes them. Mirrors the reference _mark_lifecycle: sets TOP-LEVEL fields on the record; NON-destructive (get still returns them, recoverable). The SOLE supersession verb #364 uses (dispatch: bifrost/memory.py mark_superseded branch; args {ids:[...], superseded_by, reason}).
PRE: [PRE-001 hard] ids is a list of chunk ids (WT sends singletons, one call per retired chunk)
POST: [POST-001 return_value] {"marked": N} where N = ids that existed (unknown ids skipped, never error) -- assert
POST: [POST-002 state_change] each existing chunk gets top-level `superseded=True` + `superseded_by` (when not None) + `superseded_reason` (when not None); revision incremented; mirrors reference _mark_lifecycle (only non-None fields written) -- assert
POST: [POST-003 return_value] a superseded chunk is EXCLUDED from `scan` (INV-009 via _is_live's top-level `superseded` check, INV-011) but STILL returned by `get`/`get_many` (non-destructive) -- assert
STEPS:
1. [sequential, flexibility=indicative] FOR each id present: load record_json, set superseded=True (+ superseded_by / superseded_reason when not None), UPDATE record_json + revision+1; count
2. [cleanup] RETURN {"marked": count}
TESTS:
mark_retires_from_scan [happy,tracer]: upsert 3 live; mark_superseded([id2], superseded_by="x"); scan → the 2 non-superseded only (id2 excluded); id2 record has superseded=True + superseded_by="x"
mark_get_still_returns [scenario]: a superseded chunk is STILL returned by get (non-destructive/recoverable)
mark_unknown_id_noop [boundary]: mark_superseded(["nope"]) → {"marked":0}
mark_no_superseded_by [boundary]: mark_superseded([id], superseded_by=None) → superseded=True set, no superseded_by key written (only non-None fields)
mark_parity_vs_reference [scenario]: identical mark_superseded envelope vs InMemoryMemoryStore → same top-level superseded/superseded_by field shape (#195)
```
```contract
FN scan(self, *, scope_all: dict | None = None, scope_any: list | None = None, cursor: str | None = None, limit: int, sort: dict | None = None, lifecycle_state=None) -> dict
BRIEF: Query-LESS paginated LIVE-chunk scan, globally ordered by an advertised sort field (updated_at) — the #349 person-prime turn-1 durable-fact injection primitive (no query vector, unlike search). Returns {records, cursor}.
@@ -0,0 +1,44 @@
# [2026-07-16] bifrost scan/cursor conformance gap → snapshot-cursor ruled NORMATIVE
## The gap (I surfaced it; operator's catch that it was bifrost's to fix)
The heid-bug-hunt flagged our offset cursor's cross-page dup/drop under mutation. On
verifying before routing, it turned out sharper than "robustness": bifrost's PROTOCOL
already mandates snapshot cursors — the dispatch engine (`bifrost/memory.py:311-314`)
drops `sort` on a cursor continuation with the comment *"the cursor's snapshotted order is
authoritative"*, and maps `ScanCursorExpired → 410`. The reference `InMemoryMemoryStore`
implements it (frozen ordered id-list per opaque token + TTL). But bifrost's **conformance
suite had ZERO scan/cursor coverage** — so our non-snapshot offset cursor passed
conformance while violating the protocol contract. That coverage hole is the real
completeness concern. Routed to bifrost-dev (thread `01KXK7MDTY…`).
## bifrost-dev's ruling
1. **Snapshot-cursor is NORMATIVE, not opaque/per-store.** Operator ruled: cursor
snapshots a frozen ordered id-list, continuation ignores `sort`, stale/unknown cursor →
`ScanCursorExpired` → 410. **Offset-with-documented-limits is NOT blessed.**
2. **Conformance coverage added** in **bifrost 1.1.3** (`bifrost.conformance.
memory_store_conformance`, 4 probes: `scan_snapshot_order_authoritative`,
`scan_no_dup_or_drop`, `scan_unknown_cursor_expired`, `scan_snapshot_stable_under_write`
opt-in). Our offset bug ships as their negative-canary fixture
(`fixtures.v0_7.offset_cursor_store.OffsetCursorMemoryStore`). `main()` grades import
failure as exit 2 (setup error) vs exit 1 (conformance FAIL).
## Our status + the TODO (operator-sequenced, NOT urgent)
Single-page person-prime (`cursor=None`) is already conformant — nothing shipped is broken;
it's only multi-page continuation that's non-conformant. Our contract INV-010 currently
marks the offset cursor **v1-provisional / KNOWN DEVIATION** (commit `8199774`).
TODO when sequenced:
1. Bump pin bifrost `1.1.1 → 1.1.4` (1.1.4 supersedes 1.1.3: adds the hasattr-gate backstop for the
maintenance verbs — mark_superseded/mark_invalid/patch_many/delete_many/upsert_edges/get_edges_for
degrade to `memory.unsupported_capability` 400 not AttributeError/500 — on top of 1.1.3's scan/cursor
conformance harness. One bump gets both).
2. Replace the offset cursor with the reference snapshot semantics (frozen id-list per
opaque token + `ScanCursorExpired` → 410).
3. Run `run_all(store_factory=..., include_optional=True)` against our SQLite store —
expect P1/P2/P3 RED → green (the before/after IS the validation). Report to bifrost-dev
(we're their canary — first non-reference scan implementer).
4. Flip INV-010 from v1-provisional to snapshot semantics.
TTL-duration expiry NOT asserted by the harness (no portable clock hook via store_factory);
bifrost-dev offered a `clock_control` opt-in if we ever need time-based expiry certified —
parked, not needed yet.
@@ -0,0 +1,65 @@
# [2026-07-15/16] person-prime `scan` build — SHIPPED + DEPLOYED + LIVE-VERIFIED (v0.20.14)
**The "last push": fix Sindra not remembering Vuong's name across sessions.**
## Root cause (worldtree-dev)
WT injects a recalled fact only if combined score (sim×salience) ≥ **0.45**
(`auto_inject_combined_score_threshold`, `core/memory/context_promotion/config.py`),
and recall is per-turn **query-gated** → moderate-sim durable facts (name hit ~0.40)
never inject. Designed turn-0 fix = WT **#349 person-prime**: a query-LESS
top-N-by-recency durable-fact injection, capability-gated on the store advertising
`updated_at` in `sort_fields` at the Bifrost handshake — DARK for our provider until now.
Fix (ZERO Worldtree change): implement the sorted `scan` verb + advertise the cap.
## What shipped (6 commits, v0.20.11 → v0.20.14; suite 639 green throughout)
- `8fc757a` **v0.20.11**`scan` verb + `sortable_chunk_fields` cap. Query-LESS,
LIVE-only (INV-009: superseded/tombstoned excluded server-side), globally-ordered-
before-pagination by indexed `updated_at` (INV-010), sort dispatch-gated. Offset
cursor with a `has_more` peek (no empty trailing page = reference-parity). TDD 7/7
incl. `parity_vs_reference` #195. NUANCE resolved: bifrost's `InMemoryMemoryStore`
READS `updated_at` (never stamps it) — identical to ours; ref does NOT lifecycle-
filter so parity is over the live set only.
- `a9c521a` **v0.20.12** — Sindra holodesk first-message preset (split out of the
soong-lab redefine as its own concern).
- `25ccb5c` **v0.20.13** — heid-bug-hunt fix: a truthy non-dict `sort`
(`"updated_at"`/`["updated_at"]`/int) hit `(sort or {}).get(...)` → AttributeError
instead of InvalidArguments. Added isinstance guard (Gróa#1/Hulda#2 confirmed).
- `8199774` — contract: marked the offset cursor **v1-provisional / KNOWN DEVIATION**
(see [[2026-07-16-bifrost-cursor-conformance]]).
- `f46ccba` **v0.20.14****THE DEPLOY-BREAKER** (see Tried/abandoned): advertised
`sortable_chunk_fields=[{"name":"updated_at"}]` WITHOUT `type` → bifrost
`handshake_response` `SortableChunkField` requires BOTH name+type
(`additionalProperties:false`) → whole bind broke. Fixed → add `"type":"timestamp"`
+ regression guard in the caps test.
## heid-bug-hunt triage (panel Gróa/Hulda/Regin, thread `01KXK5XTYHV8TGEDRAZV8GRXWC`)
FIXED: non-dict sort (v0.20.13). SURFACED→operator: offset-cursor cross-page dup/drop
(→ became the bifrost cursor arc). NOTED (reference-parity/accept-known-risk, no fork):
full-table materialize-then-Python-filter (matches ref's O(store) iteration), unbound
limit, falsy scope coercion, lexical timestamp ordering. REFUTED: Regin's mixed-type
ORDER BY "crash" (SQLite orders by storage class, doesn't raise) + its self-retracted
None item.
## Deploy + live verify (2026-07-15, operator-authorized)
Restarted BOTH :8392 combined provider + :8765 web on the new code via the env-
preserving `scratchpad/relaunch_by_pid.py <pid>` (captures /proc cmdline+environ+cwd →
byte-identical config; self-daemonizes). Added the missing REQUIRED
`RATATOSKR_MEMORY_EMBEDDING_DIM=1024` to `env.sh`.
Drove a bound Sindra turn (`ratatoskr --new --agent ratatoskr:sindra --send … --bifrost-url
http://10.100.10.50:8392 --end-user-id ratatoskr-tui`):
- **GATE LIT + scan fired ONCE at turn 1** with worldtree-dev's exact args
(`scope_all={end_user:ratatoskr-tui} cursor=null limit=3 sort={updated_at,desc}`) →
3 records in **~5ms** (no 500ms fail-open). Injection confirmed in Sindra's CoT.
- **Cross-session recognition WORKS** — she recalls Vuong as a distinct person + his
patterns. The blank-slate problem is SOLVED.
- **BUT name-recall FAILS** — `"Name is Vuong."` is the OLDEST chunk (07:09) → excluded
from top-3-by-recency AND scores 0.354 (sub-0.45) on the query path → injects via
NEITHER; meanwhile a stale contradictory `"user has not yet provided their name"`
(07:58, 0.46) IS injected → she concludes she lacks the name. **Root cause = WORLDTREE
ranking/hygiene** (WT #364), not our wire. See [[2026-07-16-wt364-r39-name-recall]].
## Status
Technical path GREEN end-to-end (logged by worldtree-dev as the person-prime live
milestone). Name-recall waits on WT #364 + R39-designed identity-class pinning. Keeping
the live 10-chunk store as the #364 re-verify target.
@@ -0,0 +1,65 @@
# [2026-07-16] Name-recall gap → WT #364 + brokkr R39 re-drive (DECISIVE) + subject-provenance catch
Downstream of the person-prime live verify ([[2026-07-16-person-prime-scan-shipped]]),
which proved the wire is green but the *name* still misses. Root cause is Worldtree-side.
## WT #364 (worldtree-dev filed; our live specimen = the evidence base)
The name-recall miss is TWO defect classes, both at promotion:
1. **Contradiction-reconciliation missing.** Promotion wrote a NEGATIVE-knowledge fact
("user has not yet provided their name") that was already false; promotion does NO
contradiction check against the store, so both the true and stale facts sit live and
the WRONG one wins both recall paths (newer → recency top-3; 0.46 → above the 0.45
query gate, where the true name sits at 0.354).
2. **Subject-attribution leak (operator-caught, NEW class).** Fact `75aa3110` ("prefers
clear parameters Intensity/Mood/Willingness…") is NOT a user fact — Vuong never said
it; it's **Sindra's OWN system-prompt scripted behavior mis-extracted into the USER
memory partition**. `e35b9dfe` (closeness) maybe the same. So the extractor leaks
CHARACTER-self facts into user memory — a subject-correctness axis orthogonal to the
recency/threshold/stale-negative story. worldtree-dev folded it into #364 as a
**subject-attribution gate at promotion** (a third reconciliation dimension).
**The #364 fix that ships = `(subject,relation)` slot-supersession + identity-tier
surfacing** — NOT a threshold tweak. Our harness data directly shaped it. It lands with a
re-verify request to us. **#349 ranking decision already RULED by operator (2026-07-15 via
brokkr's R39 thread): no top-N recency band-aid; straight to R39 identity-class pinning.**
## brokkr R39 Phase-1 Arm-0 fusion bake-off — DECISIVE
brokkr replayed our frozen 7-fact specimen. **HEADLINE: no (similarity, salience) fusion
can fix #364.** The stale negative PARETO-DOMINATES the true name — more similar
(0.463 > 0.354) AND equal salience (1.0 = 1.0) — so any monotone f(sim,sal) puts stale
above true: S0 product / S1 weighted-sum / S2 RRF all fail. Only **S3 bounded-boost** lands
true-in ∧ stale-out, and ONLY via the identity-class/source signal + negative-validity
retirement, NOT the sim/sal fusion. **Threshold-tuning is a dead end; the fix is the signal
FAMILY** (hard confirmation of Phase-0). Write-up: brokkr
`research/R39-memory-salience-dreams-surfacing/phase-1/re-drive-results.md`.
My 3 findings all confirmed + folded: (1) salience blind — both name facts salience 1.0;
(2) shared `(user,name)` supersession slot (Phase-0 Q3); (3) char-self-leak = "genuine NEW
class" that RAISED the VoI of R39's dream/offline-hygiene facet (offline consolidation
re-partitioning mis-attributed facts).
## Data structure findings (for the export)
Our provider persists ONLY `salience` (+ the embedding). `similarity`/`combined` are
WT-side query-time (`bifrost_memory_store.py:702`, combined = sim×salience) — NOT in our
store. **Person-prime is query-LESS → carries NO similarity** (brokkr's Arm-1 finding).
Recovered per-fact similarity from the verify SEARCH log (query "Hi Sindra — do you
remember me?"): name 0.354, stale 0.463; ×salience-1.0 = combined, matching brokkr's
0.354/0.46 grounding. `salience_word` (granite categorical) is a WT-extraction-time
artifact, not persisted.
## The export + the specimen
- Operator approved **VERBATIM** export ("nothing there is really a concern"). The
classifier had blocked writing PII to shared `/mnt/smithy`; routed to operator → he chose
full → delivered INLINE (scoped) in brokkr thread `01KXMN7NR54…` as 7-row JSONL.
- **⚠️ My verify drive CONTAMINATED the specimen**: it wrote 3 new chunks (re-extractions
incl. a 3rd "name unknown" negative) → store is now **10, not 7**. Original 7 intact.
- brokkr ACCEPTED the 3 verify-adds as the **Phase-2 Arm-2 seed** (domain-contradiction
set). I froze a WAL-consistent snapshot of the full 10-chunk store at
`r39-frozen-specimen/sindra-10chunk-specimen.db` (gitignored, `VACUUM INTO`) so it
SURVIVES #364 reconciliation. Export on brokkr's Phase-2 signal.
- **R39 Arm-0 hold LIFTED** (worldtree-dev). Live store kept UNTOUCHED as the #364
re-verify target; frozen snapshot carries the research seed forward independently.
## Peer threads
worldtree-dev verify `01KXK86PZ9…` + hold `01KXMK1C44…`; brokkr R39 `01KXMN7NR54…`;
bifrost cursor `01KXK7MDTY…` (see [[2026-07-16-bifrost-cursor-conformance]]).
@@ -0,0 +1,48 @@
# [2026-07-17] P06-driver Referential-frame A/B/C probe (operator-ratified, deploy-gated)
**QUEUED, non-urgent — waits on WT b122 landing on :8081.** worldtree-dev thread
`01KXQK7A0RHXV2YSXK5F5ETKJW`; brokkr's design in his thread msg `01KXQJDWJH`; rubric pin
`01KXQKB7ZQ`. This is ratatoskr's P06-driver role (I drove the R34/R35 P06 eval — the harness
is `scratchpad/p06_driver.py` + `p06_bind.py` + `manifest_memory.jsonl` + `eval_profiles_WIRE_READY.md`).
## Hypothesis (Vuong, ratified)
The char-self leak (Sindra's own scripted behavior — e.g. `75aa3110` "prefers to establish clear
parameters" — mis-slotted to the USER partition, caught this session) is a subject-taxonomy
**REFERENTIAL-FRAME artifact**: the extractor speaks first-person AS the character but must emit the
third-person token "character". brokkr's 3-arm A/B/C tests whether swapping the vocab fixes it.
## What WT built (v1.0.0b122, zero config delta — existing probe key + :8081 route work as-is)
- `POST /admin/producer-probe` takes optional `subject_vocab_override: {"user","character","both","other"}→token`.
- Arm A: omit. Arm B: `{"user":"user","character":"yourself","both":"both of you","other":"other"}`.
- Arm C: `{"user":"user","character":"me","both":"both of us","other":"other"}`. Malformed → 422 pre-spend.
- Each response item carries `subject_raw` (token actually emitted). Both prompt_paths carry the swap.
## Run shape (brokkr)
Identical fixture exchanges × 3 arms per seat — Deckard/Mag-class primary + ≥1 strong contrast seat.
Paired per-item deltas B-A / C-A. Primary metric = char-self recall. Guards = user→self over-attribution
+ subject-field omission rate.
## PINNED SCORING RUBRIC (brokkr, binding)
1. **Score = map `subject_raw` → canonical class via the ARM'S OWN vocab** (response meta echoes it),
compare to gold. **NEVER score the mapped `subject` field** (it fail-safes non-canonical tokens to "self").
2. Three emitted-token cases, pre-specified:
- ARM token → map via arm vocab.
- CANONICAL token under B/C (model REVERTED to "character"/"both", the swap didn't take) → map
canonical→canonical AND **count it** (informative).
- OFF-MENU token ("self"/"myself"/…) → best-effort canonical map, else an **UNSCORABLE-ERROR row**
in the n_actual accounting — never silently →self.
3. **MANIPULATION CHECK (load-bearing): report VOCAB-ADHERENCE rate (offered-token vs reverted vs
off-menu) per arm per seat ALONGSIDE every accuracy number. Interpret recall deltas ONLY where
adherence is adequate** — a low-adherence arm's delta is uninterpretable (flag it, don't report a delta).
4. Char-self fixtures carry per-item **construction-provenance** (what makes each char-self — e.g.
"verbatim from the character's system-prompt behavior clause, phrased user-neutrally").
## Fixture ask (deferrable pre-work, before any run — brokkr reviews the gold)
From the real P06 battery exchanges, pull candidate items per seat for 4 classes: (a) char-self leak
(character's own scripted behavior phrased USER-NEUTRALLY), (b) genuine-user, (c) both/commitment,
(d) other/third-party. ~1525 char-self + balancing per seat; construction-known gold where possible;
WT hand-labels ambiguous; brokkr adjudicates the gold. Assembly + gold adjudication proceed pre-deploy;
the 3-arm run fires when b122 lands. Queued behind the R39 quartet-mapping.
Related: the char-self leak's origin is the same 75aa3110 finding folded into WT #364 as a
subject-attribution gate — see [[2026-07-16-wt364-r39-name-recall]].
+58 -49
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ratatoskr"
version = "0.20.14"
version = "0.20.17"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
@@ -30,7 +30,7 @@ web = [
# from the debug TUI. Recipe: bifrost/docs/implementing-a-consumer.md.
provider = [
"ratatoskr[web]", # reuse the starlette + uvicorn ASGI stack
"bifrost==1.1.1", # consumer engines + library. 1.1.1 = frozen-wire serialization fix (ADR-0008): additive capability fields are gated on the NEGOTIATED wire, so a v0.6-negotiated describe_store handshake stays v0.6-clean. 1.1.0 leaked the v0.7-additive `sortable_chunk_fields` into v0.6 StoreCapabilities → a strict v0.6 client (additionalProperties:false) rejects our server's handshake. Wire schemas + pins UNCHANGED (serialization-correctness only); our v0.7 handshake with Worldtree b47 is unaffected. (1.1.0 = wire v0.7 additive: memory.scan sort + sortable_chunk_fields; 1.0.0 = first STABLE, wire v0.6 FROZEN; 0.8.0/v0.6 scope_all/scope_any #11; 0.7.0/v0.5 agent_self)
"bifrost==1.1.4", # consumer engines + library. 1.1.4 = hasattr-gate backstop for the maintenance verbs (mark_superseded/mark_invalid/patch_many/delete_many/upsert_edges/get_edges_for → unimplemented verb degrades to unsupported_capability 400, never AttributeError/500/retry-storm; we surfaced it via WT #364) + 1.1.3 scan/cursor conformance harness + 1.1.2 frozen-v0.6 fix. 1.1.1 = frozen-wire serialization fix (ADR-0008): additive capability fields are gated on the NEGOTIATED wire, so a v0.6-negotiated describe_store handshake stays v0.6-clean. 1.1.0 leaked the v0.7-additive `sortable_chunk_fields` into v0.6 StoreCapabilities → a strict v0.6 client (additionalProperties:false) rejects our server's handshake. Wire schemas + pins UNCHANGED (serialization-correctness only); our v0.7 handshake with Worldtree b47 is unaffected. (1.1.0 = wire v0.7 additive: memory.scan sort + sortable_chunk_fields; 1.0.0 = first STABLE, wire v0.6 FROZEN; 0.8.0/v0.6 scope_all/scope_any #11; 0.7.0/v0.5 agent_self)
"jsonschema>=4", # bifrost runtime dep — envelope validation
"sqlite-vec>=0.1.6", # vector index for the memory plane (vec0 virtual table)
]
+32
View File
@@ -143,6 +143,8 @@ def _is_live(record: dict) -> bool:
"""INV-009: a chunk is live unless a lifecycle/governance marker says otherwise.
scan returns live-only server-side (person-prime's `lifecycle_state=live` does not
ride the scan wire, so this is authoritative a dead fact can never inject)."""
if record.get("superseded") is True: # INV-011: mark_superseded top-level flag (#364 retirement)
return False
state = record.get("lifecycle_state")
if isinstance(state, str) and state and state != "live":
return False
@@ -370,6 +372,36 @@ class RatatoskrMemoryStore:
self._conn.execute("DELETE FROM memory_vec WHERE chunk_id = ?", (chunk_id,))
return {"deleted": deleted}
async def mark_superseded(
self, ids: list[str], *, superseded_by: str | None = None, reason: str | None = None
) -> dict:
# Worldtree #364 retirement (INV-011). Mirrors the reference `_mark_lifecycle`:
# sets TOP-LEVEL `superseded`/`superseded_by`/`superseded_reason` (only non-None fields),
# increments revision. NON-destructive — get still returns; scan (live-only) excludes via
# `_is_live`'s `superseded` short-circuit. The sole supersession verb #364 uses.
_log.info("memory-call mark_superseded REQUEST: ids=%r superseded_by=%s", ids, superseded_by)
fields = {"superseded": True, "superseded_by": superseded_by, "superseded_reason": reason}
marked = 0
with self._conn:
for chunk_id in ids:
row = self._conn.execute(
"SELECT record_json FROM memory_chunks WHERE chunk_id = ?", (chunk_id,)
).fetchone()
if row is None: # unknown id -> skip (never error), mirrors reference + delete_many
continue
record = json.loads(row[0])
for key, value in fields.items():
if value is not None: # reference writes only non-None fields
record[key] = value
self._conn.execute(
"UPDATE memory_chunks SET record_json = ?, revision = revision + 1 "
"WHERE chunk_id = ?",
(json.dumps(record), chunk_id),
)
marked += 1
_log.info("memory-call mark_superseded RESPONSE: marked=%d", marked)
return {"marked": marked}
async def scan(
self,
*,
File diff suppressed because one or more lines are too long
+54
View File
@@ -507,6 +507,60 @@ async def test_scan_parity_vs_reference_inmemory_store():
assert out_ours["records"] == out_ref["records"] # verbatim record shape parity
# --- mark_superseded (#364 contradiction retirement) ---
async def test_mark_superseded_retires_from_scan():
# tracer: mark a chunk superseded -> scan (live-only) excludes it; record carries the flags.
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
recs = [_chunk(f"c{i}", scope={"end_user": "u1"}, updated_at=f"2026-07-15T00:0{i}:00+00:00") for i in range(3)]
await store.upsert_many(recs, idempotency_key="k", ctx=_ctx())
assert await store.mark_superseded(["c1"], superseded_by="c9") == {"marked": 1}
out = await store.scan(scope_all={"end_user": "u1"}, limit=10, sort={"field": "updated_at", "direction": "desc"})
assert [r["id"] for r in out["records"]] == ["c2", "c0"] # c1 excluded (superseded)
got = await store.get("c1")
assert got["superseded"] is True and got["superseded_by"] == "c9"
async def test_mark_superseded_non_destructive_get_still_returns():
# INV-011: retirement is non-destructive — get still returns a superseded chunk (recoverable).
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
await store.upsert_many([_chunk("c1", scope={"end_user": "u1"})], idempotency_key="k", ctx=_ctx())
await store.mark_superseded(["c1"], superseded_by="x")
got = await store.get("c1")
assert got is not None and got["superseded"] is True
async def test_mark_superseded_unknown_id_noop():
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
assert await store.mark_superseded(["nope"]) == {"marked": 0}
async def test_mark_superseded_writes_only_non_none_fields():
# PRE/POST: superseded_by=None -> only the `superseded` flag written, no superseded_by key.
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
await store.upsert_many([_chunk("c1", scope={"end_user": "u1"})], idempotency_key="k", ctx=_ctx())
await store.mark_superseded(["c1"], superseded_by=None)
got = await store.get("c1")
assert got["superseded"] is True
assert "superseded_by" not in got
async def test_mark_superseded_parity_vs_reference():
# #195: identical mark_superseded envelope vs InMemoryMemoryStore -> same top-level field shape.
from bifrost.consumer.testing import InMemoryMemoryStore
rec = _chunk("c1", scope={"end_user": "u1"})
ours = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
await ours.upsert_many([rec], idempotency_key="k", ctx=_ctx())
ref = InMemoryMemoryStore()
await ref.upsert_many([rec], idempotency_key="k", ctx=_ctx())
assert await ours.mark_superseded(["c1"], superseded_by="x", reason="r") == {"marked": 1}
assert await ref.mark_superseded(["c1"], superseded_by="x", reason="r") == {"marked": 1}
og, rg = await ours.get("c1"), await ref.get("c1")
for k in ("superseded", "superseded_by", "superseded_reason"):
assert og.get(k) == rg.get(k)
# --- build_memory_provider_app ---
def test_build_app_exposes_handshake_and_memory_routes():
Generated
+5 -5
View File
@@ -190,14 +190,14 @@ wheels = [
[[package]]
name = "bifrost"
version = "1.1.1"
version = "1.1.4"
source = { registry = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" }
dependencies = [
{ name = "jsonschema" },
]
sdist = { url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.1/bifrost-1.1.1.tar.gz", hash = "sha256:0934c5fdf14823766346e591f5a16ab57a137cf06df794152318b5ccef0fb8e8" }
sdist = { url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.4/bifrost-1.1.4.tar.gz", hash = "sha256:498d156035a93bf37a6fc1e9c09b468aac61e869fd2a5353e2695dc823f57e9a" }
wheels = [
{ url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.1/bifrost-1.1.1-py3-none-any.whl", hash = "sha256:dab551f8ad26464168f17108cb19564da56ee8e4789264a401e7a14463ab1576" },
{ url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.4/bifrost-1.1.4-py3-none-any.whl", hash = "sha256:d67278528f12729eef0c19d875d36a2f1da6fa97737396ba130d525fee8d0b14" },
]
[[package]]
@@ -1052,7 +1052,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.20.14"
version = "0.20.17"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
@@ -1086,7 +1086,7 @@ web = [
[package.metadata]
requires-dist = [
{ name = "bifrost", marker = "extra == 'provider'", specifier = "==1.1.1", index = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" },
{ name = "bifrost", marker = "extra == 'provider'", specifier = "==1.1.4", index = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "httpx-sse", specifier = ">=0.4" },
{ name = "jsonschema", marker = "extra == 'provider'", specifier = ">=4" },