Bifrost shipped 1.0.0 — first stable release, freezing wire v0.6 (the
surface ratatoskr's combined-builder consumer #18 already adopted).
Non-breaking: byte-identical on the wire to the prior >=0.10.0 pin.
Switched floor pin -> exact pin per the stable-substrate posture.
Suite 506 green against bifrost 1.0.0.
The Worldtree-driven composite :8392 smoke ran and is proven + persisted:
one bound session drove the full both-plane lifecycle through one endpoint
(handshake both caps -> affect.fetch + memory.search -> affect.emit stored:true
-> memory.upsert_many upserted:1), both writes verified in our SQLite stores.
infra-ops allowlisted :8392 (01KVHWJGTT); #17 closed in the tracker. No open
legs remain on the composite; repo at a converged checkpoint.
#18 D1 SHIPPED: build_combined_provider_app on :8392 wraps bifrost 0.10.0's public
build_combined_app over both stores + the shared affect read route; one bound WT session
drives memory.* AND affect.* through one endpoint; op-feed plane='combined' per-path.
Shipped v0.17.15 (affect.fetch, the strong-or-absent prerequisite) -> v0.17.16 (composite)
-> v0.17.17 (#17 op-feed field-name fix) -> v0.18.0 (publishing minor). Suite 503 green.
Live-smoke PROVEN at wire+dispatch (real stores + bifrost 0.10.0 on a running :8392):
handshake grants both caps, PAD read route serves real sindra PAD, both planes dispatch at
one bound session_id. WT-driven turn gated on infra-ops adding :8392 to WT's
BIFROST_CLIENT_ALLOWED_HOSTS (requested).
New decisions: reference-impl-adopt-canonical (operator); v1-derived-from-WT-I/O-coverage
(operator). New foot-guns: memory-store check_same_thread bug (same as affect D2, exposed by
the contract-mandated search test via TestClient); :8392 infra-allowlist gate; heid-review
test-fidelity nudge cascaded into 2 latent-bug fixes.
Publishing-minor for the #18 arc: ratatoskr now exposes a COMPOSITE Bifrost
endpoint (build_combined_app, :8392) so one bound Worldtree session drives BOTH
the memory.* and affect.* planes through a single endpoint — completing the
Tier-3 consumer round-trip (durable memory + live PAD from one binding).
Shipped as patches v0.17.15 (affect.fetch prerequisite) → v0.17.16 (composite) →
v0.17.17 (#17 op-feed fix); this minor publishes the milestone.
Live-smoke (against real stores + bifrost 0.10.0 on a running :8392): handshake
grants BOTH caps by store presence; the PAD read route serves real sindra PAD;
both planes dispatch through the one endpoint at a single bound session_id with
the op-feed deriving plane per path. The remaining WT-driven turn is gated on
infra-ops adding :8392 to Worldtree's BIFROST_CLIENT_ALLOWED_HOSTS (requested).
The dispatch-layer op-feed's handshake req-summary read req.get("capabilities_requested"),
a field that never exists on the wire — bifrost's handshake handler reads
request_body["capabilities"] (reference_server/_protocol.py:181). So the op-feed's
caps_requested was silently ALWAYS None on every handshake. Read the real field.
Surfaced by the heid-code-review panel (Regin) during the #18 D1 review — a latent
#17 observability bug, not D1 drift. Regression test asserts caps_requested is
populated from a handshake body's capabilities.
Suite 502 -> 503 green.
One ASGI app fronting BOTH the memory.* and affect.* planes (:8392), so a single
bound Worldtree session both remembers AND shows live PAD. Closes#18 end-to-end
(D2 PAD read-endpoint shipped v0.17.14; D1 was bifrost-blocked, now unparked by
bifrost 0.10.0's public build_combined_app + FR-1 resolved — zero Worldtree change).
- provider/combined.py: build_combined_provider_app wraps bifrost.consumer.build_combined_app
over both stores + mounts the shared affect read route. Advertises both caps by store
presence; per-route call-time isolation is bifrost's (INV-013).
- affect_store.py: extract add_affect_read_route shared helper (the D2 INV-007 promise —
composite + standalone mount the SAME read route over the same affect.db, INV-011).
- opfeed.py: plane='combined' derives the OpEvent plane per request path
(memory-call->memory, affect-call->affect, handshake->combined; INV-012).
- serve_combined.py + ratatoskr-combined-provider console script on :8392 (additive —
standalone :8390/:8391 untouched, INV-014).
- contract: 18.contract.md § Deliverable 1 (INV-009..INV-014); D1 un-deferred.
Latent bug fixed (exposed by the contract-mandated memory `search` dispatch test running
through TestClient = a worker thread): open_memory_store lacked check_same_thread=False —
the SAME sqlite thread-safety bug already fixed in the affect store (D2). The composite
serves the memory plane over HTTP, so a memory-call on uvicorn's threadpool would trip it.
Fix: check_same_thread=False + PRAGMA busy_timeout=5000 (memory contract Concurrency note).
heid-code-review panel (Groa/Hulda/Regin): ZERO drift findings; the implementation matches
INV-009..INV-014 at function-block level. Folded the genuine test-fidelity fix (memory leg
describe_store -> search per the contract TEST) + added the PRE-001/PRE-002 guard tests.
Suite 486 -> 502 green.
bifrost 0.10.0's _supports_affect_plane (bifrost/affect.py:75-80) now requires a
callable fetch for the affect capability to advertise/dispatch at all (INV-012
strong-or-absent), so an emit-only store 400s on EVERY affect op — repinning past
the affect.fetch release (#12/#13) breaks our shipped affect plane until fetch
exists. Implement affect.fetch as a thin async wrapper over the existing get()
read seam, conformed verbatim to the reference InMemoryAffectStore.fetch:
{"found": False} or {"found": True, "snapshot": <verbatim>}, AffectInvalidArguments
on empty ids, opaque (INV-001 — never reads pad/valence).
This is the forced prerequisite for the #18 D1 composite (build_combined_app),
and a new Worldtree I/O point consumed (affect read-back over bifrost).
- Repin bifrost>=0.8.0 -> >=0.10.0 (uv lock: 0.8.0 -> 0.10.0)
- affect_store.py: add async fetch() over get()
- contract bifrost_affect_provider v1.2: fetch FN block + INV-010 (cap = supported+emit+fetch)
- tests: 3 fetch unit + parity_vs_reference_fetch through dispatch_affect_call
- suite 482 -> 486 green
Completes slice 3c: the browser-facing trigger for the web bind. A 'Bifrost
binding' <select> (none / memory / affect) on the setup panel; startSession
sends bifrost_plane in the create body (the consumer key stays server-held,
never sent from the browser). On a bound 201 the identity line renders the
bound-state indicator (plane + endpoint, never the key); bind failures surface
the error_code + bifrost_error in the setup error line.
Closes the persistent-memory caveat: web chat can now bind its own Tier-3
provider (memory persistence + affect telemetry), not persona+debug only.
Web suites 63 green (presentation-contract included); pure static HTML/JS.
Slice 3c of issue #17 — the web surface of the bind trigger, server side. Closes
the INV-008 lockstep (CLI + TUI + web all carry the bind now). Implements the
contract's "web bind split": the browser selects only the PLANE; the consumer key
and the Worldtree-visible host are SERVER-HELD config and never reach the browser.
- create_app gains bifrost_consumer_key + bifrost_visible_host (server-held,
from env via the entrypoint: RATATOSKR_BIFROST_CONSUMER_KEY /
RATATOSKR_PROVIDER_VISIBLE_HOST).
- _create_session_endpoint reads an optional `bifrost_plane` from the browser
body, builds the BifrostBinding SERVER-SIDE via endpoint_for_plane(plane,
visible_host), and calls create_session(bifrost=, consumer_key=). The 201
response echoes bound-state {plane, endpoint, status: bound} for the UI
indicator — never the key (INV-008/INV-009).
- Error routing: invalid plane / unconfigured server -> 400; BifrostHandshakeFailed
-> 502 {bifrost_error}; BifrostConsumerKeyMissing (server misconfig) -> 400.
5 new web bind tests (server constructs binding + key-never-leaks + upstream
carries bifrost body + consumer-key bearer; unconfigured -> 400; invalid plane;
handshake 502; no-plane unbound regression). Full suite 470 green; added lines
ruff + mypy clean (pre-existing web-file backlog untouched).
Follow-on: the index.html plane selector (UI trigger) — the server capability is
complete and TDD'd; the browser-side dropdown is a thin separate change.
LIVE-SMOKE PROVEN (this session): the CLI bind drove a bound sindra session
against personal Worldtree :8081 -> handshake 200 -> the op-feed captured 2
recall searches correlated to the EXACT bound session_id (2c0c7482), with the
real #297/#298 union-recall scopes. Bind + observe proven end-to-end live.
Slice 3b of issue #17 — the TUI surface of the bind trigger (web is 3c). The TUI
consumes the same ParsedArgs the cli already parses (--bifrost-plane / --bifrost-url
/ consumer key from RATATOSKR_BIFROST_CONSUMER_KEY), so this wires the bind into
_resolve_then_run's pre-flight create_session:
- bifrost + consumer_key threaded into create_session at the pre-alt-screen
resolution layer, so bind failures land on the operator's REAL stderr BEFORE
the Textual alt-screen opens (INV-002, mirrors issue #6's pre-alt-screen
routing) — never eaten by the alt-screen teardown.
- BifrostConsumerKeyMissing -> exit 22; BifrostHandshakeFailed -> exit 23 with the
same 401-scoping hint, keyed on bifrost_error == bifrost.auth_rejected. Exit
codes + label vocabulary match cli._amain exactly (INV-006).
- Bound-state indicator on success (pre-alt-screen): ". bifrost: status=bound
plane=... endpoint=...".
3 new TUI bind tests (handshake-fail / consumer-key-missing / bound-create carries
binding + indicator, run_async stubbed). Full suite 465 green; added lines ruff +
mypy clean (pre-existing tui.py lint/type backlog untouched per surgical-changes).
Slice 3a of issue #17 — the CLI surface of the bind trigger (TUI + web follow,
INV-008 lockstep). ratatoskr can now self-drive a bound session from the CLI:
- New flags: --bifrost-plane {memory,affect} (dev shortcut -> endpoint_for_plane
over --bifrost-host / RATATOSKR_PROVIDER_VISIBLE_HOST) and --bifrost-url (the
direct HTTPS/prod endpoint, bypassing the plane shortcut). Mutually exclusive;
a binding is a session-CREATE concern (forbidden with --session).
- Consumer key resolved from RATATOSKR_BIFROST_CONSUMER_KEY only (the privileged
handshake identity — never a CLI flag, distinct from the canary WORLDTREE_API_KEY).
- _amain threads bifrost + consumer_key into create_session and routes the bind
failures: BifrostConsumerKeyMissing -> exit 22; BifrostHandshakeFailed -> exit
23 with the 401-scoping hint ("use the consumer key, not WORLDTREE_API_KEY")
keyed on bifrost_error == bifrost.auth_rejected.
- Bound-state indicator on success: ". bifrost: status=bound plane=... endpoint=..."
— shows WHICH identity/endpoint bound, not a bare boolean.
Also fixes a pre-existing test-isolation bug: test_no_textual_import did a live
importlib.reload(ratatoskr.cli) that mutated the shared module in place, breaking
class identity (isinstance / pytest.raises) for every test after it. The real
check is the static source grep; the reload was vestigial and is removed.
9 new CLI bind tests; full suite 462 green; ruff clean (no new mypy errors).
Slice 2 of issue #17 — the OBSERVE half. New ratatoskr.provider.opfeed:
- OpEvent{ts, plane, op, session_id, status, req_summary, resp_summary,
turn_id=None} — scope-only summaries, never record bodies / PAD content
- OpSink Protocol + JsonlOpSink (continuous append-only JSONL, INV-007)
- instrument_provider_app(app, *, plane, sink): an ASGI middleware over the
built bifrost provider app. Buffers+replays the request, captures the
response, reads session_id off the dispatch JWT's "sub" claim (INV-005 —
present for ALL verbs incl. search/get/delete, which bifrost withholds from
the store method), emits exactly one OpEvent per inbound bifrost-call incl.
handshake + errors. Read-only over dispatch; store scope semantics untouched
(INV-004). A sink/summary failure is swallowed + logged, never breaks serve
(POST-003).
- Per-verb summaries: search {scope_all,scope_any,top_k}->{hit_count,hits};
upsert_many {record_count,scopes}->{upserted,replayed}; get/get_many/
delete_many {ids}->{found_count|deleted}; emit (affect, opaque)->{stored};
handshake {caps_requested}->{caps_granted,ok}; error->{error: code}
- serve_memory/serve wired: opt-in via RATATOSKR_OPFEED_PATH (maybe_instrument)
Resolves the contract's open question: the dispatch JWT DOES carry session_id
(= the "sub" claim). Tests drive the REAL bifrost dispatch end-to-end with
minted JWTs. 11 new tests; full suite 453 green; ruff + mypy clean (opfeed.py).
Slice 1 of issue #17 (Bifrost-binding the chat client) — the client-side
BIND primitive, TDD'd against docs/contracts/issues/17.contract.md.
- BifrostBinding{endpoint_url, scope=None} frozen dataclass (#160 shape)
- create_session(..., bifrost=, consumer_key=): carries the bifrost body
field and OVERRIDES the bearer to the consumer key per-request (INV-001 —
never falls back to the canary key)
- BifrostConsumerKeyMissing: raised BEFORE any HTTP when a binding lacks a
non-empty key (PRE-001)
- BifrostHandshakeFailed: 502 on a BOUND create -> carries detail.bifrost_error
(both-shape unwrap per the persona_state wire lesson); gated on bifrost!=None
so an unbound 502 stays SessionApiFailed (INV-002)
- endpoint_for_plane: memory->:8391 / affect->:8390, invalid->ValueError
7 new tests; full suite 442 green; ruff clean.
persona_state hard-404s every Tier-3 (colon-id) agent by design upstream
(WT api.py:1220, "Phase 2.0 has no Tier 3 persona") — so the Persona pane
showed "persona not available (HTTP 404)" for consumer-defined characters.
loadPersona now reads error_code + renders a clear Tier-3-aware message
(she still responds in character; only the affect/OCEAN readout is gated),
with distinct text for persona_not_configured / 403 / other.
Also (snapshot): sindra switched to thoughtful-character role
(mistral-small-4-reasoning); worldtree-dev pinged re Tier-3 persona_state
roadmap (thread 01KVCR6P); #17 (bifrost-binding the chat client) teed up as
the next-context target.
v0.17.7
Repin bifrost 0.7.0→0.8.0 and reimplement the memory store's search scope
filter to the v0.6 split (#11): scope_all (AND/intersection) + scope_any
(OR/union over a list of conjunctive scopes), at parity with the v0.6
reference _matches_scope / _validate_scope. No-compat: scope_filter removed.
scope_any is the union-visibility primitive that resolves the #295/#297
silent-zero AND foot-gun — a subset-scoped chunk now recalls via an OR
member. End-to-end cold recall now gated only on Worldtree emitting
scope_any on its recall path (#297, upstream).
- store: search(scope_all, scope_any); _scope_subset + _matches_scope + _validate_scope
- contract v1.2: search FN sig, INV-005 recomposed, PRE-003 both fields, scope_any_union test
- tests: scope_any union, scope_all∧scope_any compose, both-empty match-all; parity vs real 0.8.0 dispatch (433 green)
- #17 contract: sync stale scope_filter/_scope_matches-AND refs to scope_all/scope_any
- runbook + persistent-memory updated; provider bounced onto 0.8.0 (fresh empty db)
v0.17.6
bifrost 0.7.0 (wire v0.5) makes agent_self canonical: the scope lattice is
now {end_user, group, tenant, agent_self}. Our store was MORE permissive than
bifrost's reference (no _validate_scope_filter), which silently 0-zeroed the
#295 cold recall instead of a loud 400. Now matched: search rejects an
out-of-lattice axis with InvalidFilter (-> memory.invalid_filter 400), agent_self
admitted. Purely additive — everything that validated before still validates.
Closes the parity gap our own foot-gun flag opened (bifrost-dev shipped the
lattice add #10 off it). Pin bumped bifrost>=0.6.1 -> >=0.7.0. Contract
search PRE-003 + lattice_axes test; 2 new store tests; full suite 429 green.
Structured [memory-provider] request/response logging on the memory-call
path: search REQUEST (scope_filter, top_k) + RESPONSE (chunk_ids, scores,
scope), plus concise entry lines on upsert_many/delete_many. Self-contained
stdout handler so the lines reach the provider stdout under uvicorn.
Additive observability only — no search-semantics change (AND-parity with
bifrost's reference store holds). This is the first concrete brick of #17's
observe half, and the lens that root-caused #295's cold-recall miss (the
persist/recall scope-axis asymmetry) from the provider side.
The second plane of ratatoskr's Tier-3 Bifrost consumer: a durable memory
store Worldtree writes agent memory chunks into (upsert_many) and recalls
by vector similarity (search), with point reads + deletes. Implements
bifrost's own MemoryDataStore Protocol; conformance is #195 parity vs
InMemoryMemoryStore through the real dispatch_memory_call.
Store (memory_store.py): open_memory_store, describe_store, upsert_many
(replay/conflict idempotency, optimistic locking, injection rule, atomic
batch), search (cosine over sqlite-vec vec0, scope isolation INV-005,
over-fetch-then-filter so top_k counts in-scope), get/get_many,
delete_many, build_memory_provider_app. Dev shell (serve_memory.py):
ratatoskr-memory-provider entrypoint, port 8391.
TDD + heid-code-review (panel Groa/Hulda/Regin, zero true drift). Adopted
fixups: scope_filter dict guard, top_k<=0 -> [], stronger scope-isolation
+ delete-hit-search + handshake-POST tests. Partial-map optimistic-lock
semantics pinned against the reference via a new expected_revisions
parity test.
26 memory + 4 serve tests; #195 parity (upsert/search/expected_revisions)
green; ruff clean. Deps: +sqlite-vec.
The Intensity/Mood/Temperature/Willingness system-speak ban now explicitly
covers every output layer — third-person narrative, descriptive passages,
internal monologue — not just first-person dialogue, until the form reverts.
Clears the two ✗ FAIL (missing STEPS) the v2.1 parser surfaced.
#3: faithful STEPS for CliPresenterState.render, _format_duration_ms,
_format_usage (the two formatters also gain PRE/POST from their real
asserts). render STEPS enumerate AffectUpdate + AwaitingLlmFirstToken
as demoted telemetry (Worldtree #204/#201), extending POST-005 beyond
the issue #12 set.
#4: refresh the TUI presenter contract from the abandoned single-RichLog
double-display model to the shipped four-pane live-Markdown model
(v0.5.0-v0.14.0 + Worldtree #201/#204). Rewrites TuiPresenterState.render
and _stream_turn_worker (signature, POSTs, STEPS, TESTS), INV-005, the
[performance] constraint, the COMPOSE sketch, the CLASS block
(BRIEF/PROPERTIES/INV-WIRE-002), the resolved open_question, and the
_cancel_via_sse call site. Verified against src/ratatoskr/tui.py and the
real test names in tests/test_tui.py.
Both contracts: 0 validation errors (pre-existing multi-tracer warnings
on _run_turn / action_interrupt left untouched).
Pulls the upstream contract_parser.py canonical: v2.1 + issue-scoped
contract validation (target_module/scope/prd frontmatter, four new
test categories scenario/trace/adversarial/property, issue-aware
human/list output). Re-pins pinned_sha256_16 + pinned_at in the
manifest. canonical_drift.py reports 7/7 OK; no sync-induced contract
regressions.
Stand-up glue for the live affect negotiation smoke vs Worldtree v0.35.2.
- ratatoskr.provider.serve: build_app_from_env (tested env->app seam) + main
(uvicorn shell). Config from env: RATATOSKR_HEIMDALL_KEY (required),
RATATOSKR_AFFECT_DB, RATATOSKR_CONSUMER_ID, RATATOSKR_PROVIDER_HOST/PORT.
- console-script ratatoskr-provider = ratatoskr.provider.serve:main.
- Heimdall key utf-8-encoded into HS256 key_bytes; revisit if Worldtree signs
with base64/raw bytes (confirm format at wire-up).
The first slice of ratatoskr-as-Tier-3-Bifrost-consumer: a SQLite-backed,
conduit-opaque affect store Worldtree emits Tier-3 persona/affect snapshots
into, plus the thin build_affect_provider_app wiring. The bifrost library
owns the wire; this owns only the store + glue.
- ratatoskr.provider.affect_store: open_affect_store / emit / get /
build_affect_provider_app. Two-table SQLite schema (snapshots + idempotency).
- Conduit-opaque (INV-001): reads only the two addressing keys; whole-blob
hash only. LWW-by-arrival across distinct keys; replay-or-conflict
idempotency (actor-scoped from ctx), raising bifrost's typed exceptions.
- Conformance: #195 parity vs InMemoryAffectStore through the real
dispatch_affect_call engine. 17 provider tests; 395 full suite.
- Contract v1.1: idempotency model corrected to bifrost's actual semantics
(caught by real-lib TDD; the artifact-only review structurally could not).
Heid-panel reviewed (contract + code); idempotency-cache TTL pruning,
memory.* plane, and the combined two-plane server deferred (see Out of scope).
Onboard ratatoskr as the Tier-3 Bifrost consumer (durable memory +
persona/affect persistence Worldtree writes into). Lands the dependency
and the reviewed affect-plane spec; no production code yet — the patch
bump fires when the store lands at TDD-green.
- pyproject: bifrost>=0.6.1 in a `provider` optional-extra (+ starlette,
jsonschema); gitea PyPI index wired, bifrost sourced from it.
- uv.lock: bifrost 0.6.1 + jsonschema resolved from the gitea registry.
- docs/contracts/bifrost_affect_provider.contract.md: SQLite-backed,
conduit-opaque affect store (emit + ASGI wiring). Heid-panel-reviewed
(Groa/Hulda/Regin), amended for 8 text-ambiguity findings.
Establish agents/ as the home for Worldtree Tier-3 test-agent persona
sources (previously transient in /tmp). Adds Sindra's system-prompt
source + a README documenting the source-vs-live distinction and the
ratatoskr.tier3 patch sync command.
Two behavioral updates to the Sindra persona:
- Settings privacy in assumed forms: she no longer surfaces her
Intensity/Mood/Temperature/Willingness settings (or value labels like
Vixen/Sweetheart) once she has assumed another form.
- Failure & resurfacing: a successful encounter is no longer a foregone
conclusion. He can fail by disengaging or by failing to win her over
in the resist tiers; on failure she resurfaces as Sindra (reverting an
assumed form), tells him plainly, and offers comfort or a retry.
No version bump: test-agent assets + docs, no production-code change.
A design pass through /frontend-design on the web companion to retain
all debugging richness while raising usability and polish. Single-file
vanilla HTML/CSS/JS; no build, no CDN, no node_modules. HTTP surface,
endpoints, presentation contract, and INV-001..009 all unchanged.
Aesthetic direction: "Aurora telemetry instrument."
- Runic glyph + wordmark, live connection dot (idle = aurora-green,
streaming = pulsing cyan, error = dawn-red), session identity right-
aligned. Persona summary lives inline in the top bar with labeled
P/A/D micro-bars (centered baseline, [-1,1] mapped).
- Aurora signature band — thin cyan→blue→green shimmer animation
at the top edge, echoed on the setup card.
- Conversation column with turn-divider rules, cyan ❯ prompt echoes,
assistant text with a live cyan left-rule. Terminal events become
status chips (done = aurora-green, error = dawn-red, cancelled =
dawn-yellow) with metadata. Animated "awaiting first token · Ns"
indicator with elapsed counter.
- Telemetry column: tabs with live count badges that flash on new
events, sticky pane header carries the active pane name + copy
button, new-line flash highlight on each pane append, structured
empty states, persona pane structured render.
- Composer: real input, send/cancel buttons, streaming-lock state.
- Status line: keyboard legend + version footer.
- Centered setup card overlay with styled agent select on first open.
- All-monospace by intent (no-CDN constraint + right for a wire-
monitor tool); system mono stack. CSS-only motion (pulse, shimmer,
staggered rise, flash, awaiting dots).
Live Markdown — transcript response + thinking panes:
- Hand-rolled markdownSafe() renderer: escape-FIRST (INV-004
preserved), then a whitelist subset (headings, bold, italic, inline
code, fenced code blocks, ordered/unordered lists, blockquotes,
links). No raw HTML passthrough. Link href restricted to http(s):// +
conservative charset (rejects javascript:, attribute-breakout URLs).
- Per-turn live buffer; each text/thinking delta re-renders the
accumulated buffer in place (same pattern as the TUI's v0.9.0 live
MD rendering).
- Verified under node: rendering (bold/italic/code/lists/headings/
fences/safe links) + XSS neutralization (script tags, javascript:
schemes, attribute-breakout URLs, img onerror) all behave correctly.
- Tools/Debug/Persona panes stay literal monospace by deliberate
choice: they carry our structured audit lines + JSON, where MD would
corrupt readability (underscores in tool names, JSON braces, etc.).
Thinking pane per-turn breaks:
- Each turn lays down a labeled `── turn N ──` divider in the
thinking pane. The prior turn's live block is closed and a fresh
MD-rendered block opens below the divider, so each turn's chain-
of-thought is its own break-separated section.
Tests: 378 passing (no test change — server-side surface unchanged).
Markdown safety verified via standalone node harness exercising
rendering + 4 XSS vectors.
Minor per SemVer discipline: substantial new browser-side behavior
(live Markdown rendering, redesigned presentation) that consumers
would opt into via the next launch. No HTTP-caller adapts.
Initial AGENTS.md for the Codex-first coding discipline pilot.
Ratatoskr is the chosen pilot repo per operator decision 2026-05-29.
Defines: codex session's althing handle (ratatoskr-codex), reviewer
handle (ratatoskr-dev), branch + PR conventions, write-window
handshake protocol per discipline spec §7, attribution discipline,
out-of-scope files, and bootstrap protocol.
Depends on: brokkr-smithy/docs/codex-first-discipline.md v0.1
(brokkr-smithy commit 5dd061c, tag v0.5.3), corviduo-project-template/
docs/althing-monitoring.md (template commit 80e35d2) for inbound
transport canonical.
No version bump — convention/config-shape edit with no runtime effect.
Second Heid panel pass (thread 01KSPBMFRRQE) on the v0.16.0 tree:
Gróa returned zero findings; Hulda surfaced two minor tightening
items, both closed here.
1. test-gap — TestStreamFullEventVocab drove only 8 of 11 Event types
through the stream endpoint (omitted Error, Cancelled, AffectUpdate).
Serialization for all 11 was already covered by the presentation-
contract fixture tests; this was a stream-integration coverage gap.
- Added AffectUpdate to the vocab stream (non-terminal, coexists
with done).
- Added dedicated test_error_terminal_event + test_cancelled_terminal_event
(terminal events are mutually exclusive with done, so they can't
share one stream).
2. precision — the disconnect-cancel path caught bare `except Exception:
pass`, silently swallowing real CancelFailed / transport errors. The
contract intent is to swallow only the cooperative race
(CancelAlreadyCompleted). Narrowed: swallow CancelAlreadyCompleted /
CancelTurnNotFound as the no-op race; log unexpected cancel failures
as a structured stderr line for diagnosability. Never re-raises (we're
unwinding the cancelled generator and must not mask CancelledError).
Tests: +2 (376 → 378). Patch per SemVer discipline — coverage +
diagnosability tightening, no behavior change observable to callers.
Heid panel review (Gróa + Hulda, thread 01KSP5P6CSJH) on v0.15.0/
v0.15.1 surfaced one load-bearing bug + several precision items. This
pass closes them.
Load-bearing fix — cancel paths targeted the wrong turn_id:
- `_TURN_COUNTER` allocates browser-local ids (1, 2, 3…); the real
upstream Worldtree turn_id (e.g. 799) only arrives in the first SSE
event. The v0.15.x cancel/disconnect/shutdown paths posted to
/sessions/{sid}/turns/{LOCAL_ID}/cancel — wrong URL upstream.
- TurnHandle.upstream_response (dead field) → upstream_turn_id: int|None.
Captured from the first event's sse_id.turn_id in the stream
generator. All cancel paths now target it. Cancel before the upstream
stream starts (upstream_turn_id None) is a no-op
({"cancelled": false, "reason": "not_started"}).
- The old cancel tests mocked the local-id URL, so they encoded the bug;
rewritten to assert the UPSTREAM id is targeted.
Behavior change (minor-bump driver) — server-side end_user_id:
- create_app gains end_user_id kwarg; entrypoint reads
RATATOSKR_END_USER_ID and threads it in. POST /api/sessions uses
app.state.end_user_id, IGNORING any browser-supplied value (a client
can't impersonate an arbitrary end-user partition). JS no longer
sends end_user_id.
Precision fixes:
- Entrypoint missing-extras ImportError catch scoped to starlette/
uvicorn ONLY; baseline-dep / first-party import failures now
propagate as real tracebacks instead of masking as exit-12.
- Lifespan shutdown logs per-pending session_id + upstream_turn_id
(was a single aggregate count).
Tests (+18; 376 total):
- disconnect_triggers_upstream_cancel (INV-005 load-bearing — drives
the stream generator directly + cancels the consuming task; would
have caught the turn_id bug)
- cancel_targets_upstream_turn_id, cancel_before_started_is_noop,
cancel_failed_500
- server-side end_user_id: uses / ignores-body / omits-when-unset
- create_app: routes_registered / state_attached / factory_stored
- entrypoint: default_host / port_zero / happy_argv / open / no-open
- real_import_bug_propagates (precision guard)
- full_event_vocab at the stream-endpoint layer
Contract #16 amended: v0.16.0 amendment banner + INV-005/006 reworded
for upstream_turn_id + FN sketches corrected (server-side end_user_id,
upstream_response→upstream_turn_id, manual client lifecycle vs the
non-executable async-with sketch, not-started cancel branch).
Live smoke against personal:8081 during the v0.15.0 web-companion
verification surfaced that real Worldtree returns persona_state
errors in the FastAPI default envelope shape:
{"detail": {"error_code": "auth_scope_denied", "message": "..."}}
The v0.12.0 `get_persona_state` parser only inspected the top-level
`error_code` key. When the field was nested under `detail`, the
typed exception (AuthScopeDenied / PersonaNotConfigured /
AgentNotAvailable) wasn't raised; the call fell through to
SessionApiFailed, which then surfaced through the web companion as
an opaque HTTP 500 on /api/agents/{id}/persona_state.
The original test_sessions.py mocks used the flat-shape envelope, so
the bug was invisible in unit tests until the real-wire smoke.
Fix: extract error_code from either `err.get("error_code")` (flat)
OR `err.get("detail", {}).get("error_code")` (FastAPI default).
Patch per SemVer discipline — bug fix to v0.12.0 surface, no public
signature change, no new behavior. Callers that were getting the
wrong exception now get the right one; callers that were already
getting the right exception (flat-shape paths) are unchanged.
Tests: 2 new regression cases in TestGetPersonaState — one each for
the detail-envelope shape of 403 auth_scope_denied and 404
persona_not_configured. Suite: 358 passing.