Compare commits

..

6 Commits

Author SHA1 Message Date
vh f46ccbae1c fix(provider): sortable_chunk_fields needs required type — handshake was broken
DEPLOY-BREAKER caught by driving the live bind (unit tests + worldtree-dev's
name-only parser + heid-bug-hunt all missed it). bifrost handshake_response
`SortableChunkField` requires BOTH `name` and `type` (additionalProperties:false).
We advertised `[{"name":"updated_at"}]` (no `type`), so the handshake_response
failed wire-schema validation → `bifrost.schema_validation_failed` → the ENTIRE
Bifrost bind (memory + affect) broke, not just the sort. Advertise
`{"name":"updated_at","type":"timestamp"}` (matches the reference; `type` is
advisory-only). Regression guard added to the caps test (asserts required name+type,
no extra keys). Full suite 639 green.
2026-07-15 09:11:08 -07:00
vh 772fad18b4 memory: snapshot — person-prime DEPLOYED (v0.20.13, :8392+:8765 restarted); worldtree-dev pinged for turn-1 verify 2026-07-15 09:02:34 -07:00
vh 8199774405 docs(contract): mark scan cursor v1-provisional (offset, not snapshot); route conformance gap to bifrost-dev
Operator accepted offset-cursor for v1 (person-prime single-page is
conformant). INV-010 now documents the KNOWN DEVIATION: multi-page
continuation diverges from bifrost's protocol snapshot-cursor contract
(dispatch drops sort on continuation, ScanCursorExpired normative) — our
offset cursor doesn't snapshot (dup/drop under concurrent write) and never
expires. Durable fix routed to bifrost-dev as a conformance-coverage gap
(scan/cursor is untested); ratatoskr will adopt reference snapshot-cursors
if bifrost rules them normative.
2026-07-15 08:52:29 -07:00
vh 66ba06875e memory: snapshot — heid-bug-hunt triaged (non-dict sort FIXED v0.20.13; cursor-stability surfaced) 2026-07-15 08:40:49 -07:00
vh 25ccb5c75b fix(provider): scan rejects non-dict sort with InvalidArguments, never AttributeError
heid-bug-hunt panel (Gróa + Hulda, confirmed-from-code) caught that a truthy
non-dict `sort` (e.g. sort="updated_at" or sort=["updated_at"]) reached
`(sort or {}).get(...)` and crashed with AttributeError instead of the
InvalidArguments PRE-003 promises for malformed caller-controlled input. Add an
isinstance guard before field extraction. Test covers str/list/int sort values.
2026-07-15 08:39:30 -07:00
vh 22e7a1b0e7 memory: snapshot — person-prime scan CODE LANDED (v0.20.11/12); deploy blocked on operator :8392 restart
scan verb + sortable_chunk_fields cap committed (8fc757a) + Sindra holodesk
first-message preset (a9c521a); TDD 7/7 + full suite 638 green; heid-bug-hunt
panel dispatched (thread 01KXK5XTYHV8TGEDRAZV8GRXWC). DEPLOY (restart :8392 so
the handshake advertises the cap) is operator-gated — classifier denied
bouncing the running provider. worldtree-dev turn-1 verify gated on the deploy.
2026-07-15 08:23:28 -07:00
6 changed files with 67 additions and 11 deletions
@@ -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,20 @@ 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.
## Concurrency
+20 -4
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ratatoskr"
version = "0.20.12"
version = "0.20.14"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
+6 -1
View File
@@ -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)
@@ -383,6 +386,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
+17
View File
@@ -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.
Generated
+1 -1
View File
@@ -1052,7 +1052,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.20.12"
version = "0.20.14"
source = { editable = "." }
dependencies = [
{ name = "httpx" },