Compare commits

..

4 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
6 changed files with 38 additions and 8 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
+1 -1
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.13"
version = "0.20.14"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
+4 -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)
+9
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")
Generated
+1 -1
View File
@@ -1052,7 +1052,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.20.13"
version = "0.20.14"
source = { editable = "." }
dependencies = [
{ name = "httpx" },