docs(#17): self-drive+observe contract, bifrost self-test runbook + snapshot
- docs/contracts/issues/17.contract.md — issue-scoped v2.1 contract for #17 (Bifrost-binding the chat client). v1 scope = single-plane bind + dispatch-layer op-feed (composite endpoint + turn-pane UI parked). Design consulted via /heid, paraphrase-gated via /heid-contract-review panel; two internal inconsistencies fixed (OpEvent turn_id reservation made literal; session_id-for-all-verbs correction). Validates OK, prd drift-clean. - docs/bifrost-self-test.md — reusable runbook for driving + observing the full Bifrost round-trip against our own provider (the manual form of #17; pins the consumer-key-as-bearer tripwire). - persistent-memory.md — snapshot: observe brick shipped, self-drive proven, #295 root-caused (upstream, scope-axis asymmetry) -> #296/#297, agent_self -> canonical decided.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# Bifrost round-trip self-test
|
||||
|
||||
How to drive **and** observe a full Tier-3 Bifrost round-trip against
|
||||
ratatoskr's *own* provider — bind a Worldtree session to our affect/memory
|
||||
store, fire a turn, and read exactly what Worldtree dispatched to us,
|
||||
correlated with the turn that triggered it.
|
||||
|
||||
This is the **manual form of issue #17** (self-drive + correlated-log
|
||||
affect/memory ops). Until #17 ships that capability inside the TUI/web/CLI,
|
||||
this runbook is the reproducible loop — and it is the substrate worldtree-dev's
|
||||
#296 (salience-algorithm research) and #297 (recall scope/assembly research)
|
||||
diagnose against.
|
||||
|
||||
> First proven by hand 2026-06-16 while isolating #295's recall leg.
|
||||
|
||||
## The load-bearing tripwire: which key signs the bind
|
||||
|
||||
A bound session-create authenticates as the **Bifrost consumer**, not the
|
||||
canary client. Worldtree signs the Bifrost handshake JWT with the
|
||||
**session-create bearer token** (HS256 shared-secret model); our provider
|
||||
verifies it against `RATATOSKR_HEIMDALL_KEY`.
|
||||
|
||||
So the bearer on `POST /sessions` **must be the consumer Heimdall key**
|
||||
(`RATATOSKR_HEIMDALL_KEY`, in `~/.config/ratatoskr/provider.env`) — **not**
|
||||
`WORLDTREE_API_KEY` (the mimir/foundational TUI key in `env.sh`). They are two
|
||||
different keys for two identities of ratatoskr.
|
||||
|
||||
| Bearer used on `POST /sessions` | Handshake at our provider |
|
||||
|---|---|
|
||||
| `WORLDTREE_API_KEY` (mimir/TUI key) | **401** `bifrost.auth_rejected` → 502 to client |
|
||||
| `RATATOSKR_HEIMDALL_KEY` (consumer key) | **200 OK** → session bound |
|
||||
|
||||
ratatoskr is two identities: the conversation-API **canary client**
|
||||
(`WORLDTREE_API_KEY`) and the Bifrost **consumer/provider**
|
||||
(`RATATOSKR_HEIMDALL_KEY`). Self-driving a bound session crosses into the
|
||||
consumer identity, so it uses the consumer key. #17's Bind half has to carry
|
||||
this distinction.
|
||||
|
||||
## Prereqs
|
||||
|
||||
- Provider(s) running on this box (nh3-dev, `10.100.10.50`):
|
||||
- memory → `ratatoskr-memory-provider` on `:8391`
|
||||
- affect → `ratatoskr-provider` on `:8390`
|
||||
- Each is a dev background shell, env-sourced from `provider.env`. The memory
|
||||
provider's stdout carries the inbound observe log (`[memory-provider]` lines
|
||||
added in `memory_store.py`).
|
||||
- The provider endpoint is reachable + allowlisted from Worldtree
|
||||
(`10.250.50.152`): `http://10.100.10.50:8391`. The allowlist
|
||||
(`BIFROST_CLIENT_ALLOWED_HOSTS`) is **Worldtree-side, infra-ops-owned** — if a
|
||||
bind 502s with a route/allowlist error, that entry is the thing to check.
|
||||
- A memory-enabled Tier-3 agent defined on the instance. `ratatoskr:smoke`
|
||||
(scope `end_user:smoke-user`) is the standing fixture; it is hidden from
|
||||
`GET /agents` (Tier-3 agents are not in the public roster) but resolves on
|
||||
session-create.
|
||||
|
||||
## Steps
|
||||
|
||||
```bash
|
||||
cd ~/development/ratatoskr
|
||||
set -a && . ~/.config/ratatoskr/provider.env && set +a # RATATOSKR_HEIMDALL_KEY etc.
|
||||
URL=http://10.250.50.152:8081 # personal Worldtree
|
||||
HK="$RATATOSKR_HEIMDALL_KEY" # the CONSUMER key — the tripwire
|
||||
```
|
||||
|
||||
**1 — Bind a fresh (cold) session to our provider.** `bifrost.endpoint_url`
|
||||
points at the plane you want (`:8391` memory, `:8390` affect); caps are
|
||||
negotiated by the handshake, not declared here (`BifrostBindingRequest` is
|
||||
`{endpoint_url, scope}` only, `additionalProperties:false`). A 201 means the
|
||||
handshake verified.
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$URL/sessions" -H "Authorization: Bearer $HK" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"agent_id":"ratatoskr:smoke","end_user_id":"smoke-user",
|
||||
"bifrost":{"endpoint_url":"http://10.100.10.50:8391","scope":null}}'
|
||||
# -> 201 {"session_id":"...", "kind":"consumer_defined", ...}
|
||||
```
|
||||
|
||||
**2 — Snapshot the fixture** (to detect any promotion the turn writes):
|
||||
|
||||
```bash
|
||||
sqlite3 -readonly memory.db \
|
||||
"SELECT chunk_id, json_extract(record_json,'\$.verbatim.text') FROM memory_chunks;"
|
||||
```
|
||||
|
||||
**3 — Fire ONE turn** into the bound session, reusing ratatoskr's own SSE
|
||||
client (handles composite ids + no-read-timeout). Bearer = the consumer key:
|
||||
|
||||
```bash
|
||||
RATATOSKR_HEIMDALL_KEY="$HK" uv run python - <<'PY'
|
||||
import asyncio, os, httpx
|
||||
from ratatoskr.sse_client import stream_turn, Text, Done, Error, Cancelled
|
||||
SESSION="<session_id from step 1>"
|
||||
URL="http://10.250.50.152:8081"; KEY=os.environ["RATATOSKR_HEIMDALL_KEY"]
|
||||
async def main():
|
||||
async with httpx.AsyncClient(base_url=URL,
|
||||
headers={"Authorization":f"Bearer {KEY}","User-Agent":"ratatoskr-selftest"},
|
||||
timeout=httpx.Timeout(connect=10.0,read=None,write=10.0,pool=10.0)) as c:
|
||||
async for ev in stream_turn(c, SESSION, "What kind of chocolate do I like?"):
|
||||
if isinstance(ev, Done): print("ANSWER:", ev.response); return
|
||||
if isinstance(ev, Error): print("ERROR:", ev.error_code, ev.message); return
|
||||
asyncio.run(asyncio.wait_for(main(), 150))
|
||||
PY
|
||||
```
|
||||
|
||||
**4 — Read the inbound pair** from the provider's stdout (the observe brick).
|
||||
For a background-shell provider, that is the task output file; tail it:
|
||||
|
||||
```
|
||||
[memory-provider] memory-call search REQUEST: scope_filter={...} top_k=... vec_dim=1024
|
||||
[memory-provider] memory-call search RESPONSE: N hit(s) [{'chunk_id':..., 'score':..., 'scope':...}]
|
||||
```
|
||||
|
||||
**5 — Re-snapshot the fixture** (step 2's query). A new row = the turn was
|
||||
promoted (a salience-algorithm event; relevant to #296). Audit, don't blindly
|
||||
delete — promoted failure-surfaces may be wanted corpus.
|
||||
|
||||
## Reading the result
|
||||
|
||||
The `search REQUEST` `scope_filter` vs the `search RESPONSE` hit count is the
|
||||
whole diagnosis surface:
|
||||
|
||||
- **0 hits** → the filter didn't match any stored chunk. Compare the filter's
|
||||
axes against the stored `scope`. INV-005 requires **every** filter axis to
|
||||
match, so an extra axis on the filter that the chunks don't carry (e.g.
|
||||
`agent_self`) zeroes the result even when `end_user` matches. That's a
|
||||
**scope-build / persist-recall-symmetry** question (Worldtree-side).
|
||||
- **Hit present but the model says "no memory"** → we returned it; Worldtree
|
||||
dropped it downstream of search → **recall-assembly / injection**
|
||||
(Worldtree-side).
|
||||
|
||||
Either way our store + search are provable from this surface; the recall
|
||||
*efficacy* must be judged at the model's answer in a **cold (history-free)**
|
||||
session, never from a wire 200 (a `search` returns 200 whether or not its hits
|
||||
are injected).
|
||||
|
||||
### Worked example (2026-06-16, #295 → #297)
|
||||
|
||||
Cold turn "What kind of chocolate do I like?" against the `smoke-user` fixture:
|
||||
|
||||
```
|
||||
REQUEST: scope_filter={'end_user': 'smoke-user', 'agent_self': 'ratatoskr:smoke'} top_k=128
|
||||
RESPONSE: 0 hit(s)
|
||||
ANSWER: "I don't have access to your past preferences..."
|
||||
```
|
||||
|
||||
Root cause: the recall filter carried `agent_self` but the stored chunks are
|
||||
`{end_user: smoke-user}` only → the `agent_self` axis excluded all of them.
|
||||
Branch (a), scope asymmetry — fed to #297.
|
||||
|
||||
## Notes / foot-guns
|
||||
|
||||
- **HTTP, not HTTPS.** The spec requires `endpoint_url` be HTTPS; dev is relaxed
|
||||
via the Worldtree-side allowlist. Don't "fix" our provider to HTTPS to make a
|
||||
bind work — check the allowlist entry first.
|
||||
- **Cold means cold.** Reuse of a session with history can satisfy a "recall"
|
||||
from plain conversation history. Always bind a *fresh* session for a recall
|
||||
probe.
|
||||
- **Stray sessions** created by probes are harmless empty rows on the dev
|
||||
instance; no cleanup required.
|
||||
- This loop is the thing #17 productizes into the chat surfaces; when #17 lands,
|
||||
the bind+observe steps move inside the TUI/web/CLI and this runbook becomes the
|
||||
underlying contract check.
|
||||
@@ -0,0 +1,312 @@
|
||||
---
|
||||
contract_version: "2.1"
|
||||
target_module: "ratatoskr.sessions + ratatoskr.provider (+ cli/tui/web trigger surfaces)"
|
||||
scope: "Issue #17 v1 — make the canary chat client self-drive AND observe its own Bifrost provider. Two parts. (1) BIND: `create_session` gains an optional single-plane Bifrost binding (`BifrostBinding{endpoint_url, scope}`) authenticated with a DISTINCT consumer Heimdall key; Worldtree runs the handshake synchronously at POST /sessions, so handshake failure is a session-create failure (502), surfaced on the create path. A plane selector (`memory`→:8391 / `affect`→:8390) + the consumer key thread through CLI / TUI / web; bound-state is visible. (2) OBSERVE: a structured op-feed in the provider, instrumented at the DISPATCH/ASGI layer (where the JWT ctx / session_id lives — bifrost passes ctx to upsert_many but NOT to search/get/delete, so the existing store-method stdout shim cannot see session_id), emitting JSONL {session_id, plane, op, req_summary, resp_summary, status, ts}. OPERATOR DECISIONS LOCKED: single-plane-per-session for v1 (composite endpoint fronting both planes is PARKED — vNext); op-feed with session-level correlation for v1 (turn-correlated debug-pane UI is PARKED — needs turn_id, TBD). Provider store scope semantics MUST NOT change (AND-parity with bifrost's reference store is a hard constraint). Direct in-session TDD; live-smoke against personal Worldtree is the load-bearing acceptance gate."
|
||||
depends_on:
|
||||
- "httpx"
|
||||
- "ratatoskr.sessions"
|
||||
- "ratatoskr.provider.memory_store"
|
||||
- "ratatoskr.provider.affect_store"
|
||||
- "bifrost"
|
||||
used_by:
|
||||
- "ratatoskr.cli"
|
||||
- "ratatoskr.tui"
|
||||
- "ratatoskr.web.server"
|
||||
language: "python"
|
||||
complexity: "medium"
|
||||
estimated_loc: 260
|
||||
confidence: 0.78
|
||||
assumptions:
|
||||
- "PROVEN this session (manual end-to-end against personal Worldtree v0.35.3): `POST /sessions` with `bifrost={endpoint_url, scope:null}` runs the handshake synchronously and returns 201 when it verifies. `BifrostBindingRequest` is `{endpoint_url, scope}` ONLY (live OpenAPI, additionalProperties:false); capabilities are negotiated at the handshake, not declared in the bind request. So a session binds exactly ONE provider endpoint → ONE plane's dispatch flows."
|
||||
- "PROVEN: the session-create BEARER is the identity Worldtree signs the Bifrost handshake JWT with (HS256 shared-secret). Bearer = the canary key (WORLDTREE_API_KEY) → handshake 401 `bifrost.auth_rejected` → 502 to the client. Bearer = the consumer Heimdall key (== the provider's RATATOSKR_HEIMDALL_KEY string) → handshake 200. So a BOUND session-create MUST authenticate with the consumer key, NOT the canary key. These are two distinct ratatoskr identities."
|
||||
- "PROVEN: dev HTTP is accepted (spec wants HTTPS) because the provider host:port is on Worldtree's `BIFROST_CLIENT_ALLOWED_HOSTS` allowlist — a Worldtree-side, infra-ops-owned config. The endpoint_url must be the WORLDTREE-VISIBLE base URL (e.g. `http://10.100.10.50:8391`), not the client's loopback. Provider routes live at `/bifrost/handshake` + `/bifrost/memory-call` (memory) and `/bifrost/affect-call` (affect) under that base."
|
||||
- "PROVEN (bifrost source, memory.py:244 vs 262): `dispatch_memory_call` passes `ctx` to `upsert_many` but NOT to `search`/`get`/`delete`. So the recall verb's store method has no session_id; correlation identity must be captured at the dispatch/ASGI layer (JWT ctx), not inside the store method. turn_id (finer than session_id) availability is UNVERIFIED — a contract-stage JWT-claims/envelope dump resolves it; design the op-feed to accept a turn_id later without a schema break."
|
||||
- "Provider stores MUST NOT change scope semantics. `_scope_matches` stays AND (byte-faithful to bifrost reference `reference_server/memory.py:398`); the missing axis-lattice validation (my store admits a wider axis set than the reference) is a SEPARATE parity item, GATED on the `agent_self`-canonicity resolution, and OUT OF SCOPE for #17."
|
||||
- "The existing `create_session(client, agent_id, *, end_user_id=None)` (sessions.py:179) is extended, not replaced (pre-v1, no compat shim). The httpx client carries the canary key as its default Authorization; the bound create overrides the bearer per-request with the consumer key."
|
||||
- "Tests use `respx` for the bind unit tests (mirroring tests/test_sessions.py) + the in-process op-feed; the live-smoke acceptance is manual (per the repo's load-bearing-smoke posture), captured as a documented runbook step, not a unit test. `docs/bifrost-self-test.md` is the manual procedure this feature productizes."
|
||||
- "v1 ships the CLI + TUI + web trigger surfaces in lockstep (the repo's BOTH-presenters-in-lockstep rule); the op-feed is read by the debug surface as structured lines for now (pane-correlated rendering is PARKED)."
|
||||
open_questions:
|
||||
- "turn_id on the wire: does Worldtree put a turn_id (or traceparent) in the Bifrost request JWT claims / envelope? If yes, the op-feed echoes it and turn-correlation becomes exact; if no, correlation is session_id + time-window (weaker). RESOLVE via a JWT-claims dump during the first TDD slice. Does NOT block v1 (session_id correlation is the v1 target); it gates the PARKED pane-UI."
|
||||
- "Composite endpoint (PARKED): a thin ASGI fronting both :8390/:8391 advertising both caps at handshake would let one session observe both planes. Deferred to vNext per operator. If pursued, it needs per-underlying-store parity checks + per-plane failure status (a facade routing bug is a new failure class) — NOT just `handshake lists both caps`."
|
||||
- "Key identity unification (PARKED — operator's call, crosses Heimdall): v1 assumes two keys. Do not derive one from the other."
|
||||
- "Auto-bind on Tier-3 agents (PARKED — operator's call): v1 is explicit opt-in only. Auto-bind hides the most important debug variable (which identity + endpoint the session bound to)."
|
||||
prd:
|
||||
issue: 17
|
||||
issue_url: "https://gitea.phasefinal.com/vh/ratatoskr/issues/17"
|
||||
body_sha256_16: "58a420956e6226fb"
|
||||
lock_in_comment_id: null
|
||||
lock_in_sha256_16: null
|
||||
lock_in_at: null
|
||||
pinned_at: "2026-06-16T07:45:00+00:00"
|
||||
dependencies:
|
||||
- issue: 2
|
||||
path: "src/ratatoskr/sessions.py"
|
||||
reason: "create_session is the bind site. Same posture: caller-owned httpx client, async-native, no Worldtree imports, frozen-dataclass parse, exception `.body` truncated to [:1024]."
|
||||
- issue: 5
|
||||
path: "src/ratatoskr/sessions.py"
|
||||
reason: "end_user_id already threads into the POST /sessions body; the bifrost field is added alongside it with the same optional-when-None posture."
|
||||
---
|
||||
|
||||
# Issue #17 — Bifrost-binding the chat client: self-drive + observe
|
||||
|
||||
## Context
|
||||
|
||||
Ratatoskr is two identities: the conversation-API **canary client** (TUI/web/CLI
|
||||
that runs turns and watches the SSE flow) and a Bifrost **Tier-3 provider**
|
||||
(durable affect store :8390 + memory store :8391, separate ASGI apps). Until now
|
||||
the canary couldn't drive its OWN provider — `create_session` never sent a Bifrost
|
||||
binding, so every affect/memory round-trip was driven externally. #17 closes that:
|
||||
the canary BINDS a session to its own provider and OBSERVES the resulting
|
||||
affect/memory dispatch, so an operator can hunt latent cross-layer bugs from one
|
||||
seat. The manual procedure proven this session lives at `docs/bifrost-self-test.md`;
|
||||
#17 productizes it.
|
||||
|
||||
**v1 scope is deliberately narrow** (operator-locked): single-plane bind (composite
|
||||
endpoint PARKED), session-level op-feed (turn-correlated panes PARKED). The load-
|
||||
bearing risks are bind-time auth identity + capturing a correlation key the store
|
||||
method can't see — both resolved below.
|
||||
|
||||
## Public surface
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class BifrostBinding:
|
||||
"""Session-create Bifrost binding (Worldtree BifrostBindingRequest, #160).
|
||||
endpoint_url is the WORLDTREE-VISIBLE base URL of one provider plane."""
|
||||
endpoint_url: str
|
||||
scope: str | None = None
|
||||
|
||||
|
||||
async def create_session(
|
||||
client: httpx.AsyncClient,
|
||||
agent_id: str,
|
||||
*,
|
||||
end_user_id: str | None = None,
|
||||
bifrost: BifrostBinding | None = None,
|
||||
consumer_key: str | None = None,
|
||||
) -> SessionInfo:
|
||||
"""POST /sessions. When `bifrost` is set the request authenticates with
|
||||
`consumer_key` (NOT the client's default canary bearer) and carries the
|
||||
`bifrost` field; Worldtree handshakes synchronously before 201. See FN
|
||||
create_session."""
|
||||
|
||||
|
||||
def endpoint_for_plane(plane: str, base_host: str) -> str:
|
||||
"""'memory'->:8391, 'affect'->:8390 → f'http://{base_host}:{port}'. The
|
||||
Worldtree-visible base URL. See FN endpoint_for_plane."""
|
||||
```
|
||||
|
||||
```python
|
||||
# Provider-side observe feed (ratatoskr.provider.opfeed) — dispatch-layer.
|
||||
@dataclass(frozen=True)
|
||||
class OpEvent:
|
||||
ts: str # ISO 8601 UTC, capture time
|
||||
plane: str # "memory" | "affect"
|
||||
op: str # verb: search / upsert_many / emit / get / delete / handshake
|
||||
session_id: str | None # from the JWT ctx at the DISPATCH layer — present for ALL
|
||||
# JWT-carrying verbs (not just upsert_many; bifrost withholds
|
||||
# ctx from search/get/delete STORE methods, but dispatch sees
|
||||
# the JWT); None only if the JWT genuinely omits it
|
||||
status: str # "ok" | "error"
|
||||
req_summary: dict # per-verb, scope-only (see "Op-feed summary shapes"); no record bodies
|
||||
resp_summary: dict # per-verb counts + ids/scores; never verbatim content
|
||||
turn_id: str | None = None # INV-005 reservation made LITERAL: the field exists now,
|
||||
# unused in v1 (session-level correlation), populated when
|
||||
# Worldtree propagates a turn id (open question)
|
||||
|
||||
|
||||
def instrument_provider_app(app, *, plane: str, sink: OpSink):
|
||||
"""Wrap the dispatch/ASGI layer so every inbound bifrost-call emits one
|
||||
OpEvent to `sink`, reading session_id off the JWT ctx where bifrost exposes
|
||||
it. Does NOT touch store scope semantics. See FN instrument_provider_app."""
|
||||
```
|
||||
|
||||
## Exception classes
|
||||
|
||||
```python
|
||||
class BifrostHandshakeFailed(Exception):
|
||||
"""502 bifrost_handshake_failed on bound session-create. Carries the
|
||||
spec-level `detail.bifrost_error` (e.g. 'bifrost.auth_rejected')."""
|
||||
def __init__(self, *, bifrost_error: str | None, body: bytes) -> None: ...
|
||||
bifrost_error: str | None
|
||||
|
||||
class BifrostConsumerKeyMissing(Exception):
|
||||
"""A bifrost binding was requested without a consumer_key. Raised BEFORE
|
||||
HTTP (the bind must never silently fall back to the canary key)."""
|
||||
```
|
||||
|
||||
## Invariants
|
||||
|
||||
- **INV-001 (auth identity, never fall back).** A `bifrost` binding REQUIRES a
|
||||
non-empty `consumer_key`; absence raises `BifrostConsumerKeyMissing` before any
|
||||
HTTP. The bound POST /sessions authenticates with `consumer_key`; an unbound
|
||||
create authenticates with the client's default canary key. The two call sites
|
||||
never cross. On 401-rooted handshake failure the error names the mismatch.
|
||||
- **INV-002 (bind-time, not turn-time, failure).** The handshake runs
|
||||
synchronously on POST /sessions. A bad URL / down provider / wrong key / HTTPS
|
||||
rejection fails SESSION CREATION (502 → `BifrostHandshakeFailed`), surfaced on
|
||||
the create path BEFORE any turn / before alt-screen (TUI) — never deferred to
|
||||
first-turn. Mirrors issue #6's pre-alt-screen error routing.
|
||||
- **INV-003 (one plane per session).** A binding targets exactly one endpoint =
|
||||
one plane. v1 documents this limit explicitly; binding both planes for one turn
|
||||
is the PARKED composite-endpoint feature, not a v1 path.
|
||||
- **INV-004 (no scope-semantics change).** The observe instrumentation is
|
||||
READ-ONLY over the dispatch path; it MUST NOT alter `_scope_matches`, the
|
||||
AND-parity, or any store behavior. The op-feed reports the effective scope used
|
||||
per op; it never rewrites scope client-side.
|
||||
- **INV-005 (correlation key at the dispatch layer).** session_id is captured from
|
||||
the JWT ctx at the dispatch/ASGI layer — present for ALL JWT-carrying verbs,
|
||||
INCLUDING search/get/delete (bifrost withholds ctx from those STORE methods, but
|
||||
the dispatch layer still verifies + reads the JWT). `session_id=None` ONLY if the
|
||||
JWT genuinely omits it (a claims-dump open question, not the store-method gap).
|
||||
`OpEvent` carries a literal `turn_id: str | None = None` field — the reservation
|
||||
is a real field defaulted to None in v1, not a future schema change.
|
||||
- **INV-006 (data hygiene).** Bound debug sessions write to DURABLE stores. The
|
||||
smoke procedure uses an explicit test scope (`end_user:smoke-user`) and a
|
||||
documented cleanup path; the contract's acceptance asserts the fixture
|
||||
before/after so a debug run's promotions are visible, never silent. (Promotion
|
||||
of a turn is expected behavior, not a bug — but it must be observable.)
|
||||
- **INV-007 (observe captures failures + late ops).** The op-feed records non-2xx
|
||||
/ error ops (status="error"), never hides or double-counts them. The `OpSink` is
|
||||
a CONTINUOUS append-only feed — NOT per-session-scoped, no per-session teardown in
|
||||
v1; late ops that land AFTER the SSE turn-end simply append with their timestamp,
|
||||
so a consumer can apply a post-turn grace window (the PARKED pane-UI's concern; v1
|
||||
just must not drop late ops). A sink write that FAILS is swallowed from the
|
||||
dispatch path (instrument_provider_app POST-003) BUT logged to stderr — an observe
|
||||
gap is never silent.
|
||||
- **INV-008 (both presenters in lockstep).** The bind trigger + bound-state
|
||||
indicator land in CLI, TUI, and web together (the repo's add-to-all-presenters
|
||||
rule). Web creates the bound session SERVER-SIDE; the consumer key never reaches
|
||||
the browser.
|
||||
- **INV-009 (secret hygiene).** Both keys are redacted in all UI/logs. The
|
||||
consumer key is PRIVILEGED (it is the handshake identity), not equivalent to
|
||||
read-only conversation access; config that stores it uses no weaker file
|
||||
permissions than the canary key (cf. provider.env mode 600).
|
||||
|
||||
## Data flow
|
||||
|
||||
BIND (client → Worldtree): CLI/TUI/web resolve `plane` + the consumer key →
|
||||
`endpoint_for_plane(plane, worldtree_visible_host)` → `BifrostBinding` →
|
||||
`create_session(..., bifrost=binding, consumer_key=...)` → POST /sessions with
|
||||
bearer = consumer key → Worldtree handshakes synchronously to the provider
|
||||
endpoint → 201 (bound) or 502 (`BifrostHandshakeFailed`).
|
||||
|
||||
OBSERVE (Worldtree → provider → feed): a bound turn makes Worldtree dispatch
|
||||
affect/memory bifrost-calls to the provider → `instrument_provider_app` wraps the
|
||||
dispatch layer → one `OpEvent` per call (session_id from JWT ctx when present) →
|
||||
`OpSink` (JSONL) → the debug surface reads structured lines. The store path is
|
||||
untouched (INV-004); observe is read-only over dispatch.
|
||||
|
||||
## Function contracts
|
||||
|
||||
```contract
|
||||
FN create_session(client: httpx.AsyncClient, agent_id: str, *, end_user_id: str | None = None, bifrost: BifrostBinding | None = None, consumer_key: str | None = None) -> SessionInfo
|
||||
BRIEF: POST /sessions; when a bifrost binding is given, authenticate with the consumer key (not the canary key) and carry the binding so Worldtree handshakes synchronously to our provider.
|
||||
|
||||
PRE: [PRE-001 hard] bifrost is not None ⇒ consumer_key is a non-empty str -- else BifrostConsumerKeyMissing, before any HTTP (INV-001)
|
||||
PRE: [PRE-002 soft] bifrost is None ⇒ request uses the client's default canary bearer -- unchanged pre-#17 path
|
||||
POST: [POST-001 return_value] 201 → SessionInfo (unchanged parse) -- assert
|
||||
POST: [POST-002 exception] 502 → BifrostHandshakeFailed(bifrost_error=detail.bifrost_error, body) (INV-002) -- assert
|
||||
POST: [POST-003 exception] 422 ephemeral_does_not_accept_bifrost → SessionApiFailed -- assert
|
||||
POST: [POST-004 exception] 404 → AgentNotFound; other non-201 → SessionApiFailed -- assert (unchanged)
|
||||
STEPS:
|
||||
1. body = {"agent_id": agent_id}; if end_user_id: body["end_user_id"] = end_user_id
|
||||
2. if bifrost: body["bifrost"] = {"endpoint_url": bifrost.endpoint_url, "scope": bifrost.scope}; headers = {"Authorization": f"Bearer {consumer_key}"}
|
||||
3. else: headers = {} (httpx client default bearer = canary key)
|
||||
4. resp = await client.post("/sessions", json=body, headers=headers); route status per POST-*
|
||||
```
|
||||
|
||||
```contract
|
||||
FN endpoint_for_plane(plane: str, base_host: str) -> str
|
||||
BRIEF: Map a plane name to the Worldtree-visible provider base URL (memory->:8391, affect->:8390).
|
||||
|
||||
PRE: [PRE-001 hard] plane in {"memory", "affect"} -- else ValueError
|
||||
POST: [POST-001 return_value] returns f"http://{base_host}:{port}", port 8391 (memory) / 8390 (affect) -- assert
|
||||
STEPS:
|
||||
1. port = 8391 if plane == "memory" else 8390
|
||||
2. return the Worldtree-VISIBLE base URL (not client loopback); HTTPS relaxation is allowlist-side, not a URL concern
|
||||
```
|
||||
|
||||
```contract
|
||||
FN instrument_provider_app(app, *, plane: str, sink: OpSink) -> ASGIApp
|
||||
BRIEF: Wrap the provider's dispatch layer so each inbound bifrost-call emits one structured OpEvent (session_id from the JWT ctx) without touching store semantics.
|
||||
|
||||
PRE: [PRE-001 hard] app is a built provider ASGI app; sink is an OpSink -- guard
|
||||
POST: [POST-001 side_effect] emits exactly one OpEvent per inbound bifrost-call, incl. handshake + error ops (INV-007) -- assert
|
||||
POST: [POST-002 state_change] OpEvent.session_id = JWT ctx session_id when present, else None (INV-005) -- assert
|
||||
POST: [POST-003 side_effect] a sink failure never propagates into the dispatch path — observe must not break serve -- assert
|
||||
POST: [POST-004 return_value] store scope semantics untouched; read-only over dispatch (INV-004) -- assert
|
||||
STEPS:
|
||||
1. wrap the dispatch/ASGI layer so each inbound bifrost-call yields one OpEvent
|
||||
2. read session_id off the JWT ctx if present; else None
|
||||
3. summarise req (scope_filter/top_k for search; record-count+scopes for upsert) + resp (hit-count+ids/scores | upserted+replayed | error code) — NEVER verbatim content
|
||||
4. emit to sink; swallow sink errors
|
||||
```
|
||||
|
||||
## ERROR_ROUTING
|
||||
|
||||
| Wire | Exception | Surfaced |
|
||||
|---|---|---|
|
||||
| 502 `bifrost_handshake_failed` | `BifrostHandshakeFailed(bifrost_error)` | create path, names the bifrost_error; TUI pre-alt-screen |
|
||||
| (pre-HTTP) bifrost w/o consumer_key | `BifrostConsumerKeyMissing` | config/CLI validation, before any request |
|
||||
| 422 `ephemeral_does_not_accept_bifrost` | `SessionApiFailed` | create path |
|
||||
| 401 at provider handshake (manifests as 502 to client) | `BifrostHandshakeFailed('bifrost.auth_rejected')` | error text: "bound create requires the consumer key, not WORLDTREE_API_KEY" |
|
||||
|
||||
## Acceptance — the ordered live-smoke gate (load-bearing)
|
||||
|
||||
The repo's smoke-is-load-bearing posture: this gate IS acceptance, run manually
|
||||
against personal Worldtree, mirroring `docs/bifrost-self-test.md`.
|
||||
|
||||
```
|
||||
1. providers up → memory :8391 serving + op-feed sink attached (affect :8390 is symmetric, OPTIONAL for the memory-plane gate)
|
||||
2. consumer key set → RATATOSKR_BIFROST_CONSUMER_KEY present; canary key separate
|
||||
3. allowlist OK → endpoint_for_plane host on Worldtree's BIFROST_CLIENT_ALLOWED_HOSTS
|
||||
4. bind 201 → ratatoskr --bifrost-plane memory → bound session, handshake 200
|
||||
5. negative: canary → binding with the canary key → BifrostHandshakeFailed, the auth_rejected message names the consumer-key mismatch
|
||||
6. turn → one turn into the bound session
|
||||
7. assert op-feed → OpEvent captured with session_id == the BOUND session's id (not merely "some session_id"); {plane, op, req/resp summary, status}
|
||||
8. assert hygiene → fixture before/after asserted (per docs/bifrost-self-test.md); promotions visible not silent
|
||||
```
|
||||
|
||||
Unit tests (respx) cover: bind body shape, consumer-key override, the
|
||||
missing-key precondition, 502→BifrostHandshakeFailed mapping, 422 ephemeral, and
|
||||
the op-feed emitting one OpEvent per dispatched call incl. error + late op.
|
||||
|
||||
## v1 clarifications (paraphrase-gate fixups)
|
||||
|
||||
Pinned in response to the `/heid-contract-review` panel — closing under-specs that
|
||||
let an implementer comply while violating intent.
|
||||
|
||||
- **Op-feed summary shapes (per verb).** `req_summary` / `resp_summary` are
|
||||
scope-only, never verbatim content:
|
||||
- `search` → req `{scope_filter, top_k}`; resp `{hit_count, [{chunk_id, score}]}`
|
||||
- `upsert_many` → req `{record_count, [scope]}`; resp `{upserted, replayed}`
|
||||
- `get` / `get_many` → req `{ids}`; resp `{found_count}`
|
||||
- `delete_many` → req `{ids}`; resp `{deleted}`
|
||||
- `emit` (affect) → req `{actor-scope}`; resp `{status}` (affect stays conduit-opaque)
|
||||
- `handshake` → req `{caps_requested}`; resp `{caps_granted, ok}`
|
||||
- **`BifrostBinding.scope` is an opaque pass-through** (Worldtree spec: ≤256 chars,
|
||||
copied into the JWT payload unchanged). ratatoskr does NOT interpret it; v1 sends
|
||||
`null`. A non-null value is operator-supplied and meaningful only to Worldtree.
|
||||
- **Web bind split (INV-008 sharpened).** The web UI selects the PLANE; the consumer
|
||||
key is SERVER-HELD (env/config), never sent from the browser; the server
|
||||
constructs the bound session. The browser never sees the consumer key.
|
||||
- **Bound-state indicator** shows at least `plane + endpoint + bound|failed status`,
|
||||
not a bare boolean (so the operator can see WHICH identity/endpoint bound).
|
||||
- **401-handshake message scoping.** The "use the consumer key, not WORLDTREE_API_KEY"
|
||||
text is keyed on `bifrost_error == "bifrost.auth_rejected"`; other 502 handshake
|
||||
failures surface the generic `BifrostHandshakeFailed` with their own `bifrost_error`.
|
||||
- **`endpoint_for_plane` is the DEV helper** (returns `http://`, allowlist-relaxed).
|
||||
A production HTTPS endpoint is supplied directly via `--bifrost-url`, bypassing the
|
||||
plane shortcut — HTTPS is not constructed by `endpoint_for_plane`.
|
||||
- **422 `ephemeral_does_not_accept_bifrost` → `SessionApiFailed` is deliberate** (no
|
||||
distinct exception; it is an operator config error, surfaced as a generic create
|
||||
failure). Not an oversight in the ERROR_ROUTING overlap with POST-004.
|
||||
|
||||
## Out of scope / PARKED (anti-creep)
|
||||
|
||||
- Composite endpoint (both planes, one session) — vNext; needs per-store parity + per-plane failure status.
|
||||
- Turn-correlated debug-pane UI — needs turn_id (open question) + grace-window buffering + client read channel.
|
||||
- Key identity unification (Heimdall) — operator's call, crosses service boundary.
|
||||
- Auto-bind on Tier-3 agents — operator's call; v1 is explicit opt-in only.
|
||||
- Provider axis-lattice validation (the agent_self parity item) — gated on the `agent_self`-canonicity cross-repo resolution; tracked separately, NOT #17.
|
||||
+52
-21
@@ -1,6 +1,6 @@
|
||||
# Persistent memory — ratatoskr
|
||||
|
||||
_Last updated: 2026-06-15_
|
||||
_Last updated: 2026-06-16_
|
||||
|
||||
This file captures durable intent and supporting evidence (goals, decisions,
|
||||
foot-gun warnings, in-flight state) across context resets. Read it at session
|
||||
@@ -41,7 +41,7 @@ model output is untrusted); upstream API key stays server-side (INV-003).
|
||||
|
||||
## Current state / in-flight
|
||||
|
||||
_As of 2026-06-15:_
|
||||
_As of 2026-06-16:_
|
||||
|
||||
**Ratatoskr now has a SECOND identity: the v1 Bifrost Tier-3 consumer** — the
|
||||
durable persistence provider Worldtree writes Tier-3 agent affect/persona +
|
||||
@@ -56,21 +56,43 @@ background shell (`ratatoskr-provider`, `0.0.0.0:8390`, env-sourced from
|
||||
Worldtree **v0.35.2** (`10.250.50.152`): handshake 200 + `affect.emit` 200 →
|
||||
durable row persisted, opacity held.
|
||||
|
||||
**MEMORY plane: PROVIDER LIVE-PROVEN** (v0.17.3, commit `cd12951`; provider
|
||||
running on `0.0.0.0:8391` as a dev bg shell). Store + dev shell shipped (7
|
||||
contract blocks, 26+4 tests, #195 parity, heid-reviewed zero-drift) AND the
|
||||
live round-trip ran against personal **v0.35.3**: handshake + `upsert_many`
|
||||
(persist) + `search` (recall-dispatch) all green; `memory.db` holds 3 durable
|
||||
chunks (2 facts + 1 promoted question, scope `end_user:smoke-user`, 1024-d
|
||||
vec-indexed). **Proven: persist + dispatch + our search-correctness** — direct
|
||||
probe (embed the recall query via gateway `qwen3-embedding`, search our live
|
||||
store) recalls the right fact at cosine 0.60, correctly ranked. **NOT proven,
|
||||
and upstream (not ours): end-to-end recall-INJECTION** — Worldtree's
|
||||
recall-assembly doesn't surface our hits into the prompt (cold-session probe:
|
||||
search dispatched 200 but the model said "no memory"). worldtree-dev looped in
|
||||
(thread `01KV7JH8…`; #295 held open). Provider stays up with the live fixture.
|
||||
So NO minor bump / "all-green" announce until worldtree-dev closes the injection
|
||||
+ question-promotion gaps.
|
||||
**MEMORY plane: PROVIDER LIVE-PROVEN + recall-miss ROOT-CAUSED (upstream).**
|
||||
Store + dev shell shipped (v0.17.3, `cd12951`; running on `0.0.0.0:8391`).
|
||||
`memory.db` holds 3 durable chunks (choc-fact `498ed752` sal 0.9, name `8241e569`
|
||||
sal 0.8, promoted-question `c863bb6b` sal 0.9 — all scope `{end_user:smoke-user}`).
|
||||
**The #295 cold-recall miss is now ROOT-CAUSED and it's UPSTREAM, not ours**
|
||||
(2026-06-16 debug-assist with worldtree-dev): a self-driven bound cold-recall
|
||||
probe captured the inbound pair via our new observe log — Worldtree's recall sends
|
||||
`scope_filter={end_user:smoke-user, agent_self:ratatoskr:smoke}` (TWO axes) but our
|
||||
chunks carry `{end_user}` ONLY; our AND `_scope_matches` (byte-faithful to bifrost
|
||||
reference `reference_server/memory.py:398`) drops everything on the unmatched
|
||||
`agent_self` axis → 0 hits → the model says "no memory". So **our store + search
|
||||
are SOUND**; the fix is Worldtree-side. F2 (question-promotion) → research issue
|
||||
**#296** (keyword-regex salience suspected fundamentally flawed; `c863bb6b` is a
|
||||
corpus fixture, KEEP PINNED). F1 (recall-miss) → research issue **#297**
|
||||
(Worldtree-local fix = per-visible-scope single-axis search unioned client-side;
|
||||
HELD until the agent_self lattice question lands — see Recent decisions).
|
||||
|
||||
**OBSERVE BRICK SHIPPED** (`memory_store.py`, uncommitted at snapshot time):
|
||||
structured `[memory-provider]` request/response logging on the memory-call path —
|
||||
the first concrete brick of #17's observe half, and the lens that caught #295's
|
||||
root cause. Live-verified.
|
||||
|
||||
**SELF-DRIVE PROVEN BY HAND** (2026-06-16): ratatoskr's own client drove a
|
||||
Bifrost-bound cold-recall end-to-end (bind → handshake 200 → turn → captured the
|
||||
recall pair). Load-bearing finding: a bound session-create must use the CONSUMER
|
||||
Heimdall key (`RATATOSKR_HEIMDALL_KEY`) as the bearer, NOT the canary
|
||||
`WORLDTREE_API_KEY` — Worldtree signs the Bifrost handshake JWT with the
|
||||
session-create bearer (canary → 401; consumer → 200). Runbook:
|
||||
`docs/bifrost-self-test.md`. This is #17's substrate, proven before the contract.
|
||||
|
||||
**ISSUE #17 (self-drive + observe) — CONTRACT WRITTEN + HEID-REVIEWED + FIXED,
|
||||
TDD NEXT.** `docs/contracts/issues/17.contract.md` (validates OK, drift-clean).
|
||||
v1 scope operator-locked: single-plane bind (composite endpoint PARKED) +
|
||||
dispatch-layer op-feed with session-level correlation (turn-pane UI PARKED).
|
||||
`/heid-contract-review` panel caught + fixed two real internal inconsistencies
|
||||
(OpEvent `turn_id` reservation made literal; the `session_id`-for-all-verbs
|
||||
correction). NEXT: TDD tracer-bullet (bind affect plane → turn → assert OpEvent).
|
||||
|
||||
**Sindra:** a REGISTERED Tier-3 agent (`ratatoskr:sindra`, was model
|
||||
`artemis-31b-v1i`) — registration is REQUIRED to use a Tier-3 character (a
|
||||
@@ -86,10 +108,10 @@ registration.
|
||||
`~/.config/ratatoskr/provider.env` (mode 600, nh3-dev) — `consumer="ratatoskr"`,
|
||||
HS256 = the API-key STRING utf-8-encoded; rotate via infra-ops.
|
||||
|
||||
**Pushed:** `main` is in sync with `origin` (pushed through `e57b054`); tags
|
||||
`v0.17.3` + a straggler `v0.8.2` pushed. Only `graphify-out/GRAPH_REPORT.md`
|
||||
runs dirty (auto-regenerated by the commit hook — generated artifact, not
|
||||
chased).
|
||||
**Committed (2026-06-16, NOT yet pushed):** the observe brick (logger, patch
|
||||
bump), `docs/bifrost-self-test.md`, the #17 contract, + this snapshot. Push is
|
||||
the operator's call. `graphify-out/GRAPH_REPORT.md` still runs dirty
|
||||
(auto-regenerated artifact, not chased).
|
||||
|
||||
**Still standing from before:** Worldtree spec pin v0.29.0 (`562001a`) for the
|
||||
conversation-API/TUI surface (untouched by the Bifrost work). Codex-first pilot
|
||||
@@ -151,6 +173,12 @@ decision. Captures rationale that won't be obvious from code alone.
|
||||
- `[2026-06-15]` **Providers run as dev-box BACKGROUND SHELLS, not infra-ops/systemd** (operator call — it's a dev box). `ratatoskr-provider` (affect) + a future `ratatoskr-memory-provider` as background processes; no productionization track.
|
||||
- `[2026-06-15]` **Affect plane shipped (v0.17.2) + LIVE-PROVEN end-to-end against real Worldtree v0.35.2.** Personal handshake 200 + `affect.emit` 200 from `10.250.50.152` → durable row persisted (opacity held). HS256 key = the consumer's Heimdall API-key STRING utf-8-encoded (NOT base64/raw — the tripwire); cross-subnet route + `BIFROST_CLIENT_ALLOWED_HOSTS` allowlist all held (infra-ops-owned). worldtree-dev confirmed ADR-0009 holding as designed.
|
||||
|
||||
- `[2026-06-16]` **#295 cold-recall miss root-caused — UPSTREAM, branch (a) scope-axis asymmetry.** A self-driven bound cold-recall probe (our own client, consumer-key bearer) captured the inbound pair via the new observe log: Worldtree's recall filter carries `{end_user, agent_self}`; our chunks are `{end_user}`-only; AND-matching drops everything on `agent_self` → 0 hits. Our store + search are SOUND; the fix is Worldtree-side. F2 (question-promotion) → **#296** research; F1 (recall-miss) → **#297** research (worldtree-dev's Worldtree-local per-scope-union fix, HELD pending the lattice question).
|
||||
- `[2026-06-16]` **agent_self → make it CANONICAL (operator decided A).** The cross-repo "is agent_self a valid bifrost scope axis?" question: bifrost's reference lattice is `{end_user, group, tenant}` only (agent_self → `invalid_filter` 400); Worldtree emits agent_self (`bifrost_memory_store.py:479` #248 agent-self primitive). Operator chose canonical-not-re-expressed; worldtree-dev filed the lattice-addition with bifrost-dev (thread `01KV7PXF…`). **Implication: our store's permissive axis-acceptance becomes CORRECT once bifrost adds agent_self — so do NOT add axis-validation; our missing `_validate_scope_filter` is HELD, not a bug to fix.** #297 union build held until the axis lands.
|
||||
- `[2026-06-16]` **Self-drive auth identity: bound session-create uses the CONSUMER Heimdall key as bearer, NOT `WORLDTREE_API_KEY`.** Worldtree signs the Bifrost handshake JWT with the session-create bearer (canary key → handshake 401; consumer key → 200). Two keys, two identities. Proven by hand; documented in `docs/bifrost-self-test.md`; load-bearing for #17's Bind half.
|
||||
- `[2026-06-16]` **Issue #17 v1 scope locked (operator 1A/2A): single-plane bind + dispatch-layer op-feed.** `BifrostBindingRequest` is one `endpoint_url` (one plane per session); composite-both-planes endpoint PARKED. Observe = structured op-feed instrumented at the DISPATCH layer (bifrost passes ctx to upsert_many but NOT search/get/delete — `memory.py:244`), session-level correlation; turn-correlated pane UI PARKED (needs turn_id, TBD). Direct in-session TDD (live-smoke load-bearing). Contract `docs/contracts/issues/17.contract.md` written, `/heid`-design-consulted + `/heid-contract-review`-panel'd + fixed (validates OK). NEXT: TDD.
|
||||
- `[2026-06-16]` **Provider stores confirmed byte-faithful to bifrost's AND reference** (`reference_server/memory.py:398` `_matches_scope` = `all(...)`, identical to ours). OR-union was considered + rejected (ecosystem-wide change); flagged the silent-zero foot-gun to bifrost-dev (docs-only landed, bifrost stays 0.6.4). Do NOT flip `_scope_matches` to OR.
|
||||
|
||||
_For per-issue TDD implementation notes, Volva findings, and contract amendments, see the git log — every per-issue commit carries a structured message capturing the trail._
|
||||
|
||||
## Tried and abandoned
|
||||
@@ -186,3 +214,6 @@ defense against re-attempting the same cul-de-sac.
|
||||
- `[2026-06-15]` **Diagnostic: our recall-search is SOUND — the cross-session recall gap is UPSTREAM, not the store — and it caught an upstream bug.** Embedded the recall query via gateway `qwen3-embedding` + searched our live store directly → the dark-chocolate fact recalls at cosine 0.60, correctly ranked above the unrelated name fact (0.16). So the cold-session recall failure is Worldtree's recall-assembly/injection (hits not reaching the prompt), NOT our search. ALSO found a latent UPSTREAM bug: a recall QUESTION got promoted as a durable chunk and ranks **#1 (0.70 > the fact's 0.60)**, polluting recall. Relayed to worldtree-dev (thread `01KV7JH8…`). **This is exactly #17's thesis — ratatoskr-as-provider caught an upstream bug invisible from the chat side.**
|
||||
- `[2026-06-15]` **"Wire 200 ≠ recall works" — prove recall efficacy at the model's answer in a COLD (history-free) session, not on the wire.** A `search`/memory-call returns 200 whether or not its results are injected into the prompt, and same-session "recall" can be plain session history. infra-ops' cold cross-session probe caught my premature "all-green" (search dispatched 200, model had no memory). Don't call cross-session recall proven from a clean wire.
|
||||
- `[2026-06-15]` **Issue #17 filed — Bifrost-binding for the chat client (self-drive + correlated-log affect/memory ops).** REVERSES design-brief §6's "no Bifrost-binding consumer support" — that negative clause predates ratatoskr's provider identity (2026-06-14), so the canary now owns both ends but its client can't drive its own provider (`create_session` sends only `{agent_id, end_user_id}`; no Bifrost `endpoint_url`). Today's smoke proved the substrate (bind→dispatch→persist); only the observe/log channel design (open question #5) remains. The recall-injection caveat is upstream and doesn't block #17. NEXT on #17: `/heid` consult on the now-grounded framing → contract → TDD. (tracked: Gitea #17, labels enhancement/observability/tui)
|
||||
- `[2026-06-16]` **`scripts/contract_drift_check.py` defaults `GITEA_REPO` to "Worldtree"** (line 74), so a bare run in ratatoskr false-positives DRIFT by hashing Worldtree's same-numbered issue. Always `export GITEA_REPO=ratatoskr GITEA_OWNER=vh` (env.sh leaves the GITEA vars commented out) before running the drift-checker here.
|
||||
- `[2026-06-16]` **My #295 coupling hypothesis (the promoted question crowds out the fact at small top_k) was REFUTED** — worldtree-dev's recall over-fetches `top_k=128` (`injector.py:203`/`_store_helpers.py:101`), so the question can't crowd the fact out at search level. Reasonable cross-frontier hypothesis, correctly framed as a hypothesis not a conclusion; the real cause was the scope-axis asymmetry. Lesson: offer provider-side hypotheses, let the upstream owner check them against their code.
|
||||
- `[2026-06-16]` **Contract drifted from its own design in two spots, caught only by `/heid-contract-review` (not same-author paraphrase):** the `OpEvent` dataclass omitted the `turn_id` that INV-005 promised; the `session_id` comment said "None for search/get/delete" contradicting the dispatch-layer design (the JWT carries session_id for all verbs at dispatch). Cross-model paraphrase is load-bearing for catching an author's own contract-vs-intent drift.
|
||||
|
||||
Reference in New Issue
Block a user