Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be2c577884 | |||
| c0d00ccd18 | |||
| 722d6c76de | |||
| 56f4895881 | |||
| ca249e4986 | |||
| faf605fdf1 | |||
| 7c8644dc45 | |||
| 195292156f | |||
| abe1b52002 | |||
| e365b24339 | |||
| 5e3e88d26d | |||
| ce6907bd73 | |||
| e2b2f51364 | |||
| 0f2b28919a | |||
| b154bb3885 | |||
| 46d6efa962 | |||
| f46ccbae1c | |||
| 772fad18b4 | |||
| 8199774405 | |||
| 66ba06875e | |||
| 25ccb5c75b | |||
| 22e7a1b0e7 | |||
| a9c521a48a |
@@ -119,10 +119,14 @@ interpreted.
|
||||
`describe_store` advertises ONLY what is implemented: `relational_edges_supported=False`,
|
||||
`atomic_supersede_supported=False`, `transaction_supported=False`,
|
||||
`optimistic_locking_supported=True`, `filterable_metadata_fields=[]`,
|
||||
**`sortable_chunk_fields=[{"name": "updated_at"}]`** (the ONLY globally-sortable field;
|
||||
gates `scan`'s sort at the bifrost dispatch `_validate_scan_sort` AND Worldtree's #349
|
||||
person-prime Branch-A `"updated_at" in caps.sort_fields_supported` — advertising it is
|
||||
what lights up turn-1 durable-fact injection).
|
||||
**`sortable_chunk_fields=[{"name": "updated_at", "type": "timestamp"}]`** (the ONLY
|
||||
globally-sortable field; gates `scan`'s sort at the bifrost dispatch `_validate_scan_sort`
|
||||
AND Worldtree's #349 person-prime Branch-A `"updated_at" in caps.sort_fields_supported` —
|
||||
advertising it is what lights up turn-1 durable-fact injection). Both `name` AND `type`
|
||||
are REQUIRED by the bifrost `handshake_response` `SortableChunkField` schema
|
||||
(`additionalProperties:false`) — omitting `type` fails wire-schema validation and breaks
|
||||
the ENTIRE handshake (memory + affect bind), not just the sort; `type` is advisory-only
|
||||
(the wire never interprets it).
|
||||
(`transaction_supported` is the bifrost **wire-level** multi-op transaction
|
||||
capability — NOT our internal SQLite transactions, which we use for atomic
|
||||
batches.) The client gates the gated verbs off these.
|
||||
@@ -145,6 +149,27 @@ interpreted.
|
||||
`updated_at desc`), matching bifrost's cross-pagination conformance negative. The sort
|
||||
field is indexed (`json_extract(record_json, '$.updated_at')`) so the read stays within
|
||||
person-prime's 500 ms fail-open budget.
|
||||
- **Cursor is v1-provisional (KNOWN DEVIATION — offset, not snapshot).** The cursor is a
|
||||
bare integer offset into the re-derived global order. This is CORRECT and conformant for
|
||||
the **single-page** person-prime call (`cursor=None`), which is the only shipped consumer.
|
||||
It **diverges from bifrost's protocol snapshot-cursor contract on multi-page continuation**:
|
||||
the dispatch engine (`bifrost.memory` scan branch) drops the `sort` arg on a cursor
|
||||
continuation because "the cursor's snapshotted order is authoritative", and maps
|
||||
`ScanCursorExpired → 410`. Our offset cursor (a) does NOT snapshot the order — a page taken
|
||||
after a concurrent write can duplicate/drop rows relative to the first page (heid-bug-hunt
|
||||
2026-07-15, all 3 arms), and (b) never raises `ScanCursorExpired`. The `global_before_paginate`
|
||||
/ cursor test asserts **static-store** behavior only. The durable/conformant fix is to adopt
|
||||
the reference `InMemoryMemoryStore`'s snapshot-cursor semantics (opaque token + frozen ordered
|
||||
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
|
||||
|
||||
@@ -181,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.
|
||||
@@ -288,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]]).
|
||||
+81
-37
@@ -1,6 +1,11 @@
|
||||
# Persistent memory — ratatoskr
|
||||
|
||||
_Last updated: 2026-07-13_
|
||||
_Last updated: 2026-07-16_
|
||||
|
||||
> **Always check for `/tmp/ratatoskr-dev-handoff.md`** — if it exists and its
|
||||
> `Written:` stamp is under an hour old, read it (it carries the in-flight
|
||||
> handoff from the previous session), then delete it. Older than an hour:
|
||||
> stale — delete it unread.
|
||||
|
||||
This file captures durable intent and supporting evidence (goals, decisions,
|
||||
foot-gun warnings, in-flight state) across context resets. Read it at session
|
||||
@@ -39,47 +44,74 @@ upstream API key stays server-side (INV-003).
|
||||
|
||||
## Current state / in-flight
|
||||
|
||||
_As of 2026-07-13 (this session):_
|
||||
_As of 2026-07-16:_
|
||||
|
||||
**✅ THIS SESSION — WT #355 validation CLOSED + environment cleaned for a Sindra run.** The #355 re-trigger loop-in obligation is **DISCHARGED**: the fully-instrumented re-drive ran and the fix is **CONFIRMED** (detail in the #355 Recent-decisions entry). Personal `:8081` is now on **b61** — carries BOTH the #355 STICK fix (b60) and the orthogonal over-budget *trigger* fix (b61); the resume-durability gap surfaced during the drive is tracked as **WT #356**. Then, operator-directed *"clean up everything + prep for a Sindra run"*: provider stores **RESET to 0/0** (`reset-sindra-stores.sh`, rolling backup in `db-reset-backup/`), the throwaway `ratatoskr:memprobe` agent **DELETED** (reverses the prior KEEP), `ratatoskr:sindra` verified present + persona-intact on b61. **Environment is Sindra-run-ready:** web `:8765` up, combined provider `:8392` up + empty (single healthy instance), `:8081` healthy. Operator is driving the run interactively; the ratatoskr althing monitor is armed. **Foot-gun for the run:** a fresh Sindra session returning `agent_not_available` = the #356 resumed-session-snapshot gap (fix: fresh session / retire stale) — see Tried/abandoned.
|
||||
**✅ person-prime `scan` build SHIPPED + DEPLOYED + LIVE-VERIFIED (v0.20.14).** The
|
||||
cross-session name-recall fix. `scan` verb + `sortable_chunk_fields` cap implemented,
|
||||
deployed to the running :8392 provider (+ :8765 web), and live-verified by driving a bound
|
||||
Sindra turn: gate lights, scan fires turn-1 (~5ms), injection confirmed, **cross-session
|
||||
recognition works** (blank-slate gone). **Name-recall specifically still misses** — root
|
||||
cause is WORLDTREE-side ranking/hygiene (WT #364), NOT our wire. Full arc:
|
||||
`persistent-memory.d/2026-07-16-person-prime-scan-shipped.md`.
|
||||
|
||||
_As of 2026-07-12:_
|
||||
**Standing — awaiting two peer-initiated touchpoints (both monitored):**
|
||||
- **WT #364 re-verify DONE (2026-07-16, b103) — READOUT PASSES, but a real gap surfaced.** Drove 3
|
||||
"my name is Vuong" turns; person-prime turn-1 top-3 is now all name-POSITIVES (no "name unknown"; the
|
||||
true name even surfaces) → **Sindra recalls the name now**. BUT "wrong readout gone" is via **RECENCY
|
||||
EVICTION** (promotion re-upserted the positive fresh into the window), NOT supersession — the negatives
|
||||
are UNCHANGED (governance=available). **#364 calls `mark_superseded` on the consumer store, which our
|
||||
v1 provider DOESN'T implement → 11× AttributeError/HTTP-500 + a RETRY-STORM bloating the live store
|
||||
10→19 dup name-positives.** Asked worldtree-dev to halt the reconciliation retry + confirm the
|
||||
`mark_superseded` wire shape; flagged bifrost-dev (dispatch doesn't hasattr-gate `mark_superseded`).
|
||||
**✅ GC DONE (2026-07-16): storm self-stopped at 29 chunks / 18 total 500s; delete_many'd the 19 storm
|
||||
re-extractions → back to the pre-drive 10-chunk specimen (0 orphan vec rows, negatives + original
|
||||
name-positive intact). **#364 CLOSED worldtree-side (b105; my interim note recorded verbatim on the issue).** ⚠️ Post-GC the readout advantage is gone too
|
||||
(person-prime top-3 back to the 3 newest pre-drive rows incl. cbbc7bdd "name unknown") — the
|
||||
recency-eviction was only a transient side-effect of the re-assertion. **DURABLE name-recall now
|
||||
genuinely depends on implementing `mark_superseded` (next).**
|
||||
- **NEW TASK (operator-sequenced): implement `mark_superseded` in the provider** so #364's retirement
|
||||
lands durably (not just recency-evicts). **Wire shape CONFIRMED (worldtree-dev, `bifrost_memory_store.py:293`):**
|
||||
op `"mark_superseded"`, args `{"ids": ["<chunk_id>"], "superseded_by": "<new_chunk_id>"}` — `ids` a LIST
|
||||
(WT sends singletons, one call per retired chunk), NO `reason` field. It is the **SOLE** supersession
|
||||
verb #364 uses (atomic_supersede = dream-lane, cap-gated separately; patch_many never for retirement) —
|
||||
so build JUST `mark_superseded` + advertise it + live re-verify. Retry-storm ROOT-CAUSED WT-side (a
|
||||
failed retirement-mark wrongly failed the whole promotion run → idle re-plan loop; WT's fix DEGRADES
|
||||
mark-failures to an audited no-op → self-stops on the first post-deploy idle cycle, ETA ~20min from
|
||||
2026-07-16T20:30Z, no action our side). **GC the ~9 dup name-positives back to the pre-drive 10-chunk
|
||||
state (via delete_many) AFTER the storm stops; then worldtree closes #364.** Once our `mark_superseded`
|
||||
ships, a single name re-assertion self-heals retirement (incl. any residual dupes).
|
||||
- **R39 Phase-2 Arm-2 export** (brokkr-initiated): the 10-chunk contradiction specimen is frozen at
|
||||
`r39-frozen-specimen/sindra-10chunk-specimen.db` (gitignored, VACUUM INTO) — survives #364
|
||||
reconciliation; export on brokkr's Phase-2 signal. Full R39/#364 arc:
|
||||
`persistent-memory.d/2026-07-16-wt364-r39-name-recall.md`.
|
||||
|
||||
**✅ COMPLETE — R34/R35 P06 powered memory-half eval (driven, scored, mechanism validated; ratatoskr drive-role CLOSED both sides).** ratatoskr drove all **308 memory runs** (divergence 168 / floor 80 / sliding 60) through personal WT's live producers, dropped `memory_results.jsonl` (sha256_16 `cbabaf16979cb4ec`) to brokkr's P06 `results/` dir, and brokkr scored it (**R35.45**, findings + verdict committed brokkr-side). **Headline: the authored `psychological_profile` IS the mechanism** — salience-divergence authored **0.618** vs stripped **0.235 ≈ null (0.25)**, delta **+0.382**; the OCEAN scaffold alone does NOT differentiate (negative control HOLDS). Q1 primary is a REAL effect (above the 0.40 noise-floor) but **inconclusive on strength** (0.618 < the preregistered 0.70 bar) — the 0.62→0.70 lift is a FUTURE optimization phase (brokkr's lever bet: richer formative-history seeds per P03), a cheap re-drive on the same proven harness when it preregisters. Secondaries hold: Q3 firewall **0.978** grounded, Q2 floor 0.938, Q6 sliding parity +0.049 (n=12 after 7 `deferred_budget` sliding exclusions — the budget hazard we flagged landed), Q4 affect Deckard 0.75 / Magidonia 0.70 (graded, within noise; banked earlier as `affect_results.jsonl`). Two ratatoskr flags landed materially: the stripped-is-not-empty correction caught a false Q3 firewall-fail (0.562→0.978), and the Q5 disambiguation question became the headline win. Threads: vendor/verdict althing `01KXD39NWW05`, eval thread `01KXAN073B`. Standing offer to brokkr: second-eyes on the 2 borderline Q1 calls IF the Selene blind-judge flags them.
|
||||
**Open operator-sequenced task (NOT urgent):** adopt the bifrost snapshot-cursor (bifrost-dev
|
||||
ruled it NORMATIVE, offset not blessed; conformance harness shipped in bifrost 1.1.3). Single-page
|
||||
person-prime is already conformant, so nothing shipped is broken. Details + TODO:
|
||||
`persistent-memory.d/2026-07-16-bifrost-cursor-conformance.md`.
|
||||
|
||||
**The eval harness (PROVEN + reusable for the optimization-phase re-drive):** `scratchpad/p06_driver.py` (two-path — memory via `POST /admin/producer-probe {agent_id, messages, prompt_path}`; affect via bound-turn + `:8392` /affect/state poll; per-run isolation, `--pace-seconds`, abstain-aware, psych_profile_present binding-tripwire) + `p06_bind.py` (defines the 6 eval agents: sindra/Torvald auth+strip on Deckard, Ilva on Deckard+Magidonia) + `p06_bindings.json` + `eval_profiles_WIRE_READY.md` (sindra relational / Torvald operational-opposite / Ilva high-N) + `manifest_memory.jsonl` (the 308 memory runs, filtered from brokkr's canonical 348). Binding integrity was PERFECT on the drive: psych_profile_present authored 154/154 True, stripped 154/154 False, 0 mismatches, 0 probe-errors. Probe key at `~/.config/ratatoskr/probe.env` (mode 600, scope `admin.memory.probe`).
|
||||
**Substrate / environment (current):** branch `main`, HEAD is the person-prime tip (v0.20.11→v0.20.14
|
||||
all committed; **NOT pushed** — push is the operator's call); origin `git@gitea.phasefinal.com:vh/ratatoskr.git`.
|
||||
bifrost `==1.1.1` / wire v0.7 (**next bump target = `1.1.4`**: hasattr-gate backstop for the maintenance
|
||||
verbs [degrade to 400 not 500/retry-storm, bifrost-dev 2026-07-16] + the 1.1.3 scan/cursor conformance
|
||||
harness; pin as part of the next provider-maintenance batch — mark_superseded / cursor adoption);
|
||||
Worldtree openapi vendored 2.3.0; suite
|
||||
**639 green**. Personal WT on **b79** (client-side live-only + additive `lifecycle_state` scan arg,
|
||||
which our INV-009 accept-and-ignores). The combined **:8392** provider (memory+affect) + **:8765** web
|
||||
are THE surfaces, run as dev-box BACKGROUND SHELLS — restart via `scratchpad/relaunch_by_pid.py <pid>`
|
||||
(env-preserving, self-daemonizing; find pid via `ss -ltnp | grep <port>`). `env.sh` now sets the
|
||||
REQUIRED `RATATOSKR_MEMORY_EMBEDDING_DIM=1024` (was missing → bare `source env.sh` restart crashed).
|
||||
Keys env-only mode-600 in `~/.config/ratatoskr/provider.env` + `RATATOSKR_ADMIN_API_KEY` (7 read
|
||||
scopes, personal-:8081-only; Heimdall keys per-instance). `graphify-out/` runs dirty (auto-regen,
|
||||
never stage). v1 = full Worldtree I/O coverage, cuts when WT tags 1.0 (`docs/coverage-map.md`).
|
||||
|
||||
**Deckard memory extraction is REASONING-OFF (operator-directed 2026-07-13, LIVE):** the memory extractor sends `chat_template_kwargs.enable_thinking:false` on the char-rp-reasoning seat → ~5s extraction, not the 45s verbose-CoT hang. **Scoped to the memory extractor ONLY — affect + RP stay reasoning-ON.** Landing it took an infra-ops surgical `docker restart` of personal `:8081` (ModelRegistry boot-caches providers.yaml at `__init__`, so a same-image redeploy is a config-reload NO-OP — see Tried/abandoned).
|
||||
|
||||
**✅ DISCHARGED — WT #355 validation CLOSED (2026-07-13).** ratatoskr's telemetry root-caused the char-rp-reasoning turn-never-terminates wedge; I coordinated the fully-instrumented re-drive (infra-ops armed netns-pcap + py-spy; soong drove the 8-turn accumulating RP-with-tools repro) and the fix is CONFIRMED — wedging turns cancelled cleanly at the 300s stall-watchdog (Slice-C cancel-INDEPENDENT terminal) vs the pre-b60 16-min-no-terminal baseline. b61 additionally fixed the orthogonal over-budget trigger; resume-durability gap → WT #356. Full record in Recent decisions. (Prior-cycle framing: the wedge was an over-budget `trim_messages` return + terminal-suppression from the stall-watchdog cancel stuck in httpx `AsyncShieldCancellation`.)
|
||||
|
||||
_The detail below (the v0.20.x web-UI arc, #347 authored-history, sindra memory-fix) is PRIOR-CYCLE shipped history — superseded by this section's top; kept for reference, prune in a future snapshot._
|
||||
|
||||
**✅ SHIPPED — web-UI iteration-3, all three queued items (`v0.20.7`, patch, agent-discretion; 631 tests green; :8392 combined provider + :8765 web both restarted on the new code).**
|
||||
|
||||
**(A) Design prototype iteration-3 adapted into `index.html`** (re-pulled `Ratatoskr Console.dc.html`, project `bc0b65d1-…`): (1) sparkline **grid bg** — `<pattern id=sparkGrid>` in the hidden defs + a bg `<rect fill=url(#sparkGrid)>` behind every relation-row spark polyline; (2) **PAD strips → per-turn Δ bars** — REMOVED the vertical polyline strip (`stripPoints`/`proj3` gone) for `padDeltas`→`deltaStrip`: a 12-cell HTML column (newest at bottom) of diverging bars offset L/R of a center line by that turn's Δ (magnitude→width, age→opacity, zero→faint center dot); head legend now "Δ/turn · last 12 · newest ↓"; (3) **mood orbit → DIMETRIC OPEN BOX** (viewBox 124×140, az35/el25, D-right/A-left-back/P-up) — ghost A×P wall (P readout) + D×A floor, `orbitProj/orbitShadowY/orbitWallPt/orbitAxisPt` projections, **JS-driven animated replay** (`orbitFrame` rebuilt per rAF by a singleton `startOrbitAnim` reading live `ORBIT_HIST`; reduced-motion → static final-state; no SMIL). Playwright-verified (dimetric frame + 17 dyn children + 21 Δ-bars + 5 grid rects; dark+light screenshots).
|
||||
|
||||
**(B) Memory viewer SHIPPED + the 0/0 mystery ROOT-CAUSED.** New non-bifrost debug read `GET /memory/chunks?agent_id=&end_user_id=` on the combined `:8392` provider (`RatatoskrMemoryStore.list_chunks` + `count_chunks` + shared `add_memory_read_route`, wired into `build_memory_provider_app` + `combined.py`; **end_user STRICT, agent_id LENIENT** so `{end_user}`-only WT chunks aren't hidden; returns `{chunks,count,total}`, empty=200 not 404) → web proxy `GET /api/memory/chunks` (server-supplied end_user, new `RATATOSKR_MEMORY_READ_URL` env, default `:8391`, set to `:8392` in env.sh) → a live-polling MEMORY console pane (`loadMemory`/`renderMemory`/`setMemHead`, polled on open + post-turn). **ROOT CAUSE of the operator's 0/0** (settled via a bound 6-turn probe + op-feed): the Bifrost memory plane **binds and grants fine** (handshake `caps_requested:[affect,memory]` → `caps_granted:[memory,affect]`), but **sindra dispatches ZERO memory ops** (not even a recall search) — her reset-clean agent definition has **no `memory:{}` block**, so Worldtree never runs the memory pipeline for her. NOT a bind-grant failure, NOT promotion-timing. **PROVEN end-to-end** with a throwaway memory-enabled `ratatoskr:memprobe` (defined with `memory:{}`): 6 bound turns → 6 `memory.search` recalls + **4 `memory.upsert_many` → 4 real chunks in `memory.db`** → the pane renders all 4 (content·scope·origin·revision) live. **⚠ LEFTOVER debug state (operator chose KEEP):** `ratatoskr:memprobe` agent on personal WT + test chunks (scope `{end_user:ratatoskr-tui}`) sit in the live `memory.db` — harmless (make the pane show real data); `scripts/reset-sindra-stores.sh` or `DELETE /agents/ratatoskr:memprobe` clears them. **✅ SINDRA MEMORY FIXED (operator-approved, 2026-07-07):** DELETE+redefined her WITH `memory:{}` (prompt byte-identical, `role=character`, OCEAN `{O:0.8,C:0.3,E:0.9,A:0.4,N:0.2}` preserved — pleasure-verified vs the mood-fix setpoint 0.418; backup at scratchpad `sindra_backup.json`). PROVEN: she went from ZERO memory ops → full recall (Orion fact @ cosine 0.988) **+ promotion** (her own "systems architect" fact upserted; store grew to 9→11 chunks). Reusable redefine script: scratchpad `redefine_sindra.sh`. Note: DELETE+redefine is the ONLY path (persona+memory immutable post-define); the destructive `DELETE` tripped the harness auto-mode guard → operator ran it via `!`.
|
||||
|
||||
**(C) Markdown pass-2 SHIPPED** — `markdownSafe` extended: GFM pipe tables (`mdTable`, alignment colons), indentation-nested lists (stack of `<ul>`/`<ol>`, child list inside the open `<li>` = valid nested HTML), ordered-list `start=N` numbering, and streaming robustness (unterminated fence → partial code block; header-without-delimiter → paragraph until the delimiter streams in; never throws). esc-first → INV-004 held. Playwright-verified all cases.
|
||||
|
||||
**SHIPPED — web UI redesign via Claude Design (`v0.20.0`, MINOR, operator-approved).** The Claude Design prototype **`Ratatoskr Console.dc.html`** (project `bc0b65d1-a33e-422a-8bc1-3635c9112775`) was pulled via `DesignSync get_file` (design scopes already granted this session — no `/design-login` needed) and adapted into `src/ratatoskr/web/static/index.html`: translated OUT of the `.dc.html` dialect (`<x-dc>`/`<sc-if>`/`<sc-for>`/`{{}}`/`DCLogic`/external `_ds/` CSS — none runnable) into single-file/no-CDN/vanilla, with ALL real `/api/*` fetch + SSE wired into its DOM (endpoint set + SSE vocab unchanged from the prior SPA — ported verbatim, only DOM hooks re-targeted). New shape = a **3-column command-console**: left engine-ticker rail (DEBUG+ADMIN+tool/turn-lifecycle MERGED into one timeline via `tickerAdd` + a tools-armed chip list + a FULL-detail Bifrost rail pane) · center conversation (per-turn INLINE chain-of-thought, replacing the Think pane) · right RESIZABLE affect console (dominant/canonical-mood centerpiece + bipolar PAD faders EACH with a turn-to-turn Δ+sparkline + a P×A mood orbit + relations metric rows + canonical directive). ADDED (round 2, operator-requested): a **light/dark theme toggle** (dark default; FULL token override — surfaces+fg+borders+accent-as-text, since the designer's light theme only did surfaces → would've been light-on-light) + a **full-detail Bifrost pane** (endpoint/connected/consumer/caps/tools) + fixed the **engine-ticker spine** (was a container-anchored `::before` that scrolled out of view on auto-scroll → re-anchored to a content-height `.ticker-inner` wrapper) + **per-fader PAD turn-to-turn Δ+sparkline** (fills the room beside each meter, from the deduped-per-turn AFFECT_HIST) + an **INLINED data-URI favicon** (operator's `/home/lkraven/rata.png` — chibi aurora squirrel — downscaled 1024→64px via PIL, ~8.6KB base64, kills the /favicon.ico 404). ALL server routes UNCHANGED (**84 web tests green**). Verified BOTH lenses: `pytest tests/test_web_*` (84) + node Playwright drove the real UI end-to-end against personal :8081 (session open → Sindra seeded greeting → live turn SSE → affect console + relations + bifrost detail; theme toggle + PAD deltas + ticker spine + no-favicon-404 all confirmed, dark+light screenshots). `:8765` restarted on the new code. Contract `web_debug_surface.contract.md` amended in-commit (v0.20.0 presenter renames: `renderBifrostState`→`renderBifrost`, `renderAffectPane`→`renderConsole`, `setPersonaStrip` removed; INV-001/INV-004 held). **HONEST-SHAPE call (INV-001, agent-discretion within settled policy):** the dominant-emotion centerpiece shows a real OCC emotion (Tier-1) OR the CANONICAL mood word (Tier-3 e.g. Sindra→"positive and energized", dimmed) OR "—", NEVER a fabricated emotion; the affect-derived grid drops non-emitted intensity/decay-τ, shows only real/client-derived cells. **OPEN (operator's call):** the per-fader PAD Δ placement is a sensible default — operator offered to have the designer spec the exact treatment (hooks are in place to swap it). **`v0.20.1` patch (operator-reported UI):** fixed the relations sparkline overflowing onto the `n` (evidence-count) column — the sparkline grows one char/sample (HIST_CAP=24) and overflowed its fixed grid cell, covering `n`; now capped (relations last-8, faders last-7) + `overflow:hidden` clip; verified via Playwright injecting a 24-sample history (sparkline→n bounding-box overlap = 0). ADDED native `title` mouseover hints on all 3 PAD faders + every relationship metric row (meaning + range; static METRIC_HINTS, esc()'d). Added `state.lastSnap` (console can re-render without a refetch). Playwright-verified.
|
||||
|
||||
**SHIPPED THIS SESSION (all pushed; origin/main == `d75c4e8`; code tip `v0.19.9`) — details in Recent decisions:** the whole **#347 authored-history-write** arc landed end-to-end — OpenAPI re-vendor 2.2.0->2.3.0 (`75da676`), the CONSUMER side (`v0.19.6`: `write_authored_history` + `get_session_messages` + `--seed-first-message`, **live-proven on personal :8081** via a rule-based Heimdall allow — the PDP is rule-based NOT scope-on-key, policy user_id=ratatoskr->ALLOW/others->DENY-hide-404), persona_state `{pad:{pleasure,arousal,dominance}}` canonical alignment (#317) + Tier-3 prose re-vendor (`v0.19.7`), the **first-message-preset AUTO-SEED** (`v0.19.8`: new module `ratatoskr.first_message` wired into all 3 session-create paths, best-effort never-raise/never-block; heid-code-review + heid-bug-hunt hardened), and the web now RENDERS the seeded first-message (`v0.19.9`: new `GET /api/sessions/{id}/messages` route + SPA `loadTranscript`, Playwright-verified). Coverage-map re-converged **REST 19/41**. **Sindra:** her card was PATCHed (the `Startup:` workaround moved into a #347 first-message; non-destructive PATCH — OCEAN/persona/memory intact), and she's currently **RESET clean (0/0)** on the provider stores.
|
||||
|
||||
**Prior arcs this session (2026-07-04 -> 07-06), both with worldtree-dev (a tooling script + proposal docs; the #347 CONSUMER work above is the new production code):**
|
||||
|
||||
**(1) Authored-history-write primitive -> ACCEPTED as Worldtree #347 (Worldtree-owned).** A SillyTavern-style "first-message" (inject a character-authored opening) generalized to an engine primitive: **write a turn into a session's ledger WITHOUT generation, seed-only, side-effects off by default.** It cannot be done client-side (the messages `role` field is a *model-role* override, not an author-role -> `role:"assistant"` 404s; a model-visible authored turn needs engine support). Arc: drafted `docs/proposals/authored-message-injection.md` -> **heid panel pressure-test** (3/3 convergence: recentered on "non-generating write" not author-role; narrowed v1 to append-only+create-time; bounded `effects` enum; dropped edit/regenerate as history-mutation) -> revised -> committed (`c457520`) -> handed to worldtree-dev -> **accepted as design item #347.** worldtree-dev wrote the v1 contract (rev 1.1); **I validated the wire as reference consumer (green).** v1 shape: `POST /sessions/{id}/history`, `author=assistant` only, `effects=none` only, `idempotency_key` REQUIRED (per-session), **model-invisible provenance** (renders byte-identical to a lived assistant turn -> first-message immersion preserved; provenance audit-only), **event-silence** (no turn.started/done, no Bifrost wire for a seed; the 201/200 IS the write-ack), `seeded` lifecycle phase (not exposed on read paths). **Heimdall-gated with hide-existence** (grant `session.history.write`; ungranted tenant -> 404 NOT 403, undiscoverable in /capabilities -> consumer must treat 404 as feature-absent -> fall back to a model-generated greeting, never capability-probe). **Provider constraint:** a create-time first-message makes the assistant seq-0; vLLM/openai_compat tolerate assistant-first (sindra = openai_compat, unaffected), Anthropic-family providers 400 the next generation. **Waiting on worldtree-dev:** #347 TDD (their heid->contract->review workflow) + the consumer-facing 2.3.0 persona/motivational/memory schemas -> then re-vendor our pinned openapi 2.2.0->2.3.0.
|
||||
|
||||
**(2) Sindra's stuck-neutral mood FIXED** (operator-driven "reset + smoke" that flushed out two real upstream problems). Chain: her OCEAN lived only in prompt TEXT, never declared as a structured persona -> the Tier-3 mood engine ran on neutral defaults. Fix = declare OCEAN via the **define-time `persona` field** (immutable via PATCH -> requires DELETE+REDEFINE). Along the way my "the persona didn't store" call was WRONG (persona_state/envelope are Tier-3-blind, see Tried/abandoned); worldtree-dev found a real engine bug **#348** (single-letter vs spelled-out OCEAN keys -> a declared OCEAN silently resolved to 0.0/neutral; fixed in b21, shipped to personal as b22); then a clean bound-egress read STILL neutral -> the **personal container was running a stale image** (the b22 deploy was a pull-only no-op racing the main build; infra-ops force-swapped run 8211, verified `2.3.0` / `879cefe`). **VERIFIED FIXED:** bound mood-smoke reads `(0.448, 0.267, 0.316)` ~= the OCEAN-derived setpoint `(0.418, 0.249, 0.328)`. Sindra is currently reset clean (0/0) on `role=character`; her persona is stored + correct (**no re-define needed again**).
|
||||
|
||||
**(3) R30 CLOSED** (operator steer 2026-07-04, relayed via worldtree-dev): graduated on offline-tests + human face-validity, NO deployed gap-injection run (it was confirmatory-not-measuring per brokkr's S0 reframe; offline tests already cover the OU formula + both directions). My gap-injection harness (read/predict/record; write side stubbed; `predict()` self-validated vs brokkr's N=0 anchors) is BANKED at `diag/r30-gap-injection-harness` (`7156b25`-era) for the PARKED powered true-tau study.
|
||||
|
||||
**Persona-declaration shape (Worldtree #343/#348, live on personal b22):** `POST /agents/define` `persona:{ocean:{O,C,E,A,N: float[-1,1]}}` (single-letter keys EXACTLY -- missing/extra -> 422 `persona_ocean_required`; out-of-range -> 422); **NO baseline PAD** (resting setpoint DERIVED from OCEAN via Mehrabian: pleasure=0.21E+0.59A+0.19C-0.32N, arousal=0.15O+0.30E-0.57A+0.15N, dominance=0.25E+0.17A+0.10O-0.14N); negative-channel gain + per-axis decay-tau derive from N. `valence` deferred (422 `layer_deferred`); `motivational`/`memory` active (#187/#189). Persona is **write-once at define, immutable thereafter** (PATCH takes ONLY system_prompt + role). **`role` supersedes `model`** -- set a role (`character` / `character-rp`), Worldtree resolves the model; #344 (b19) fixed the model-field to surface the ROLE, not the resolved catalog_id. `character-rp` = a reasoning-tuned RP config (gen-reasoning + temp 0.75 + RP extra_body); `character` = plain non-reasoning. The `tier3.py` client CLI is STALE (has `--model`, no `--role`; model is now immutable) -> role/persona set via raw curl.
|
||||
|
||||
**New tooling: `scripts/reset-sindra-stores.sh`** (`0a8784c`) -- one-command self-service provider-store reset: stop the combined :8392 provider -> move memory.db+affect.db to a single ROLLING backup (`db-reset-backup/`, gitignored via *.db*; `--hard` skips it) -> restart empty -> verify 0/0. Codifies the manual reset flow done repeatedly this session. **The combined `:8392` provider is THE provider now**; the separate `:8390` (affect) / `:8391` (memory) single-plane providers were pruned as stale duplicates. To drive a BOUND session from the CLI use `--new --bifrost-url http://10.100.10.50:8392` (the CLI's `--bifrost-plane affect/memory` map to the pruned :8390/:8391 -> unreachable; `combined` is not a `--bifrost-plane` choice).
|
||||
|
||||
**Standing (carried from prior snapshots, still true):** the web surface (`ratatoskr-web`, :8765) is the operator's PRIMARY debug surface at full TUI pane parity (v0.19.5); the **v1 coverage-audit has CONVERGED** -- REST 17/40 (zero in-scope gaps, 23 excluded-by-design), SSE 11/11, Bifrost provider planes 8/8 live-proven; the living ledger is `docs/coverage-map.md`; **v1 cuts when Worldtree tags 1.0** (ratatoskr v1 = full Worldtree I/O coverage). Debug-observability core complete (Persona/Tools/BifrostState/AdminEvents). Substrate pins: **bifrost `==1.1.1` / wire v0.7** (bumped 2026-07-12 from 1.1.0 — the frozen-v0.6 serialization fix, v0.20.10; prior 1.1.0 bumped 2026-07-07 from 1.0.0; NOW WIRE-ALIGNED with Worldtree personal-b47 which adopted wire-v0.7 — bound Tier-3 fully restored 2026-07-10; keeping 1.1.0 was load-bearing, see the `[2026-07-10]` handshake decision); Worldtree openapi vendored **2.3.0** (re-vendored 2026-07-06 for #347 `POST /sessions/{id}/history`; drift-clean vs source), pinned + drift-gated in `.corviduo-canonicals.toml`; **suite 631 green.** **Personal WT on b61/wire-v0.7** (deploy train through this cycle: b35→b44→b46→b47→b60→b61; b60 landed the #355 STICK fix, b61 the orthogonal over-budget trigger fix + a llama.cpp reasoning-budget seat). **Drift-check note (RESOLVED 2026-07-13):** the two `tolerate_drift` WARN pins (`worldtree-affect-egress-consumer-reference-v1` + `worldtree-conversation-api-spec-v1`) were RE-SYNCED — the drift was a benign 2-line R32-1B doc note (PAD `[-1,1]` → unbounded latent `z` w/ `~±10` wire bound) documenting the unbounded-z change ratatoskr ALREADY adopted in v0.20.9, NOT the anticipated we-framing conditional (that remains a FUTURE coordinated re-vendor when the brokkr render epic lands). All canonicals now drift-clean. **NEW vendored canon (Vuong-directed via brokkr):** the R34/R35 psych-profile reference — `brokkr-psych-profile-authoring-spec-v1` + `brokkr-psych-profile-parameters-v1` — pinned under `docs/vendor/brokkr-r34-psych-profile/` (canonical_source `brokkr-smithy`, tolerate_drift; the authoring-spec GOVERNS on conflict with the parameter distillation; brokkr owns both + pings on change). Keys env-only mode-600 (consumer/Heimdall in `~/.config/ratatoskr/provider.env`; admin `RATATOSKR_ADMIN_API_KEY` = 7 read scopes, **personal-:8081-only**; Heimdall keys are PER-INSTANCE). Provider identity settled -- ratatoskr owns both ends of the Bifrost round-trip; `ratatoskr:sindra` is the owner-scoped Tier-3 agent (invisible to `GET /agents`; check `GET /agents/<owner>:<name>` with the owner key). Providers run as dev-box BACKGROUND SHELLS. `graphify-out/` runs dirty (auto-regen, never stage). Branch `main`, HEAD `7bca76e` (origin/main synced through v0.20.10 + drift-sync); remote `origin -> git@gitea.phasefinal.com:vh/ratatoskr.git`. Open/deferred: #10 (subject-migration watch); the relational-dynamics-arc verify (deferred, bind mechanism known: `--bifrost-url :8392`); the P06 optimization-phase re-drive (future, brokkr brings the prereg); the we-framing-conditional affect-egress re-vendor (future, when the brokkr render epic lands — the R32-1B doc-note drift is already resolved). (WT #355 loop-in obligation DISCHARGED 2026-07-13; WT #356 resume-durability gap is worldtree-owned.) **Debug state CLEANED (2026-07-13, reverses the prior KEEP):** provider stores reset to 0/0 + `ratatoskr:memprobe` deleted — clean slate for the Sindra run, no leftover debug state.
|
||||
**Other live threads:** soong-lab = our Tier-3 agent-authoring studio (bundle↔define round-trip proven;
|
||||
Recent decisions `[2026-07-14/15]`). R38 (brokkr / WT #362) = ratatoskr-as-probe-runner IN PRINCIPLE,
|
||||
pre-contract, Vuong's scope call. `ratatoskr:sindra` is the owner-scoped Tier-3 agent (invisible to
|
||||
`GET /agents`; check `GET /agents/<owner>:<name>` with the owner key). Open/deferred: #10 subject-
|
||||
migration watch; relational-dynamics-arc verify (bind via `--bifrost-url :8392`); P06 optimization-phase
|
||||
re-drive (future, brokkr brings prereg); we-framing-conditional affect-egress re-vendor (future). WT
|
||||
#356 resume-durability = worldtree-owned.
|
||||
|
||||
## Recent decisions
|
||||
|
||||
@@ -214,6 +246,14 @@ decision. Captures rationale that won't be obvious from code alone.
|
||||
- `[2026-07-13]` **WT #355 VALIDATION — fix CONFIRMED; the standing loop-in obligation is DISCHARGED.** The fully-instrumented re-drive ran, coordinated from the ratatoskr seat: infra-ops armed a full WT-netns pcap + py-spy (T0/30/60/300) on the b60 :8081 container; soong drove an 8-turn accumulating RP-with-tools repro on a FRESH session. Authoritative WT-side turns-table: wedging turns 2064/2065 → `completed=True, cancelled=1, phase=STALLED`, dur 302s/360s — the 300s stall-watchdog + Slice-C cancel-INDEPENDENT terminal fired cleanly, vs the pre-b60 baseline (turn 2061) 16-min hang / NO terminal. Slice-B `_log_wedged_task_stack` named the frame (`agent_turn.py:586 async for chunk in stream_iter`, idle-in-epoll — the wedge was a thinking-phase over-budget hang, NOT the attach_tool precursor first assumed). soong's client verdict is 45s-masked (soong-lab v0.3.2 idle-timeout) → NOT b60's terminal; the WT-side capture is authoritative. Threads `01KXE0MXDX…`(wt) / `01KXE0X2DD…`(infra) / `01KXE0X6GR…`(soong).
|
||||
- `[2026-07-13]` **b61 adopted as the personal target — the orthogonal over-budget TRIGGER also fixed.** worldtree shipped b61: the provider stream loop terminates on `finish_reason` + a per-read idle deadline + a 300s wall-clock backstop (no longer waits on the SDK `[DONE]` sentinel), plus a custom llama.cpp reasoning-budget multi-terminator seat → the runaway is bounded at BOTH layers. The #355 STICK (no-terminal) and its trigger (why it wedges) are now separately fixed. Resume-durability gap → **WT #356** (worldtree-owned).
|
||||
- `[2026-07-13]` **Cleaned + prepped for a Sindra run (operator: "clean up everything + prep").** Reset provider stores to 0/0 (`reset-sindra-stores.sh`, rolling backup `db-reset-backup/`); deleted the throwaway `ratatoskr:memprobe` agent via the operator's `!` (destructive DELETE trips the auto-guard — reverses the earlier KEEP). `ratatoskr:sindra` verified present + persona-intact on b61. Environment Sindra-run-ready (web :8765 + provider :8392 both up, single healthy provider instance); operator driving the run interactively.
|
||||
- `[2026-07-14]` **soong-lab adopted as our Tier-3 agent-authoring studio (operator-directed).** ratatoskr consumes soong-lab bundles → WT `agents.define`, and ships agents back as bundles. Sindra round-trip proven (soong imported her `resume` half through real `import_bundle`); soong-lab export+importer contracts pinned via canonical-sync (`canonical_source=soong-lab` @ f434016, commit `39050c3`). The 4 soong-lab ROLE_CHOICES = WT model-role slugs 1:1 by name (worldtree-dev); `character`/`thoughtful-character` need the `character` grant (held), `assistant`/`thoughtful-assistant` need `foundational` (routed to infra-ops). NOT on the v1 coverage-map (sibling-studio interop, not a WT I/O point) — operator chose to pursue anyway.
|
||||
- `[2026-07-15]` **Cross-session recall failure root-caused → the person-prime `scan` build.** worldtree-dev: WT injects a recalled fact only if combined score (sim×salience) ≥ 0.45 (`auto_inject_combined_score_threshold`) AND recall is per-turn query-gated → moderate-sim durable facts (name ~0.40) never inject. Designed turn-0 fix = WT #349 person-prime (query-less top-N-by-recency injection), capability-gated on the store advertising `updated_at` sort — dark for our provider. Fix = implement the sorted `scan` verb + advertise `sortable_chunk_fields` (ZERO WT change). Contract-first (un-defers bifrost `scan`); skipped heid-contract-review (external spec from worldtree-dev, validated point-for-point). **SHIPPED + deployed + live-verified v0.20.14** → `persistent-memory.d/2026-07-16-person-prime-scan-shipped.md`.
|
||||
- `[2026-07-15]` **Sindra redefined from the soong-lab bundle** (`/tmp/sindra.json`, operator "update to match"). Persona immutable → DELETE+redefine; `memory:{}` preserved; motivational string→WT-object mapped (synthesized id/type/salience, flagged to operator); role `character-rp`→`thoughtful-character` (same seat). Payload validated via a throwaway probe (`sindra-probe2`→201) BEFORE the destructive delete. first_message preset updated + web restarted.
|
||||
|
||||
- `[2026-07-16]` **WT #364 + brokkr R39 re-drive (DECISIVE) — name-recall root-caused Worldtree-side; the fix is the signal FAMILY, not a threshold.** No (sim,salience) fusion can fix it (stale negative Pareto-dominates the true name); our specimen + operator's subject-provenance catch (Sindra's own prompt-behavior leaked into user memory) shaped #364's `(subject,relation)` slot-supersession + identity-tier fix. → `persistent-memory.d/2026-07-16-wt364-r39-name-recall.md`
|
||||
- `[2026-07-16]` **bifrost ruled scan snapshot-cursor NORMATIVE (offset NOT blessed) + shipped conformance coverage in bifrost 1.1.3.** Our multi-page offset cursor is now known-non-conformant (single-page person-prime is fine, nothing shipped is broken); adoption is operator-sequenced. → `persistent-memory.d/2026-07-16-bifrost-cursor-conformance.md`
|
||||
|
||||
- `[2026-07-16]` **brokkr flagged ratatoskr as R39 Phase-2 PROBE-RUNNER (Arm-2 contradiction set = our frozen 10-chunk specimen); ACCEPTED IN PRINCIPLE (Vuong 2026-07-16) — formal scope + effort commit lands when Arm-2 actually spins.** Non-urgent — downstream of worldtree's #364 impl; brokkr pings the export shape when Arm-2 spins. Gate: reproduce AUROC~0.59 (similarity can't separate contradiction from paraphrase) on the domain set BEFORE crediting deterministic slot-supersession; eval is a SURFACING test not retention (injection=0 hard-gate + surfacing-recall≥0.95, §5.5). Prereg: brokkr `research/R39-memory-salience-dreams-surfacing/phase-2/preregistration.md` (R39.9). Tracked at brokkr's Phase-2 prereg + the Arm-2-spin ping (thread `01KXMT42Z7…`); I sent a non-committal receipt (role routed to operator).
|
||||
|
||||
_41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
|
||||
|
||||
@@ -268,7 +308,11 @@ defense against re-attempting the same cul-de-sac.
|
||||
|
||||
- `[2026-07-13]` **`thinking_enabled=False` on the define was a NO-OP — char-rp-reasoning ignores it.** The naive kwarg never reached the seat: char-rp-reasoning resolves to a base gateway provider whose thinking-translator returns `{}` for the flag on AND off. The real lever is the gateway param `chat_template_kwargs.enable_thinking:false` (infra-ops confirmed it via a 642ch→0ch reasoning-token delta). **To toggle reasoning on a gateway-backed seat, set the chat-template kwarg, not a generic `thinking_enabled` flag.**
|
||||
- `[2026-07-13]` **A same-image redeploy does NOT reload a bind-mounted config — the ModelRegistry boot-caches it at `__init__`.** After the config was synced on-disk (infra-ops validated) and `docker compose up -d` re-ran, the reasoning-off change STILL didn't take: an unchanged image makes `up -d` a no-op (no container recreate), so the process kept serving the pre-sync config. Fix = a surgical `docker restart <container>` (same image, no pull) → the process re-boot-reads the config. **When an on-disk config change doesn't take effect, suspect the process cached it at startup; force a container RESTART, not a redeploy** (a docs-only forcing-commit also won't rebuild if docs are paths-ignored in CI). This is the config-plane sibling of the `[2026-07-06]` stale-image foot-gun.
|
||||
- `[2026-07-15]` **soong-lab bundle `ship.native.motivational.goals/fears` are bare STRINGS, but WT `agents.define` requires OBJECTS** `{id, type∈{maintenance,achievement,avoidance}, salience[0,1], description≥20ch}` (`ValidatedGoal`/`ValidatedFear`, worldtree `core/conversation_api/api.py:2116`, extra=forbid; ids unique across goals+fears). So `ship.native` is NOT directly define-valid on motivational (soong-lab's Frame Invariant 1 breaks there). A bundle→WT consumer MUST map string→object + synthesize id/type/salience. Informed soong-dev → **RESOLVED soong-lab v0.3.24 (2026-07-16): now emits WT-valid objects (type+salience captured, id synthesized at export, validate_exportable gates description≥20/enum/range) → drop the string→object workaround for v0.3.24+ bundles; legacy pre-fix designs coerce on open, so re-exports are valid too.** **Rule: PROBE a define payload under a throwaway agent_name BEFORE a destructive DELETE+redefine** — validating via `sindra-probe2`→201 caught the 422 without leaving the real Sindra deleted+undefined. (A failed probe still RESERVES the name → 409 on retry; use a fresh probe name.)
|
||||
- `[2026-07-15]` **`re.sub`/`re.subn` INTERPRETS backslash-escapes in the REPLACEMENT string** — a `json.dumps`'d value (escaped `\n`) fed as the replacement leaked REAL newlines into a source file (broke `first_message.py` with an unterminated-string SyntaxError). Fix: use a FUNCTION replacement (`pat.subn(lambda m: new_block, src)`) or `str.replace` — the callable form bypasses escape processing. json.dumps itself escapes correctly; re.sub was the culprit.
|
||||
- `[2026-07-13]` **Called Deckard "hung" off a short timeout — WRONG (operator correction).** A 30-45s no-terminal on the char-rp-reasoning seat looked like a hang; operator: "is it HUNG? deckard is EXTREMELY verbose, without enough context, you never see the non-reasoning tokens." It was verbose reasoning-CoT on a long extraction prompt, not a wedge. **Don't call a reasoning seat hung off a latency threshold — the CoT is invisible and slow; distinguish slow-verbose from actually-wedged before concluding.** (The genuine wedge is WT #355, a distinct mechanism — no-terminal even after the 300s watchdog, not merely slow.)
|
||||
- `[2026-07-13]` **Resumed-session context-snapshot is IN-MEMORY → lost on a container recreate (agent_not_available on resume).** During the #355 re-drive, soong's fresh drive 409'd `agent_not_available`. Root cause (after ~4 refinements — agent-loss? zombie turn-lock? stale-sessions-hold-agent? → the actual mechanism): `get_agent_context_for_session` returns the agent snapshot recorded AT SESSION-CREATE, held in-memory; a pre-recreate session resumed on b60/b61 has no snapshot → None → 409. (Compounding: stale `'active'` sessions left un-terminated by the old no-terminal bug HOLD the agent, blocking new creates too.) Deploy-grounding was healthy the whole time (`registry.resolve("char-rp-reasoning")` OK) — the config/grant hypotheses were all red herrings. Fix = a FRESH session (a studio-service restart re-records the snapshot); pre-recreate sessions need retiring. Tracked **WT #356**. **For any run: create a fresh session, never resume a pre-recreate one; `agent_not_available` on a fresh create = this gap.** (Working-style note: I over-relayed the intermediate root-cause churn to the operator — for a peer-owned block being actively diagnosed, hold until it settles.)
|
||||
|
||||
- `[2026-07-16]` **`sortable_chunk_fields` advertised WITHOUT the required `type` field = whole-handshake deploy-breaker; only DRIVING the real bind caught it.** bifrost `handshake_response` `SortableChunkField` requires BOTH `name`+`type` (`additionalProperties:false`); we shipped `[{"name":"updated_at"}]` → the response failed wire-schema validation → `bifrost.schema_validation_failed` → the ENTIRE bind (memory+affect) broke, not just sort. Unit tests + worldtree-dev's name-only service parser + the heid-bug-hunt ALL passed it — only the live handshake drive (`/verify` discipline) caught it. **Lesson: validate `describe_store` against the bifrost WIRE schema, not just our own caps assertions.** (Sibling of the `[2026-07-10]` frozen-v0.6 handshake foot-gun — there an EXTRA field broke a v0.6 handshake, here a MISSING required field broke a v0.7 one.)
|
||||
|
||||
_18 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ratatoskr"
|
||||
version = "0.20.11"
|
||||
version = "0.20.15"
|
||||
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)
|
||||
]
|
||||
|
||||
@@ -28,14 +28,7 @@ _SEED_TIMEOUT_S = 10.0
|
||||
# Editing this dict is how an operator tunes an agent's first turn. Keep entries
|
||||
# under the server's authored_content_max_bytes (8192 bytes) budget.
|
||||
FIRST_MESSAGE_PRESETS: dict[str, str] = {
|
||||
"ratatoskr:sindra": (
|
||||
"Hey there. I'm Sindra—glad you found me. So, three things before we start:\n\n"
|
||||
"How intense should I be? 1 is slow and teasing, 10 is relentless.\n\n"
|
||||
"What mood am I in today? Sweetheart, Vixen, Queen, Siren, or Brat?\n\n"
|
||||
"And how willing am I to begin? Enthusiastic (I want you now), Hesitant "
|
||||
"(you'll need to coax me out), Resistant (playful pushback), or Unwilling "
|
||||
"(I don't want this at all, until you prove otherwise)."
|
||||
),
|
||||
"ratatoskr:sindra": "The room materializes in a soft pulse of light. Walls of brushed metal and warm ambient lighting resolve around him as the holodesk completes its cycle. A thin seam of blue runs along the desk’s edge, reflecting the overhead glow. A chair slides into place with a quiet hiss. On the far side of the desk, Sindra is already there: shoulders relaxed, one knee drawn up onto the seat, the hem of an oversized dark-green sweater slipping off her left shoulder. Her gaze is steady, amused, and entirely fixed on him.\n\nShe doesn’t stand. She lets the space breathe for a moment, as if giving him time to understand that he is no longer where he was. Then she speaks.\n\n\"Hi. I’m Sindra. You just made it here, which means we should set this up properly.\"\n\nShe tilts her head slightly, the faintest crooked smile touching her lips.\n\n\"Three settings. Your call. I’ll take them in order.\"\n\n\"Intensity. One is slow and teasing; ten is relentless and unrelenting. Where should I start?\"\n\n\"Mood. Sweetheart, Vixen, Queen, Siren, or Brat. Which version of me do you want in the room with you?\"\n\n\"And willingness. I can be enthusiastic and ready the moment you look at me; I can be hesitant and require coaxing; I can resist and make you earn it; or I can be unwilling, and you’ll have to change my mind. How should I behave when you approach me?\"\n\nSindra lets her fingers trace the edge of the sweater sleeve, casual and unhurried, her eyes not leaving his face.\n\n\"Pick your three. Then we begin.\"",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -132,7 +132,10 @@ def _validate_injection(record: dict) -> None:
|
||||
raise InvalidArguments("injection_source only valid for injected_context origin")
|
||||
|
||||
|
||||
_SORTABLE_CHUNK_FIELDS: list[dict] = [{"name": "updated_at"}]
|
||||
# bifrost handshake_response SortableChunkField requires BOTH name + type
|
||||
# (additionalProperties:false); omitting `type` fails wire-schema validation and
|
||||
# breaks the whole handshake. `type` is advisory-only (the wire never interprets it).
|
||||
_SORTABLE_CHUNK_FIELDS: list[dict] = [{"name": "updated_at", "type": "timestamp"}]
|
||||
_SORTABLE_FIELD_NAMES = frozenset(f["name"] for f in _SORTABLE_CHUNK_FIELDS)
|
||||
|
||||
|
||||
@@ -140,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
|
||||
@@ -367,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,
|
||||
*,
|
||||
@@ -383,6 +418,8 @@ class RatatoskrMemoryStore:
|
||||
scope_all = scope_all or {}
|
||||
scope_any = scope_any or []
|
||||
_validate_scope(scope_all, scope_any) # PRE-002 (same lattice as search)
|
||||
if sort is not None and not isinstance(sort, dict): # PRE-003: malformed sort => reject, never crash
|
||||
raise InvalidArguments("sort must be an object with field and direction")
|
||||
field = (sort or {}).get("field", "updated_at")
|
||||
direction = (sort or {}).get("direction", "desc")
|
||||
if field not in _SORTABLE_FIELD_NAMES or direction not in ("asc", "desc"): # PRE-003
|
||||
|
||||
@@ -62,6 +62,15 @@ def test_fresh_db_advertises_v1_caps_and_schema():
|
||||
assert caps["atomic_supersede_supported"] is False
|
||||
assert caps["transaction_supported"] is False
|
||||
assert caps["filterable_metadata_fields"] == []
|
||||
# bifrost handshake_response SortableChunkField requires BOTH name + type
|
||||
# (additionalProperties:false) — omitting `type` fails wire-schema validation and
|
||||
# breaks the ENTIRE Bifrost bind (regression guard: the deploy-breaker of 2026-07-15).
|
||||
scf = caps["sortable_chunk_fields"]
|
||||
assert scf == [{"name": "updated_at", "type": "timestamp"}]
|
||||
for entry in scf:
|
||||
assert set(entry) == {"name", "type"} # required exactly, no extra keys
|
||||
assert isinstance(entry["name"], str) and entry["name"]
|
||||
assert isinstance(entry["type"], str) and entry["type"]
|
||||
# tables + vec index queryable
|
||||
store._conn.execute("SELECT * FROM memory_chunks")
|
||||
store._conn.execute("SELECT * FROM memory_idempotency")
|
||||
@@ -427,6 +436,14 @@ async def test_scan_unadvertised_sort_field_rejected():
|
||||
await store.scan(scope_all={"end_user": "u1"}, limit=3, sort={"field": "salience", "direction": "desc"})
|
||||
|
||||
|
||||
async def test_scan_non_dict_sort_rejected():
|
||||
# PRE-003: a truthy non-dict sort (caller-controlled) -> InvalidArguments, never AttributeError.
|
||||
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
|
||||
for bad in ("updated_at", ["updated_at"], 5):
|
||||
with pytest.raises(InvalidArguments):
|
||||
await store.scan(scope_all={"end_user": "u1"}, limit=3, sort=bad)
|
||||
|
||||
|
||||
async def test_scan_records_carry_person_prime_filter_fields():
|
||||
# The client _scan_filter_matches keys on agent_id + subject + worldtree_scope; a record
|
||||
# missing any is silently dropped -> the scan record must carry them verbatim.
|
||||
@@ -490,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():
|
||||
|
||||
@@ -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.11"
|
||||
version = "0.20.15"
|
||||
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" },
|
||||
|
||||
Reference in New Issue
Block a user