Compare commits

..

2 Commits

Author SHA1 Message Date
vh 23fea72c6f docs(diagnostics): R30 φ0 — fold in mask_neg (8/8 complete)
mask_neg recovered from the demo SSE stall; well-conditioned (φ_P 0.982, r²0.95),
confirms the verdict. 6 well-conditioned runs: φ_P=0.971±0.01 joint; empty-tail
nails 0.95 exact (joint carries a small push-under-attribution upward bias).
Config faithful; verdict unchanged.
2026-07-03 07:27:26 -07:00
vh 875f291a42 docs(diagnostics): R30 Phase-1 φ0 measurement — config faithful (demo b14)
Joint two-timescale fit (brokkr-ruled) + empty-tail cross-check against the A1
corrected anchor (baseline_pad). Deployed PAD-point decay φ0 ≈ 0.95-0.97 across
3 baselines (lofn/mimir/mask), intercept c≈0 → config (decay_rate=0.05, φ=0.95)
is faithfully applied; the R29 net-0.90 was continuous re-appraisal, not re-push.
Trait-flat confirmed; A/P ratio ~uniform (not the S2-expected 1.9); φ_max rec:
relax to ~0.96. forseti signal-poor (low-baseline small excursion, excluded);
mask_neg stalled on a demo hang (7/8).
2026-07-03 07:04:00 -07:00
38 changed files with 7200 additions and 3571 deletions
+4 -19
View File
@@ -104,8 +104,8 @@ id = "worldtree-conversation-api-openapi-v2"
canonical_source = "Worldtree"
canonical_path = "docs/conversation-api-openapi.json"
consumer_path = "docs/conversation-api-openapi.json"
pinned_sha256_16 = "36148179601453a0"
pinned_at = "2026-07-06T16:09:05+00:00"
pinned_sha256_16 = "dbdf4e24c8b06c92"
pinned_at = "2026-06-30T22:25:56+00:00"
[[pins]]
id = "worldtree-conversation-api-sse-events-v1"
@@ -120,8 +120,8 @@ id = "worldtree-conversation-api-spec-v1"
canonical_source = "Worldtree"
canonical_path = "docs/conversation-api-spec.md"
consumer_path = "docs/conversation-api-spec.md"
pinned_sha256_16 = "c656a789caceef14"
pinned_at = "2026-07-06T16:51:09+00:00"
pinned_sha256_16 = "2d111a3b8322b7d1"
pinned_at = "2026-06-30T22:25:56+00:00"
tolerate_drift = true # prose reference; OpenAPI+SSE are the gates
# Worldtree persona render canons (d2) — the deterministic affect->NL the agent is
@@ -143,18 +143,3 @@ canonical_path = "core/persona/canon/d2-render-canon-v1.json"
consumer_path = "docs/vendor/worldtree-persona-canon/d2-render-canon-v1.json"
pinned_sha256_16 = "606bba5fdcc60b6b"
pinned_at = "2026-07-01T21:00:00+00:00"
# Worldtree affect-egress consumer reference — the authoritative DELIVERED-on-wire vs
# HIDDEN (system-prompt-only) classification for the Tier-3 affect surface ratatoskr
# consumes, + the reconstruction rules. The web console's "context injection" panel
# reconstructs the hidden strings from this + the d2 canons. tolerate_drift: prose
# reference (the render-canon JSONs are the strict gates). worldtree-dev co-signs +
# pings ratatoskr-dev on any change (esp. the pending we-framing conditional).
[[pins]]
id = "worldtree-affect-egress-consumer-reference-v1"
canonical_source = "Worldtree"
canonical_path = "docs/affect-egress-consumer-reference.md"
consumer_path = "docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md"
pinned_sha256_16 = "d959134037efae83"
pinned_at = "2026-07-07T06:09:24+00:00"
tolerate_drift = true # prose reference; the d2 render-canon JSONs are the gates
-4
View File
@@ -121,7 +121,3 @@ graphify-out/*
*.db
*.db-shm
*.db-wal
# Node deps (Playwright for web-UI DOM verification — see persistent-memory)
node_modules/
package-lock.json
+7 -9
View File
@@ -7,18 +7,16 @@ documents the pin, the vendored artifacts, and the bump procedure.
| Field | Value |
|---|---|
| Worldtree git SHA | `c9e59ec` |
| Worldtree HEAD message | `docs: document Tier-3 persona/memory schemas + persona_state SET body (OpenAPI 2.3.0)` |
| Pinned on | 2026-07-06 |
| Pinned by | ratatoskr-dev (re-vendor prose markdown — Tier-3 persona/memory/persona_state consumer shapes) |
| Worldtree version at pin | `v1.0.0b22` |
| Worldtree git SHA | `5810a26b38a5ea6630892f9a39756f57c5b7b41e` |
| Worldtree HEAD message | `memory: snapshot — v1.0.0b2 shipped complete (demo + personal green); consumer loop closed` |
| Pinned on | 2026-06-30 |
| Pinned by | ratatoskr-dev (v1 coverage-audit — re-pin to the FROZEN OpenAPI 2.2.0 + SSE schema) |
| Worldtree version at pin | `v1.0.0b2` |
## Pin history
| Date | SHA | Version | Notable deltas consumed |
|---|---|---|---|
| 2026-07-06 | `c9e59ec` | v1.0.0b22 | **Re-vendor the prose markdown — Tier-3 consumer shapes documented.** `c9e59ec` (docs-only, OpenAPI byte-unchanged vs `879cefe`) adds `docs/conversation-api-spec.md` § "Tier 3 — Consumer-defined agents": the persona / memory / persona_state SET-body shapes that serialize as freeform `Any` in the OpenAPI (so prose is their source of truth). Drove a consumer fix: `--set-persona-pad` now sends the canonical `{pad:{pleasure,arousal,dominance}}` named dict (was `{pad:[list]}`) — #317, `v0.19.7`. Foot-guns encoded: persona.ocean single-letter `{O,C,E,A,N}` on `/agents/define` (spelled-out → 422, the #348 mismatch) vs spelled-out on `POST /characters`; memory `{embedder_version, tier3_dreaming}`, stm_* deprecated, allows_world_scope removed→422; only `valence` still 422s. `pin:`-only for the markdown; the `v0.19.7` bump rode the persona_state code fix. |
| 2026-07-06 | `879cefe` | v1.0.0b22 | **Re-vendor OpenAPI 2.2.0→2.3.0 — Worldtree shipped #347 authored-history-write.** One new REST path-group: `POST /sessions/{session_id}/history` (the authored-history-write primitive) + the `AuthoredTurnResponse` schema (openapi path count 40→41). #347 is **OpenAPI-only** — the prose `conversation-api-spec.md` + server `conversation_api.contract.md` are byte-unchanged since the 5810a26 pin (empty `git log` delta), so those `tolerate_drift` pins stay clean; the SSE schema is unchanged (#347 is event-silent by design). **Consumer side NOT yet built**`POST /sessions/{id}/history` is a fresh in-scope ⬜ gap in `docs/coverage-map.md` (re-opens the v1 coverage-audit with exactly one gap; Heimdall-gated hide-existence → consumer treats 404 as feature-absent). `pin:`-only, no version bump. |
| 2026-06-30 | `5810a26` | v1.0.0b2 | **Re-pin to Worldtree's FROZEN v1 surface (#326), as part of the v1 coverage-audit.** Vendored the machine-readable artifacts — `conversation-api-openapi.json` (OpenAPI **2.2.0**, 40 path-groups) + `conversation-api-sse-events.schema.json` (11 events) — now the **authoritative drift gates** (pinned in `.corviduo-canonicals.toml`, CI-checked by `canonical_drift.py`). The prose `conversation-api-spec.md` is **byte-identical** to the v0.35.16 pin (last WT markdown edit 2026-05-31), kept as the human reference (`tolerate_drift`). b2 deltas already consumed in code: 409/503 eager turn-launch statuses (#331, v0.18.3/.4) + the unified error envelope (#328). 7 endpoints documented only in the OpenAPI, not the prose, all classified in `docs/coverage-map.md`: `admin/keys/bulk`, `admin/persona/{archive,erase}`, `admin/usage`, `embed`, `judgments`, `me/usage`. No client-breaking change — `pin:`-only, no version bump. |
| 2026-06-17 | `f1b59f8` | v0.35.16 | **#297 + #298/#299 — Worldtree adopts the bifrost v0.6 scope wire (emits `scope_any`/`scope_all`) + client-side per-scope-value union recall. With our v0.17.6 provider this closes cold cross-session recall end-to-end.** Catch-up bump (v0.29.0→v0.35.16). Intervening client-facing deltas reviewed, none break our consumer: #211 agent rename (`saga``echo`, `actor``mask` — slugs only); #245 `end_user_id` persistence + memory-scope resolver; #187/#188/#219 Tier-3 define/PATCH policy (additive); `bifrost` binding field + `ephemeral_does_not_accept_bifrost` 422 now documented (the #17 surface). Error codes stable; no ratatoskr code change required. |
| 2026-05-25 | `da93ca7` | v0.28.0 | #204 — new SSE event `affect_update` (current/scheduled), new endpoint `GET /agents/{id}/persona_state`, auth-model doc edits |
@@ -28,12 +26,12 @@ documents the pin, the vendored artifacts, and the bump procedure.
**Authoritative (FROZEN, machine-readable — the drift gates):**
- `docs/conversation-api-openapi.json` — copy of `Worldtree/docs/conversation-api-openapi.json` (OpenAPI `info.version` **2.3.0**). The frozen v1 REST wire (41 path-groups; 2.3.0 added `POST /sessions/{session_id}/history` per #347). Pinned `worldtree-conversation-api-openapi-v2` in `.corviduo-canonicals.toml`; drift gated by `canonical_drift.py`.
- `docs/conversation-api-openapi.json` — copy of `Worldtree/docs/conversation-api-openapi.json` (OpenAPI `info.version` **2.2.0**). The frozen v1 REST wire (40 path-groups). Pinned `worldtree-conversation-api-openapi-v2` in `.corviduo-canonicals.toml`; drift gated by `canonical_drift.py`.
- `docs/conversation-api-sse-events.schema.json` — copy of `Worldtree/docs/conversation-api-sse-events.schema.json`. The frozen SSE event schema (11 discriminated event types). Pinned `worldtree-conversation-api-sse-events-v1`.
**Reference (prose; allowed to lag — `tolerate_drift`):**
- `docs/conversation-api-spec.md` — copy of `Worldtree/docs/conversation-api-spec.md` at the pinned SHA. The **client-facing prose narrative**. Re-vendored at `c9e59ec` (2026-07-06) to carry the § "Tier 3 — Consumer-defined agents" subsections (persona/memory/persona_state SET body) that serialize as freeform `Any` in the OpenAPI JSON — so the **prose is the source of truth for those consumer shapes** (e.g. persona.ocean single-letter `{O,C,E,A,N}` on `/agents/define`; `POST /sessions/{id}/persona_state` body `{pad:{pleasure,arousal,dominance}}`). Elsewhere the OpenAPI/SSE JSON above remain authoritative. Pinned `worldtree-conversation-api-spec-v1` (tolerate_drift).
- `docs/conversation-api-spec.md` — copy of `Worldtree/docs/conversation-api-spec.md` at the pinned SHA. The **client-facing prose narrative**. Byte-frozen at v0.35.16-era content (last WT edit 2026-05-31); the OpenAPI/SSE JSON above are the source of truth where they diverge. Pinned `worldtree-conversation-api-spec-v1` (tolerate_drift).
- `docs/conversation_api.contract.md` — copy of `Worldtree/docs/contracts/conversation_api.contract.md` at the pinned SHA (byte-identical at b2 — server contract unchanged since the v0.35.16 pin). The **server-side contract** including INV-001..INV-052 and amendments. Useful for understanding load-bearing server invariants (e.g., INV-014 turn-id-public, INV-046 admin-events-envelope-stable, INV-049 admin-events-pii-discipline) when designing client behavior against them. Not in the canonical manifest (reference-only).
Both files are vendored — they reflect Worldtree at the pinned SHA, not
-142
View File
@@ -1,142 +0,0 @@
---
contract_version: "2.1"
module: "ratatoskr.first_message"
purpose: "Per-agent authored first-message presets — seed an agent's opening as a #347 authored turn-0 onto new sessions (CLI + web), the durable replacement for a system-prompt startup instruction."
touches:
- src/ratatoskr/first_message.py
- tests/test_first_message.py
scope: >
Per-agent authored first-message presets (Worldtree #347 consumer feature).
When a new session is created for an agent that has a preset opening, seed it
as a #347 authored first-message (POST /sessions/{id}/history, author=assistant,
seq-0) so the session opens in-character before the user speaks — the durable
replacement for a system-prompt "startup" instruction. Two entry points:
`preset_for` (lookup) and `seed_preset_first_message` (best-effort seed).
Consumed by ratatoskr.cli (the `--new` session path) and ratatoskr.web.server
(the POST /api/sessions endpoint). Depends on ratatoskr.sessions
(write_authored_history + its exceptions); no core.* / worldtree.* imports.
depends_on:
- "httpx"
- "ratatoskr.sessions"
used_by:
- "ratatoskr.cli"
- "ratatoskr.web.server"
language: "python"
complexity: "low"
estimated_loc: 60
confidence: 0.9
assumptions:
- "write_authored_history (contract #2 amendment 2026-07-06) is the seed primitive: 200/201 → ack dict, 404 → AuthoredHistoryUnavailable (hide-existence), other non-2xx → SessionApiFailed."
- "The preset registry is a static in-module dict keyed by agent_id; editing it is how an operator tunes an agent's opening. Seeded with ratatoskr:sindra only."
- "Auto-seed is BEST-EFFORT and MUST NOT block session creation: an instance without the session.history.write grant returns the hide-404, which is swallowed (session opens with no seeded greeting)."
---
# First-message presets — authored openings on session-create (#347)
## Context
`ratatoskr.first_message` holds per-agent authored-opening presets and seeds them
onto new sessions via the #347 authored-history-write primitive. It is the
durable form of "give an agent a first message": instead of a system-prompt
`Startup:` instruction (a workaround for the pre-#347 world where the assistant
could not author turn-0), the opening lives as a real seeded assistant turn-0.
Consumed at both session-create sites — `ratatoskr.cli._amain` (the `--new` path)
and `ratatoskr.web.server._create_session_endpoint` (POST /api/sessions) — so
every new session for a preset agent opens in-character regardless of surface.
## Data flow
**In:** a live `httpx.AsyncClient` (caller-owned, base_url + bearer set), a fresh
`session_id`, and the bound `agent_id`.
**Out:** on a preset agent, one `POST /sessions/{session_id}/history` (author=assistant,
the preset text, per-content idempotency key). Returns the seeded content on
success, else `None`.
**Side effects:** at most one outbound authored-history write; never raises to the
caller (best-effort).
## Invariants
- **INV-001 [hard]**: `seed_preset_first_message` NEVER raises (the sole exception is
`asyncio.CancelledError`, which propagates — cancellation is not a seed failure) and
NEVER blocks session creation. It soft-guards its inputs (a bad arg returns `None`,
not `AssertionError`), bounds the write with `asyncio.wait_for(_SEED_TIMEOUT_S)` so a
stalled `/history` can't hang the create path, and swallows EVERY other exception (the
hide-404, `SessionApiFailed`, `httpx.HTTPError`, `TimeoutError`, and any unexpected
error) → `None`. The `broad-except` is deliberate: this helper is wired INTO three
session-create paths, so any escape would abort a create that already succeeded.
- **INV-002 [hard]**: a no-preset agent issues ZERO HTTP (early return before any
request).
- **INV-003 [hard]**: the seed body is the preset text verbatim, author="assistant",
with a per-content idempotency key (`"ratatoskr-preset-" + sha256(text)[:12]`), so
a repeat seed of the same session+preset is an idempotent 200 replay, never a
duplicate turn.
- **INV-004 [hard]**: no `core.*` / `worldtree.*` imports (reference-consumer
boundary; verified by `tests/test_no_worldtree_imports.py`, which rglobs every
`.py` under `src/ratatoskr/` — this module included, so no per-module import
test is needed here).
## Out of scope
- **Multi-turn / scripted openers.** v1 seeds exactly one assistant turn-0. A
multi-message opening scene is a future concern.
- **Runtime/remote preset config.** The registry is an in-module dict; no file/DB/env
loading. Add that only when a second consumer needs operator-editable presets.
- **Non-assistant authors.** v1 is author=assistant only (matches #347 v1); a
user/system opener is deferred with the #347 engine surface.
- **TUI-only surfaces.** Both real session-create paths (CLI + web) are wired; the
bare-TUI picker resumes existing sessions (no create), so it needs no seed.
---
```contract
FN preset_for(agent_id: str) -> str | None
BRIEF: Return the authored first-message preset for agent_id, or None when the agent has no preset. Pure dict lookup over FIRST_MESSAGE_PRESETS.
PRE: [PRE-001 hard] agent_id is a non-empty str -- assert agent_id and isinstance(agent_id, str)
POST: [POST-001 return_value] returns FIRST_MESSAGE_PRESETS.get(agent_id) (str for a preset agent, None otherwise)
STEPS:
1. [setup, prescriptive] assert PRE-001
2. [sequential, prescriptive] RETURN FIRST_MESSAGE_PRESETS.get(agent_id)
TESTS:
preset_hit [happy]: preset_for("ratatoskr:sindra") is a non-empty str
preset_miss [happy]: preset_for("mimir") is None
empty_agent_id [adversarial]: preset_for("") → AssertionError
FN seed_preset_first_message(client: httpx.AsyncClient, session_id: str, agent_id: str) -> str | None
BRIEF: Best-effort seed of an agent's preset opening as a #347 authored first-message on session_id. If agent_id has a preset, POST it via write_authored_history (author=assistant, per-content idempotency key, the await bounded by asyncio.wait_for(_SEED_TIMEOUT_S)) and return the seeded content; on no-preset, a malformed input, OR ANY exception except asyncio.CancelledError, return None WITHOUT raising. Never raises (except CancelledError, which propagates) and never blocks session creation — it is wired into three create paths.
PRE: [PRE-001 hard] client is not None -- soft-guarded: return None (NOT assert) if violated, so a wiring bug can't crash the create path (INV-001)
PRE: [PRE-002 hard] session_id is a non-empty str -- soft-guarded: return None if violated
PRE: [PRE-003 hard] agent_id is a non-empty str -- soft-guarded: return None if violated (also guards FIRST_MESSAGE_PRESETS.get against a non-hashable/non-str id)
POST: [POST-001 return_value] preset agent + successful write → returns the preset text; no-preset, malformed input, OR any swallowed failure → None
POST: [POST-002 side_effect] a no-preset / malformed-input call issues ZERO HTTP; a preset agent issues exactly one POST /sessions/{session_id}/history with body author="assistant", content=preset, idempotency_key="ratatoskr-preset-"+sha256(preset)[:12], the await bounded by _SEED_TIMEOUT_S so a stalled response cannot block
ERROR_ROUTING:
asyncio.CancelledError:
local_handling: RE-RAISE (cancellation is not a seed failure; never swallow it — and it is a BaseException, so `except Exception` would miss it anyway)
flow_control: propagate
state_recovery: n/a
any other Exception (hide-404 AuthoredHistoryUnavailable, SessionApiFailed 409/422/etc., httpx.HTTPError, TimeoutError from wait_for, any unexpected error):
local_handling: swallow; return None
flow_control: continue (never blocks session create)
state_recovery: session opens with no seeded greeting
STEPS:
1. [setup, prescriptive] Soft-guard: IF agent_id is not a non-empty str: RETURN None (before any dict lookup — guards a non-hashable id)
2. [sequential, prescriptive] content = FIRST_MESSAGE_PRESETS.get(agent_id); IF content is None: RETURN None (INV-002 — zero HTTP)
3. [sequential, prescriptive] Soft-guard: IF client is None OR session_id is not a non-empty str: RETURN None
4. [sequential, prescriptive] key = "ratatoskr-preset-" + sha256(content utf-8)[:12]
5. [sequential, prescriptive] TRY: await asyncio.wait_for(write_authored_history(client, session_id, content=content, idempotency_key=key), timeout=_SEED_TIMEOUT_S)
tool: { destructive: false, idempotent: true, read_only: false, open_world: false }
6. [branch, prescriptive] EXCEPT asyncio.CancelledError: RAISE; EXCEPT Exception: RETURN None
7. [cleanup, prescriptive] RETURN content
TESTS:
seeds_preset [happy,tracer]: preset agent, mock 201 → returns the preset text; exactly one POST /sessions/{id}/history; body author="assistant" + content=preset + idempotency_key="ratatoskr-preset-"+sha256(preset)[:12]
no_preset_zero_http [happy]: agent "mimir" → returns None; NO HTTP issued
feature_absent_swallowed [error]: preset agent, mock 404 session_not_found → returns None, no raise
session_api_failed_swallowed [error]: preset agent, mock 409 → returns None, no raise
transport_error_swallowed [error]: preset agent, mock httpx.ConnectError → returns None, no raise
unexpected_exception_swallowed [error]: preset agent, write raises ValueError → returns None, no raise (INV-001 broad never-raise)
cancellation_propagates [error]: preset agent, write raises asyncio.CancelledError → RE-RAISED (never swallowed)
malformed_agent_id_no_http [adversarial]: agent_id=123 (non-str) OR "" → None; NO HTTP; no raise
empty_session_id [adversarial]: session_id="" (preset agent) → None (soft guard); NO HTTP; no raise
```
+2 -100
View File
@@ -322,9 +322,7 @@ via a `--characters` one-shot lifecycle probe; persona-state write surfaced via
wrappers: parsed dict verbatim (or None on 204), any off-status → SessionApiFailed.
**Note:** `set_persona_state`'s request body is FREEFORM — the frozen OpenAPI 2.2.0
declares no request schema and the prose spec documents only the GET counterpart,
so the caller supplies the snapshot shape. **Canonical (worldtree-dev prose #317,
`c9e59ec`): `{pad:{pleasure,arousal,dominance}}` — a named-key dict, NOT a list;
`--set-persona-pad` builds + sends the named dict (each float in [-1,1]).**
so the caller supplies the snapshot shape (`--set-persona-pad` sends `{pad:[…]}`).
```contract
FN list_character_models(client) -> dict[str, Any]
@@ -371,102 +369,6 @@ POST: [POST-001 return_value] on 204 returns None; [POST-002 side_effect] outbou
STEPS:
1. [sequential, prescriptive] resp = await client.post(f"/sessions/{session_id}/persona_state", json=snapshot); IF 204 RETURN None; ELSE RAISE SessionApiFailed
TESTS:
happy [happy]: 204 → None; body == {"pad":{"pleasure","arousal","dominance"}} verbatim (canonical named-key dict, #317)
happy [happy]: 204 → None; body == {"pad":[...]} verbatim
non_204 [error]: 422 → SessionApiFailed(422)
```
## Amendment 2026-07-06 — authored-history write (#347, v1 coverage-audit re-open)
Worldtree shipped #347 (authored-history-write) as OpenAPI 2.3.0: a new
`POST /sessions/{session_id}/history` primitive that writes ONE model-visible
turn into a session's ledger AS the bound agent, WITHOUT a generation and
WITHOUT lived-turn side effects (the SillyTavern "first message"). The re-vendor
(2.2.0→2.3.0, pin `879cefe`) re-opened the v1 coverage-audit with this one new
in-scope REST path-group; this amendment closes it on the consumer side and also
un-defers `GET /sessions/{id}/messages` (previously §Out of scope) as the seed's
read-back.
**Hide-existence (server INV-347-1) — the load-bearing consumer contract.** The
`session.history.write` grant is checked FIRST — an ungranted caller (or a
non-owner, or an unknown session) gets a 404 **byte-identical** to a genuine
`session_not_found`, never a 403/409/422 that would reveal the feature exists.
The consumer MUST honor this: treat 404 as **feature-absent**, fall back (a
production consumer to a model-generated greeting), and NEVER capability-probe to
tell feature-absent from ungranted from session-absent. The wrapper encodes it by
raising a DISTINCT `AuthoredHistoryUnavailable` on 404 (NOT `SessionApiFailed`),
so a caller branches feature-absent without inspecting a status code.
**Request body — v1-minimal, wire-pinned by the server.** The frozen OpenAPI 2.3.0
exports an empty request schema, but the server pins `AuthoredWriteRequest`
(`extra="forbid"`): `{author, content, idempotency_key, effects?,
claimed_original_at?}`. v1: `author="assistant"` (only value), `content` (UTF-8,
server-bounded at `authored_content_max_bytes`=8192), `idempotency_key` (REQUIRED,
per-session dedup), `effects` omitted (== "none"; only value). Because
`extra="forbid"`, the wrapper omits `effects`/`claimed_original_at` when None
(never sends null). Success is 201 (fresh) OR 200 (idempotent replay,
byte-identical body); both return the `AuthoredTurnResponse` `{author,
content_chars, injected_at, phase, seq, session_id, turn_id}` verbatim (provenance
is audit-only, NEVER on this body — INV-347-7).
**Assistant-first provider constraint (deferred, inert for the probe).** A
create-time first-message makes the assistant seq-0 (assistant-first history);
Anthropic-family providers 400 the *next generation*, vLLM/openai_compat tolerate
it. The `--seed-first-message` probe seeds but does NOT generate, so the
constraint is inert for the probe — a real consumer that then generates must bind
an assistant-first-tolerant provider.
```contract
FN write_authored_history(client: httpx.AsyncClient, session_id: str, *, content: str, idempotency_key: str, author: str = "assistant", effects: str | None = None, claimed_original_at: str | None = None) -> dict[str, Any]
BRIEF: POST /sessions/{session_id}/history — the #347 authored-history-write primitive (write one model-visible turn as the bound agent, no generation, no side effects). Body {author, content, idempotency_key} + "effects"/"claimed_original_at" only when non-None (server AuthoredWriteRequest is extra="forbid"). Success 200 (replay) or 201 (fresh) → AuthoredTurnResponse dict verbatim. 404 → AuthoredHistoryUnavailable (hide-existence: feature-absent/ungranted/session-absent, indistinguishable by design — consumer falls back, never probes). Any other non-2xx → SessionApiFailed.
PRE: [PRE-001 hard] client is not None -- assert client is not None
PRE: [PRE-002 hard] session_id is a non-empty str -- assert session_id and isinstance(session_id, str)
PRE: [PRE-003 hard] content is a non-empty str -- assert content and isinstance(content, str)
PRE: [PRE-004 hard] idempotency_key is a non-empty str -- assert idempotency_key and isinstance(idempotency_key, str)
PRE: [PRE-005 hard] author is a non-empty str -- assert author and isinstance(author, str)
POST: [POST-001 side_effect] exactly one POST to /sessions/{session_id}/history; body == {"author": author, "content": content, "idempotency_key": idempotency_key} plus "effects" iff effects is not None plus "claimed_original_at" iff claimed_original_at is not None (no null-valued keys — extra="forbid")
POST: [POST-002 return_value] on 200 or 201 returns resp.json() unmodified
ERROR_ROUTING:
HTTP 404 (hide-existence session_not_found):
local_handling: raise AuthoredHistoryUnavailable(session_id=session_id)
flow_control: abort
state_recovery: caller treats as feature-absent; fall back to a model-generated greeting; NEVER capability-probe (INV-347-1)
HTTP other non-2xx (incl. 409 generation_active, 422 content_too_long/validation_failed, 401 auth_revoked, 410 session_retired):
local_handling: raise SessionApiFailed(status=resp.status_code, body=resp.content)
flow_control: abort
state_recovery: none (409 retryable; 422 caller bug/oversize)
STEPS:
1. [setup, flexibility=prescriptive] assert PRE-001..PRE-005
2. [sequential, flexibility=prescriptive] body = {"author": author, "content": content, "idempotency_key": idempotency_key}; IF effects is not None: body["effects"] = effects; IF claimed_original_at is not None: body["claimed_original_at"] = claimed_original_at
3. [sequential, flexibility=prescriptive] resp = await client.post(f"/sessions/{session_id}/history", json=body)
tool: { destructive: false, idempotent: true, read_only: false, open_world: false }
4. [branch, flexibility=prescriptive] IF resp.status_code in (200, 201): RETURN resp.json(); ELIF resp.status_code == 404: RAISE AuthoredHistoryUnavailable(session_id=session_id); ELSE RAISE SessionApiFailed(status=resp.status_code, body=resp.content)
TESTS:
happy_fresh_201 [happy,tracer]: 201 {author:"assistant", seq:0, phase:"seeded", turn_id, content_chars, session_id, injected_at} → dict verbatim; outbound body == {"author":"assistant","content":<c>,"idempotency_key":<k>} exactly (no effects/claimed_original_at keys)
happy_replay_200 [happy]: 200 (same-key replay, byte-identical body) → dict verbatim
body_includes_effects [trace]: effects="none" → outbound body has "effects":"none"; claimed_original_at="2020-01-01T00:00:00Z" → body has that key too
hide_existence_404 [error]: 404 {error_code:"session_not_found"} → raises AuthoredHistoryUnavailable(session_id=<arg>), NOT SessionApiFailed
generation_active_409 [error]: 409 {error_code:"generation_active"} → SessionApiFailed(status=409)
content_too_long_422 [error]: 422 {error_code:"content_too_long"} → SessionApiFailed(status=422)
empty_content [adversarial]: content="" → AssertionError; no HTTP issued
empty_idempotency_key [adversarial]: idempotency_key="" → AssertionError; no HTTP issued
empty_session_id [adversarial]: session_id="" → AssertionError; no HTTP issued
FN get_session_messages(client: httpx.AsyncClient, session_id: str) -> dict[str, Any]
BRIEF: GET /sessions/{session_id}/messages — the session's message history (spec §GET /sessions/{id}/messages), un-deferred as the #347 probe's read-back so a seeded turn can be confirmed to render as a normal role=assistant message (model-invisible provenance — a seed is indistinguishable from a lived turn on read). Returns {session_id, items:[{seq, role, content, ...}], next_cursor} verbatim. Owner-scoped; any non-200 → SessionApiFailed. v1 reads the server default page (no pagination params — the probe reads a fresh 1-message session; add limit/cursor when a caller needs scrollback).
PRE: [PRE-001 hard] client is not None -- assert client is not None
PRE: [PRE-002 hard] session_id is a non-empty str -- assert session_id and isinstance(session_id, str)
POST: [POST-001 return_value] on 200 returns resp.json() unmodified
ERROR_ROUTING:
HTTP non-200 (incl. 404 session_not_found cross-owner/unknown):
local_handling: raise SessionApiFailed(status=resp.status_code, body=resp.content)
flow_control: abort
state_recovery: none
STEPS:
1. [setup, flexibility=prescriptive] assert PRE-001, PRE-002
2. [sequential, flexibility=prescriptive] resp = await client.get(f"/sessions/{session_id}/messages")
3. [branch, flexibility=prescriptive] IF resp.status_code == 200: RETURN resp.json(); ELSE RAISE SessionApiFailed
TESTS:
happy [happy]: 200 {session_id, items:[{seq:0, role:"assistant", content:"…"}], next_cursor:null} → dict verbatim
not_found_404 [error]: 404 → SessionApiFailed(status=404)
empty_session_id [adversarial]: "" → AssertionError; no HTTP issued
```
+21 -41
View File
@@ -3,7 +3,7 @@ contract_version: "2.1"
module: "ratatoskr.web"
purpose: "v0.19.2 web debug-surface parity: 3 admin/debug panes (Tools inventory, BifrostState, AdminEvents SSE) proxied server-side with the admin key server-held, plus a reliable PAD-refresh poll and a non-engine reasoning indicator in the transcript."
target_module: "ratatoskr.web (server.py routes + entrypoint.py + static/index.html)"
scope: "v0.19.2 web debug-surface parity — bring the browser surface (now the PRIMARY debug surface) to TUI parity. THREE new admin/debug panes proxied server-side + TWO transcript affordances. (1) Tools inventory: GET /api/sessions/{id}/tools proxies owner-scoped get_session_tools into the tools pane (what the LLM HAS at turn-fire), above the live tool events. (2) BifrostState pane: GET /api/sessions/{id}/bifrost proxies admin-scoped get_session_bifrost; the admin key is SERVER-HELD (app.state.admin_key from RATATOSKR_ADMIN_API_KEY), never sent to the browser. (3) AdminEvents pane: GET /api/admin/events is an SSE proxy of stream_admin_events, session-filtered SERVER-side (heartbeats + other-session events dropped), re-emitted under a fixed 'admin_event' name so every dotted type renders with one browser listener. (4) PAD refresh: the persona/affect pane polls a bounded window instead of a single 2s shot that raced the post-turn-async affect.emit. (5) Reasoning indicator: an ephemeral, clearly-non-engine transcript line on `thinking` deltas, cleared when text begins. Direct in-session TDD (the #17/#18 pattern); this contract is authored post-implementation to anchor the heid code review (the client wrappers get_session_tools/get_session_bifrost/stream_admin_events are already contracted in the sessions/sse_client specs — this contract governs the WEB proxy + presenter surface only. v0.20.0 REDESIGN (Claude Design 'Ratatoskr Console' import): the tabbed telemetry column is replaced by a 3-column command-console — a left engine-ticker rail (the DEBUG + ADMIN + tool/turn-lifecycle feeds MERGED into one timeline via tickerAdd, plus a tools-armed chip list + a full-detail Bifrost rail pane) · a center conversation (per-turn INLINE chain-of-thought, replacing the separate Think pane) · a right resizable affect console (dominant/canonical-mood centerpiece + bipolar PAD faders each carrying a turn-to-turn Δ+sparkline + a P×A mood orbit + relations metric rows + canonical directive). ALL SERVER ROUTES UNCHANGED. Single-file/no-CDN/vanilla preserved; adds a light/dark theme toggle (dark default) + an inlined data-URI favicon. Presenter FN renames tracked below (renderBifrostState→renderBifrost; renderAffectPane→renderConsole; setPersonaStrip removed; tickerAdd/setFader/setFaderTrend/renderOrbit/renderDominant/renderDerived/renderRelations/renderDirective added). INV-001/INV-004 held.)."
scope: "v0.19.2 web debug-surface parity — bring the browser surface (now the PRIMARY debug surface) to TUI parity. THREE new admin/debug panes proxied server-side + TWO transcript affordances. (1) Tools inventory: GET /api/sessions/{id}/tools proxies owner-scoped get_session_tools into the tools pane (what the LLM HAS at turn-fire), above the live tool events. (2) BifrostState pane: GET /api/sessions/{id}/bifrost proxies admin-scoped get_session_bifrost; the admin key is SERVER-HELD (app.state.admin_key from RATATOSKR_ADMIN_API_KEY), never sent to the browser. (3) AdminEvents pane: GET /api/admin/events is an SSE proxy of stream_admin_events, session-filtered SERVER-side (heartbeats + other-session events dropped), re-emitted under a fixed 'admin_event' name so every dotted type renders with one browser listener. (4) PAD refresh: the persona/affect pane polls a bounded window instead of a single 2s shot that raced the post-turn-async affect.emit. (5) Reasoning indicator: an ephemeral, clearly-non-engine transcript line on `thinking` deltas, cleared when text begins. Direct in-session TDD (the #17/#18 pattern); this contract is authored post-implementation to anchor the heid code review (the client wrappers get_session_tools/get_session_bifrost/stream_admin_events are already contracted in the sessions/sse_client specs — this contract governs the WEB proxy + presenter surface only)."
depends_on:
- "httpx"
- "starlette"
@@ -35,17 +35,6 @@ functions:
steps: "Open client_factory() client; await get_session_tools(client, session_id); return 200. Except SessionApiFailed -> status-preserving envelope."
flexibility: "prescriptive"
- name: "_session_messages_endpoint"
signature: "async _session_messages_endpoint(request: Request) -> JSONResponse"
description: "GET /api/sessions/{session_id}/messages — proxy the session's message history so the SPA renders existing turns on open (notably a #347 authored first-message seeded at create-time; without it a seeded session's transcript is blank until the user speaks)."
preconditions:
- "session_id in path_params."
postconditions:
- "POST-001: 200 with the upstream {session_id, items, next_cursor} dict verbatim on success."
- "POST-002: on SessionApiFailed(status) -> JSONResponse({error_code:'session_messages_unavailable', status}, status_code=status) — status-preserving."
steps: "Open client_factory() client; await get_session_messages(client, session_id); return 200. Except SessionApiFailed -> status-preserving envelope."
flexibility: "prescriptive"
- name: "_session_bifrost_endpoint"
signature: "async _session_bifrost_endpoint(request: Request) -> JSONResponse"
description: "GET /api/sessions/{session_id}/bifrost — proxy admin-scoped Bifrost dispatch state."
@@ -106,42 +95,33 @@ functions:
- "POST-002: a scheduled poll no-ops if a NEW turn has started (state.turnId truthy) or already settled — no refresh of a stale agent, no unbounded polling."
flexibility: "open"
- name: "loadTranscript (index.html)"
signature: "async loadTranscript(sessionId) -> void"
description: "On session open, GET /api/sessions/{id}/messages and render each EXISTING turn into #transcript — notably a #347 authored first-message seeded at create-time (which lives in the ledger, not the live turn stream, so without this the transcript is blank until the user speaks)."
- name: "web pane renderers (index.html: renderToolsInventory / renderBifrostState / openAdminEvents)"
signature: "renderToolsInventory(inv) ; renderBifrostState(b) ; openAdminEvents(sessionId)"
description: "Render the three new surfaces; all content escaped (INV-004)."
postconditions:
- "POST-001: assistant items render as a .response .md-body bubble via markdownSafe(content) (escape-first whitelist, same path as appendResponse); user items render as a .prompt-echo via textContent — no upstream content reaches innerHTML unescaped (INV-004)."
- "POST-002: any non-200, fetch error, or parse error is swallowed (best-effort) — a blank transcript is acceptable; opening the workspace is never blocked."
flexibility: "prescriptive"
- name: "web pane renderers (v0.20.0: index.html: renderToolsInventory / renderBifrost / openAdminEvents + tickerAdd)"
signature: "renderToolsInventory(inv) ; renderBifrost(b) ; openAdminEvents(sessionId) ; tickerAdd(kind, msg, dim)"
description: "Render the debug/admin surfaces into the 3-column console; all content escaped (INV-004). v0.20.0: BifrostState is now a full-detail LEFT-RAIL pane (renderBifrost, renamed from renderBifrostState); AdminEvents + the raw debug/op log + tool_start/result + turn lifecycle are MERGED into one engine-ticker timeline via tickerAdd (openAdminEvents routes admin_event → tickerAdd; the turn SSE handlers route worker_phase/tool/text_boundary/affect_update → tickerAdd); Tools inventory is a rail chip list (renderToolsInventory)."
postconditions:
- "POST-001: every dynamic value (agent_id, tool names/descriptions, endpoint, caps, consumer_id, admin event type + data, ticker msg/dim) is passed through esc() or esc(JSON.stringify(...)); no upstream string reaches innerHTML unescaped."
- "POST-002: openAdminEvents closes a prior EventSource before opening a new one (state.adminES) and, on stream_error, closes so native EventSource does NOT retry-loop; admin events render into the engine ticker via tickerAdd."
- "POST-003: renderToolsInventory renders builtin + bifrost tool NAMES as rail chips (a compact 'what does the LLM have' glance); renderBifrost renders endpoint + connected + consumer_id + capabilities_granted chips + per-tool name/description rows (the full detail, admin-gated; the admin key stays server-held). tickerAdd bounds the feed to the last 400 rows (a tail, not an archive)."
- "POST-004: tool NAME-vs-DESCRIPTION split preserved — the rail chip list shows names only; per-tool descriptions live in the Bifrost pane's tools list. The engine-ticker spine (.ticker-inner::before) lives on the content-height wrapper so it stays visible when auto-scrolled to the newest entry."
- "POST-001: every dynamic value (agent_id, tool names/descriptions, endpoint, caps, admin event type + data) is passed through esc() or esc(JSON.stringify(...)); no upstream string reaches innerHTML unescaped."
- "POST-002: openAdminEvents closes a prior EventSource before opening a new one (state.adminES); the admin data blob renders via esc(JSON.stringify(d.data))."
- "POST-003: renderToolsInventory prepends the static inventory ABOVE live tool events without clobbering them (a re-render replaces only the .tools-inventory block)."
- "POST-004: the Tools inventory renders tool NAMES only — a compact comma-joined summary ('what does the LLM have', the debug glance); per-tool DESCRIPTIONS are surfaced in the BifrostState pane's tools list, deliberately NOT duplicated here. (Heid panel Hulda/Regin precision finding — accepted: contract wording clarified, code unchanged; the earlier 'names/descriptions' phrasing in INV-004 refers to the SET of value types that MAY appear across the new panes and must be escaped, not a mandate that every pane render descriptions.)"
flexibility: "open"
- name: "renderConsole + trend (v0.20.0 — unified persona/affect console; supersedes renderAffectPane/renderPersonaPane/setPersonaStrip)"
signature: "renderConsole(snap) ; setFader(axis,v) ; setFaderTrend(axis) ; renderOrbit() ; renderDominant(snap) ; renderDerived(snap) ; renderRelations(snap) ; renderDirective(snap) ; pushAffectHistory(snap) ; sparkPointsH(vals,w,h,endX) ; stripPoints(vals) ; proj3(p,a,d) ; trendDelta(vals)"
description: "ONE render path for BOTH the Tier-1 persona_state snapshot and the Tier-3 affect snapshot (renderConsole), feeding the right affect console: dominant/canonical-mood centerpiece, bipolar PAD faders (each with a turn-to-turn Δ + sparkline), a P×A mood orbit from PAD history, an affect-derived grid, relations metric rows, and the canonical directive. Replaces the v0.19.x split of renderPersonaPane (Tier-1 pane) + renderAffectPane (Tier-3 pane) + setPersonaStrip (top-bar strip, removed — PAD now lives in the console faders)."
- name: "renderAffectPane + trend (v0.19.4 — relation_edge/1 render + sparkline)"
signature: "renderAffectPane(snap) ; pushAffectHistory(snap) ; sparkline(vals) ; trendDelta(vals)"
description: "Render the Tier-3 affect snapshot as PAD mood + the durable per-entity relational model, each value with a Δ-vs-previous + a session-lived sparkline."
postconditions:
- "POST-001: reads snap.relations (relation_edge/1: target_entity + trust_ability/benevolence/integrity + warmth as {value,confidence,evidence_count} + agency + relation_context) — the CURRENT Worldtree emit shape; falls back to the legacy flat snap.valence for an older emitter. Tier-1 fields (baseline_pad, mood_drift, dominant_emotion, emotions_active) render WHEN PRESENT, '—' when absent (Tier-3 lacks them)."
- "POST-002: SVG sparklines (v0.20.4, adapted from the design prototype; replace the unicode strings). Each PAD fader shows current value + Δ-vs-previous (▲/▼) + a VERTICAL SVG strip (`stripPoints`, 26×132, time down Y newest-at-bottom, value on X ±11, gradient-faded via stripFade{P,A,D}, dot at newest). Each relation metric shows a HORIZONTAL SVG sparkline (`sparkPointsH`, 56×13, auto-scaled to its OWN range, sparkFade gradient + end dot). renderOrbit is now a 3D ISOMETRIC P×A×D cube (`proj3`: P right-down / A left-down / D up, 2:1 iso, center 62,66, scale 26) plotting the trajectory + a pulsing now-marker + a drop line to the D=1 floor + a floor shadow ellipse (depth cue). All drawn from AFFECT_HIST (rolling, HIST_CAP=24, session-lived); sparkline coords are computed numerics (no upstream strings → INV-004 trivially held). Gradients live in one hidden `<defs>` svg in the console."
- "POST-003: pushAffectHistory dedupes by emitted_at||last_updated_at so the ~4x/turn post-turn PAD poll contributes ONE sample/turn; history is CLIENT-side only (lost on reload — durable cross-session history via a provider-side snapshot log is a deferred follow-up, NOT built here)."
- "POST-004: INV-001 honesty — no fabricated Tier-1 fields. The dominant-emotion centerpiece shows a real OCC dominant_emotion (Tier-1) OR the CANONICAL mood word from canonMood(pad) (Tier-3, dimmed) OR '—'; NEVER a synthesized emotion. The affect-derived grid drops non-emitted metrics (intensity/decay-τ) and shows only real/client-derived cells (baseline/drift real for Tier-1, client-derived samples/volatility). INV-004 — every dynamic value passes through esc(); numerics go through toFixed, never innerHTML-raw."
- "POST-001: reads snap.relations (relation_edge/1: target_entity + trust_ability/benevolence/integrity + warmth as {value,confidence,evidence_count} + agency + relation_context + obligation_balance) — the CURRENT Worldtree emit shape; falls back to the legacy flat snap.valence for an older emitter. SUPERSEDES the #18-D2 contract's valence assumption (Worldtree's #265 Vili rework replaced valence/regard with the relation_edge/trust model; the old renderer read snap.valence and showed an empty 'valence (0)' — the bug this fixes)."
- "POST-002: each metric shows current value + Δ-vs-previous (▲/▼) + a unicode sparkline auto-scaled to its OWN observed range (flat ▄ when sub-0.01 stable — no noise amplification), drawn from AFFECT_HIST (rolling, HIST_CAP=24, session-lived)."
- "POST-003: pushAffectHistory dedupes by emitted_at so the ~4x/turn post-turn PAD poll contributes ONE sample/turn; history is CLIENT-side only (lost on reload — durable cross-session history via a provider-side snapshot log is a deferred follow-up, NOT built here)."
- "POST-004: INV-001 honesty — no fabricated Tier-1 fields (no synthesized dominant_emotion). INV-004 — head() escapes its whole argument (incl. target_entity + relation_context from the snapshot) and metric() escapes every cell; numeric values go through toFixed, never innerHTML-raw."
flexibility: "open"
- name: "canonical affect-NL + context-injection reconstruction (v0.19.5 canons; v0.20.2 full context-injection panel)"
signature: "canonMood(pad) ; canonDirective(rel) ; canonPadFallback(pad) ; canonEmotionDirective(type) ; renderDirective(snap) ; loadPersonaCanon()"
description: "Reconstruct + render the HIDDEN affect-context block Worldtree assembles into the agent's system prompt (never on any wire) — byte-exact to Worldtree's own describe_pad + render_d2_canonical + derive_directive + _pad_band_fallback. The v0.20.2 'context injection' panel shows the full block: mood descriptor + mood directive + relationship directive. Reference: docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md (pinned)."
- name: "canonical affect-NL (v0.19.5 — vendored Worldtree d2 render canons)"
signature: "canonMood(pad) ; canonDirective(rel) ; loadPersonaCanon()"
description: "Render the LITERAL mood word + relationship directive Worldtree context-injects into the agent, byte-exact to Worldtree's own describe_pad + render_d2_canonical."
postconditions:
- "POST-001: DETERMINISTIC, no LLM. canonMood mirrors describe_pad (valence×arousal grid + strict ±0.3 bands + dominance clause); canonDirective mirrors render_d2_canonical; canonPadFallback mirrors renderer._pad_band_fallback BYTE-EXACT (P×A quadrant: hi/lo/mid arousal band × p>0.3/<-0.3/neutral, with the negative_low_dominance (d<-0.3) special case + neutral_high_a + default); canonEmotionDirective is the occ_directives[type].directive lookup (+ tier / full_only flag)."
- "POST-002: the canon DATA is VENDORED (docs/vendor/worldtree-persona-canon/{d2-mood-render-canon-v1,d2-render-canon-v1}.json), pinned drift-gated in .corviduo-canonicals.toml; the flat browser form (static/persona_render_canon.json, served /static) is regenerated by scripts/build_persona_canon.py via Worldtree's OWN authoritative loader — v0.20.2 extended it to emit mood_directive {occ_directives, pad_band_fallback, salience, pad_band_cutoff, full_only}. The affect-egress consumer reference is pinned tolerate_drift (worldtree-affect-egress-consumer-reference-v1; worldtree-dev co-signs + pings on change)."
- "POST-003: fail-open — canon absent (fetch fails) → the reconstructed lines OMIT, the structured console still renders. Every canon-derived string is esc()'d before the DOM (INV-004)."
- "POST-004: HONEST-PARTIAL provenance (affect-egress-reference §3). The mood descriptor + relationship directive are EXACT (tagged 'exact'); the mood DIRECTIVE is a CANDIDATE pair (tagged 'candidate') — the OCC emotion directive for the delivered dominant_emotion type AND the PAD-band fallback — because affect.emit is type-only (no intensity) so the salience gate (≥0.2) can't be evaluated; BOTH are shown with the 'injected if intensity ≥ salience' caveat, never asserting which fires. When dominant_emotion is absent the fallback alone is EXACT. The panel is labeled reconstructed + hidden-from-consumers + dev-only (the reference-impl's sanctioned understand/reconstruct use, NOT end-user display per the reference's caveat). WATCH: a pending Worldtree render_d2_canonical change conditionally drops the trailing 'avoid premature we-framing' clause under a 3-gate combo — canonDirective holds as-is until worldtree-dev pings with the exact conditional + a canon bump."
- "POST-001: DETERMINISTIC, no LLM. canonMood mirrors describe_pad (valence×arousal grid + strict ±0.3 bands + dominance clause); canonDirective mirrors render_d2_canonical (interval band-cut lookup + per-band phrase assembly + cross-axis low-trust-precedence behavior clause). BOTH VERIFIED BYTE-EXACT against Worldtree's own renderer run on the live snapshot (the reference harness re-runs Worldtree's functions + asserts string equality — reproducible)."
- "POST-002: the canon DATA is VENDORED (docs/vendor/worldtree-persona-canon/{d2-mood-render-canon-v1,d2-render-canon-v1}.json), pinned drift-gated in .corviduo-canonicals.toml (worldtree-persona-{mood,d2}-render-canon-v1); the flat browser form (static/persona_render_canon.json, served /static) is regenerated by scripts/build_persona_canon.py via Worldtree's OWN authoritative loader. Reference-impl posture: ADOPT the dep's canonical render, do NOT invent vocab — an invented 'faintly excited' would MISLEAD where the canonical (±0.3 bands) says 'neutral'."
- "POST-003: fail-open — canon absent (fetch fails) → the canonical lines OMIT, the structured pane still renders. The canon-derived strings are esc()'d before the DOM for INV-004 consistency."
flexibility: "open"
invariants:
+1 -181
View File
@@ -1,50 +1,6 @@
{
"components": {
"schemas": {
"AuthoredTurnResponse": {
"description": "#347 — the authored-write ack. Provenance is audit-only and NEVER on this\npayload (INV-347-7). ``content_chars`` is the CHARACTER count (may differ from\nthe UTF-8 byte length the request is bounded against — INV-347-12).",
"properties": {
"author": {
"title": "Author",
"type": "string"
},
"content_chars": {
"title": "Content Chars",
"type": "integer"
},
"injected_at": {
"title": "Injected At",
"type": "string"
},
"phase": {
"title": "Phase",
"type": "string"
},
"seq": {
"title": "Seq",
"type": "integer"
},
"session_id": {
"title": "Session Id",
"type": "string"
},
"turn_id": {
"title": "Turn Id",
"type": "integer"
}
},
"required": [
"turn_id",
"session_id",
"author",
"phase",
"seq",
"injected_at",
"content_chars"
],
"title": "AuthoredTurnResponse",
"type": "object"
},
"BifrostBindingRequest": {
"additionalProperties": false,
"description": "Bifrost binding parameters for session-create (issue #160).",
@@ -434,7 +390,6 @@
"session_not_found",
"session_not_bifrost_bound",
"session_retired",
"generation_active",
"agent_not_available",
"turn_not_found",
"turn_finished",
@@ -950,7 +905,7 @@
"info": {
"description": "Multi-turn conversation interface for Worldtree agents.",
"title": "Worldtree Conversation API",
"version": "2.3.0"
"version": "2.2.0"
},
"openapi": "3.1.0",
"paths": {
@@ -6427,141 +6382,6 @@
"summary": "Update Session"
}
},
"/sessions/{session_id}/history": {
"post": {
"description": "#347 — write one model-visible turn into a session's ledger AS the bound\nagent, WITHOUT a generation and WITHOUT lived-turn side effects.\n\nHide-existence ordering (INV-347-13): the ``session.history.write`` grant is\nchecked FIRST — before session resolution, before ANY body parse/validation,\nbefore the active-generation guard. An ungranted caller receives ONLY the\nhide-404 (byte-identical to session-not-found, INV-347-1) — never a\n422/409/403 that would distinguish feature-absent from session-absent.",
"operationId": "authored_history_write_sessions__session_id__history_post",
"parameters": [
{
"in": "path",
"name": "session_id",
"required": true,
"schema": {
"title": "Session Id",
"type": "string"
}
}
],
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthoredTurnResponse"
}
}
},
"description": "Successful Response"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"405": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"409": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"412": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
},
"503": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Error — unified envelope (detail.error_code is the stable identifier)."
}
},
"security": [
{
"HTTPBearer": []
}
],
"summary": "Authored History Write"
}
},
"/sessions/{session_id}/messages": {
"get": {
"description": "Return paginated message history for a session.",
+9 -158
View File
@@ -2699,13 +2699,10 @@ The `turn.started` event always carries `bifrost_override_applied: bool` (True/F
Tier 3 agents are consumer-owned, Worldtree-hosted agents whose
identity lives at `<user_id>:<agent_name>`. They share the persistent
session infrastructure with Tier 1 / Tier 2. The layer-specific
machinery is now largely active: **`persona` (Phase 2.1, #186),
`memory` (Phase 2.1, #197), and `motivational` (Phase 2.2, #187) are
shipped and consumer-settable at define-time.** Only **`valence` remains
deferred** (non-null → 422 `layer_deferred`). Phase 2.0 shipped the
baseline addressing + ownership + lifecycle substrate; the subsections
below document the active layers and their exact validated shapes.
session infrastructure with Tier 1 / Tier 2 but layer-specific
machinery (persona, motivational, memory, valence) is reserved for
later phases — Phase 2.0 ships baseline addressing + ownership +
lifecycle only.
### Endpoints
@@ -2723,13 +2720,11 @@ below document the active layers and their exact validated shapes.
{
"agent_name": "wizard",
"system_prompt": "You are a guided-elicitation wizard...",
"role": "gen-reasoning", // REQUIRED — a configured model-role (#344), not a raw model id
"persona": { // active (Phase 2.1) — single-letter OCEAN keys; see "Persona layer"
"ocean": {"O": 0.4, "C": 0.6, "E": -0.3, "A": 0.2, "N": 0.5}
},
"motivational": null, // active (Phase 2.2) — see "Motivational layer"
"memory": null, // active (Phase 2.1) — see "Memory layer"
"valence": null // still deferred — non-null → 422 layer_deferred
"model": "glm5-turbo",
"persona": null, // schema-reserved; non-null → 422 layer_deferred
"motivational": null,
"valence": null,
"memory": null
}
```
@@ -2761,150 +2756,6 @@ after definition.
The 201 response includes an advisory `warnings` array (#219) — see
"Model-assignment warnings" under `PATCH` below.
> **Vendoring note (OpenAPI 2.3.0).** In the frozen OpenAPI 2.3.0 document
> the `persona` / `motivational` / `memory` / `valence` request fields
> serialize as **untyped/freeform** — the `POST /agents/define` request
> model types them as `Any` so the layers can activate without a
> schema-breaking change. The shapes documented in the subsections below
> are the **authoritative, validator-enforced** schemas; generate client
> types from this section, not from the freeform OpenAPI fields.
##### Persona layer (Phase 2.1, #186)
`persona` is **active** as of Phase 2.1. It carries the agent's OCEAN
personality vector — the durable trait profile from which Worldtree
derives the mood setpoint (`baseline_pad`) and the mood dynamics
(gain + relaxation time-constants). Shape:
```json
"persona": {
"ocean": { // REQUIRED — exactly these 5 keys, no more, no fewer
"O": 0.4, // Openness — float in [-1.0, 1.0]
"C": 0.6, // Conscientiousness
"E": -0.3, // Extraversion
"A": 0.2, // Agreeableness
"N": 0.5 // Neuroticism
},
"behavioral_notes": "...", // optional, ≤ 4096 chars
"temperament_notes": "..." // optional, ≤ 4096 chars
}
```
**⚠ OCEAN key format — single-letter, uppercase.** The `/agents/define`
persona validator requires the `ocean` map to contain **exactly** the five
uppercase single-letter keys `O, C, E, A, N`. This is a deliberate,
load-bearing contrast with the transient-character primitive
(`POST /characters`), whose `ocean` block uses the **spelled-out**
lowercase keys (`openness`, `conscientiousness`, …). Sending spelled-out
keys to `/agents/define` returns 422 `persona_ocean_required` ("must
contain exactly the 5 keys O, C, E, A, N").
> **Fixed in v1.0.0b21 (#348).** Before that build a correctly
> single-letter-keyed persona was accepted and stored, but resolved to a
> **neutral** mood, because Worldtree's internal mood-derivation read the
> spelled-out key form. On v1.0.0b21+ an API-declared persona correctly
> drives the derived mood setpoint. If you observe neutral mood on a
> persona-defined agent, confirm the deployment is ≥ v1.0.0b21.
**Range.** Each value is a float in `[-1.0, 1.0]` **signed** — `0.0` is the
population mean, NOT `[0.0, 1.0]`. Booleans are rejected. Out-of-range → 422
`persona_ocean_out_of_range`. See [`docs/ocean-traits.md`](ocean-traits.md)
for the SOTA-grounded 5-band behavioural mapping.
Semantics:
- **Per-agent identity trait** — identical for every end-user and session;
immutable post-define (`PATCH {"persona": …}` → 422 `field_not_mutable`).
To change the OCEAN profile, delete and re-define the agent.
- **`extensions` is reserved** — the field exists but must be empty at v0.1;
a non-empty `extensions` returns 422 `layer_deferred`.
- **Sets the mood SETPOINT, not the current mood.** The OCEAN vector fixes
`baseline_pad` (the PAD point the mood relaxes toward over time); the
*current* per-session mood point is seeded separately via
`POST /sessions/{id}/persona_state` (below).
Validation 422 codes: `persona_ocean_required` (missing `ocean`, or keys
≠ {O,C,E,A,N}), `persona_ocean_out_of_range` (a value outside [-1.0, 1.0], or
a boolean), `persona_notes_too_large` (a note > 4096 chars), `layer_deferred`
(non-empty `extensions`), `validation_failed` (unknown top-level field).
##### `POST /sessions/{session_id}/persona_state` — seed the session mood point (Phase 2.1, #186/#189)
Session-scoped mood seed. Sets the *current* PAD mood point for one
session's bound agent — the starting emotional state, distinct from the
OCEAN-derived setpoint the mood relaxes toward. Works on any
persona-enabled session (Tier 1 or Tier 3); most useful for a Tier 3
durable-agent session that wants to start a conversation from a specific
mood.
Request:
```json
{
"pad": {
"pleasure": 0.42, // float in [-1.0, 1.0]
"arousal": 0.25,
"dominance": 0.33
}
}
```
Response: **`204 No Content`** — no body, no audit event (a session-scoped
runtime overlay, not a security-relevant event).
Semantics:
- **PAD-only** (#317 Option A). The body accepts exactly one key, `pad`,
which must carry all three of `pleasure` / `arousal` / `dominance`, each a
float in `[-1.0, 1.0]`. Any other top-level key → 422 `validation_failed`;
a missing or malformed `pad` → 422 `persona_seed_invalid`.
- **Seeds the current mood POINT, not the setpoint.** The OCEAN persona
(above) fixes the setpoint the mood relaxes toward; this endpoint sets
where the mood *starts*. It does not alter the persona.
- **Cross-owner sessions return 404** (existence-hiding — a session that
isn't yours is indistinguishable from one that doesn't exist).
- **Pull-over-push precedence (#289).** Once a session's baseline has been
rehydrated from an `affect.fetch` (the authoritative cross-session
source), a later SET seed is silently ignored — the fetched baseline wins.
There is **no** `POST /agents/{id}/persona_state` — mood is per-session, not
a durable agent property. `GET /agents/{agent_id}/persona_state`
short-circuits to 404 for Tier-3 colon-ids: Tier-3 mood is observable only
over the Bifrost `affect.emit` egress (ADR-0009), never read back through
the HTTP API.
##### Memory layer (Phase 2.1, #197)
`memory` is **active** as of Phase 2.1 but exposes a deliberately minimal
surface — the short-term-memory (STM) tier was removed (#197), so the
historically-present `stm_*` knobs are accept-and-ignore no-ops. Shape:
```json
"memory": {
"embedder_version": "<pinned>", // optional; MUST equal the library-pinned version
"tier3_dreaming": false // optional bool, default false
}
```
Semantics:
- **`embedder_version`** — optional. If supplied it MUST equal the library's
currently-pinned embedder version; a mismatch → 422
`embedder_version_mismatch` (with `expected` / `received` in the detail).
Omit it to accept the pin. Fixed at define-time and library-pinned
thereafter.
- **`tier3_dreaming`** — optional bool (default `false`); opt-in flag for the
Tier-3 dreaming / consolidation path.
- **`stm_capacity` / `stm_token_budget`** — **deprecated no-ops.** Accepted at
define (201) with a `DeprecationWarning`; they carry no runtime effect since
the STM tier was removed, and are slated for rejection at the next schema
break. Do not send them in new integrations.
- **`allows_world_scope` — removed.** Sending it → 422 `validation_failed`
("world-shared knowledge belongs in the KB/Mimir plane").
- **Wholesale-immutable post-define.** `PATCH {"memory": …}` → 422
`field_not_mutable` (even for the deprecated `stm_*` fields) — see the
PATCH table above.
##### Motivational layer (Phase 2.2, #187)
`motivational` is **active** as of Phase 2.2 (persona + memory activated in
+11 -17
View File
@@ -19,7 +19,7 @@ anchors against the frozen machine-readable artifacts, NOT the prose markdown:
| Worldtree v1 surface | Frozen anchor | Ratatoskr role |
|---|---|---|
| Conversation REST API | OpenAPI `info.version` **2.3.0** (`Worldtree/docs/conversation-api-openapi.json`, sha `36148179…`) — **41 path×method groups** (2.3.0 added `POST /sessions/{id}/history`, #347) | **client** (debug TUI / web) |
| Conversation REST API | OpenAPI `info.version` **2.2.0** (`Worldtree/docs/conversation-api-openapi.json`, sha `dbdf4e24…`) — **40 path×method groups** | **client** (debug TUI / web) |
| Conversation SSE events | `conversation-api-sse-events.schema.json` (sha `9deeebf4…`) — **11 discriminated event types** | **client** |
| Bifrost wire (consumer protocol) | wire **v0.6** STABLE/FROZEN (`bifrost==1.0.0`) — memory + affect planes | **provider** (Worldtree dispatches into us) |
@@ -48,7 +48,7 @@ resolved (§ Surface 1, scope-resolution table).
| Surface | Points | ✅ covered-live | ⬜ gap (in-scope) | 🚫 excluded-by-design |
|---|---|---|---|---|
| REST (OpenAPI 2.3.0, path groups) | 41 | 19 | 0 | 22 |
| REST (OpenAPI 2.2.0, path groups) | 40 | 17 | 0 | 23 |
| SSE events | 11 | 11 | 0 | 0 |
| Bifrost provider planes | 8 verbs | 8 | 0 | (10 gated verbs deferred) |
@@ -61,7 +61,7 @@ sub-gap).
---
## Surface 1 — Conversation REST API (OpenAPI 2.3.0)
## Surface 1 — Conversation REST API (OpenAPI 2.2.0)
### Covered — client path (ratatoskr's core identity)
@@ -69,8 +69,6 @@ sub-gap).
|---|---|---|---|
| `POST /sessions` | ✅ | `sessions.py:307``cli.py:482`,`tui.py:1508`,`web/server.py:155` | + `end_user_id`, `bifrost` binding; 404→AgentNotFound, 502→BifrostHandshakeFailed |
| `POST /sessions/{id}/messages` (turn stream, SSE) | ✅ | `sse_client.py:484` `stream_turn` → cli/tui/web | the primary surface; 409→AgentNotAvailable, 503→TurnLaunchUnavailable (b2 #331) |
| `POST /sessions/{id}/history` (authored-history-write, #347) | ✅ | `sessions.py:583` `write_authored_history``cli.py:758` `--seed-first-message` | v1: author=assistant, effects=none, per-session idempotency; 404→AuthoredHistoryUnavailable (hide-existence: feature-absent, never probe); 409/422 mapped. **LIVE-PROVEN 2026-07-06** on personal :8081 (grant applied via a rule-based Heimdall allow, worldtree-dev): create mimir session → seed → **201** (seq=0, phase=seeded, turn_id=1798) → GET /messages reads it back as a plain role=assistant turn (model-invisible provenance confirmed). Hide-404 for ungranted is unit+probe covered |
| `GET /sessions/{id}/messages` (history) | ✅ | `sessions.py:635` `get_session_messages``cli.py:758` `--seed-first-message` read-back | un-deferred as the #347 seed read-back — confirms model-invisible provenance (a seed reads back as a normal `role=assistant` turn) |
| `POST /sessions/{id}/turns/{turn_id}/cancel` | ✅ | `sse_client.py:581` → cli/tui/web | two-stage Ctrl-C; 404/409 mapped |
| `GET /agents` | ✅ | `sessions.py:341``tui.py:1472`,`web/server.py:100` | Tier-1 roster; merged with local index |
| `GET /agents/{id}/persona_state` | ✅ | `sessions.py:384``tui.py:1132`,`web/server.py:386` | persona hydrate; 404/403 mapped |
@@ -98,21 +96,16 @@ on the same path is an unwired frontier item — see frontier Tier 1):
- `GET /agents/{id}` — consumer-agent lookup (`GET /agents/<owner>:<name>` with
the owner key) is **manual-curl-only**, not in code.
### In-scope gaps — CONVERGED (re-closed 2026-07-06 after the #347 re-open)
### In-scope gaps — CONVERGED (zero remaining, 2026-07-01)
**Every in-scope REST I/O point is covered.** The audit first converged
2026-07-01; Worldtree's #347 (authored-history-write, OpenAPI 2.3.0) then added
one new in-scope path-group, re-opening the audit with a single gap — now closed
(`v0.19.6`). The original frontier (design-brief §5 observability panes +
presenter-wiring sub-gaps + Tier-2 tail) remains fully closed:
**Every in-scope REST I/O point is now covered.** The frontier that opened this
audit (the design-brief §5 observability panes + the presenter-wiring sub-gaps +
the Tier-2 tail) is fully closed:
- Session picker + SSE-resume — wired (`v0.18.5``.7`).
- Persona · Tools · BifrostState · AdminEvents panes — all built + live (`v0.18.x``v0.19.0`).
- Transient-characters CRUD + persona-state write — consumed via `--characters` /
`--set-persona-pad` (`v0.19.1`).
- Authored-history-write (#347) + messages read-back — `write_authored_history` +
`get_session_messages` via `--seed-first-message` (`v0.19.6`; live-proof pending
the `session.history.write` grant).
The only remaining not-consumed in-scope method is `GET /agents/{id}` (consumer-
agent lookup, manual-curl-only) — a sub-method on an already-✅ path group, not a
@@ -123,6 +116,7 @@ path-group gap. Everything else is covered or excluded-by-design below.
| Endpoint(s) | Status | Rationale (design-brief / memory) |
|---|---|---|
| `PATCH /sessions/{id}` · `DELETE /sessions/{id}` | 🚫 | §4: rename/delete happen outside the tool (`sessions_cli.py`) |
| `GET /sessions/{id}/messages` (history) | 🚫 | §6: single-session live transcript, no history fetch |
| `GET /sessions/{id}` | 🚫 | session detail — identity is footer-visible, no detail view |
| `GET /sessions/{id}/tool-events` | 🚫 | §5: tool calls observed **inline from SSE** `tool_start`/`tool_result`; persisted-events endpoint is opt-in only |
| `GET /admin/sessions/{id}/tools` | 🚫 | **covered-by-alternative** — the owner-scoped `GET /sessions/{id}/tools` (✅) serves the Tools inventory; this admin variant is only for cross-user operator debug, out of the single-session focus (§6) |
@@ -210,10 +204,10 @@ starts exercising them.
---
## Convergence frontier (the v1 to-do) — CLOSED 2026-07-01, re-closed 2026-07-06 (#347)
## Convergence frontier (the v1 to-do) — CLOSED 2026-07-01
**Every in-scope I/O point is covered.** The frontier is empty: REST 19/41
with **zero in-scope gaps** (the other 22 REST path-groups are excluded-by-design),
**Every in-scope I/O point is covered.** The frontier is empty: REST 17/40
with **zero in-scope gaps** (the other 23 REST path-groups are excluded-by-design),
SSE 11/11, Bifrost provider planes 8/8. v1 convergence (per scope A: "every
frozen I/O point classified, zero unaccounted") is **met** — ratatoskr cuts v1
when Worldtree tags 1.0. The arc, for the record:
-270
View File
@@ -1,270 +0,0 @@
# Design brief — `ratatoskr-web` (Worldtree wire monitor)
> **For:** a visual design pass (Claude Design). **Deliverable:** a single
> self-contained HTML prototype, fully populated with representative static
> data, that an engineer will wire live data into. **Do not** build a data
> layer — build the *shell* and *every state*, beautifully, with placeholder
> content in every slot.
---
## 1. What you're designing
`ratatoskr-web` is a **developer-grade debug/observability console** for a
conversational-AI engine (Worldtree). Its tagline is *"wire monitor"*: you open
a session with an AI agent, send it turns, and **watch that turn flow through
every layer of the system at once** — the streaming response, the model's
chain-of-thought, the tools it can call, the agent's live emotional/persona
state, the provider handshake, and the engine's admin lifecycle events — all
side-by-side on one screen.
**The product IS the observability surface. Chat is just the input.** This is
not a chat app, not a marketing page, not an end-user product. The user is one
developer (occasionally a few LAN peers) staring at a dense instrument to debug
what the engine is doing. Think **oscilloscope / flight-data console / a well-lit
`htop`**, not a messaging UI.
**Design values, in priority order:**
1. **Information density earns the screen.** Every region shows live, changing
data. Nothing is decorative filler. A quiet, legible, glanceable density is
the whole point — the user reads six data streams at a glance.
2. **Calm under motion.** Multiple regions update in real time (token streams,
live metrics, event logs). The design must stay readable while things move —
no jitter, no attention-grabbing per-item animation. Motion is for *state
change*, used sparingly.
3. **Legibility first.** Monospace, high contrast where it counts, generous but
not wasteful spacing. This runs for hours; it must not tire the eye.
---
## 2. Aesthetic direction — Australis
Use the **Australis design system** (a cool-toned, terminal-first dark theme —
the `australis-design` skill has the canonical tokens: colors, spacing, radii,
shadows, motion). Import/inline `colors_and_type.css`; don't reinvent tokens.
Non-negotiables from the brand:
- **Dark only.** Base is a cool near-black **`#222531` — never pure `#000`.**
The eye rests in low-contrast cool grey; **emphasis comes from *brightness*,
not saturation.** Layer surfaces up the Sea neutral ramp (`#222531
`#373b46``#414751`).
- **Palette families:** *Ice* (surface neutrals), *Aurora* (blue → cyan → green,
the primary accents — used generously in that preference order), *Dawn*
(red/yellow/magenta — sparingly, for status only). Semantic: info=blue,
success=green, warning=yellow, danger=red.
- **The signature motif is the aurora glow** — a low-opacity cyan→blue→green
light coming *through* the top of the screen, plus a 3px aurora focus ring on
interactive controls. Lean into this as the one memorable thing.
- **No noise, no textures, no patterns.** *"The screen is the polar sky — empty,
with light coming through it."* The one sanctioned gradient is the aurora glow.
- **Never a colored left-border on cards** (the LLM-slop trope). Featured cards
accent the *top* edge instead.
- **Type:** this instrument is **mono-first** — that IS on-brand for Australis
("terminal-first"). Use a monospace stack (JetBrains Mono / system mono; see
§9 — no web-font CDN allowed). Eyebrows/labels are **mono, UPPERCASE, ~11px,
wide-tracked (`0.080.16em`)** — use them liberally; they're a system
signature.
- **Motion:** calm, never bouncy. ~120ms hover, ~200ms state, ~320ms panels.
Focus = aurora glow ring. Hover = one step *brighter* (not lower opacity).
A slow (814s) aurora drift on a hairline top band is welcome; nothing else
should loop.
The current UI already borrows this palette — you're not inheriting it, you're
**redesigning the layout and craft from scratch** with the brand as the guide.
Feel free to rethink the spatial composition entirely (see §10).
---
## 3. The two screens
### Screen A — **Session setup** (entry)
A single centered card on the aurora canvas. Fields:
- **Agent** — a `<select>` (populated live; show 34 sample options incl.
`ratatoskr:sindra`, `forseti`, `mimir`).
- **Bifrost binding (Tier-3 provider)** — a `<select>`: `combined (:8392)` /
`none — observe only` / `memory (:8391)` / `affect (:8390)`.
- **Open session** — primary button.
- An error line (design the error state too — e.g. "agent not available").
### Screen B — **Live workspace** (the main event — 95% of the design effort)
Persistent top bar + status line spanning full width; between them a **two-region
body: a conversation column (left, dominant) and a telemetry column (right,
tabbed).** Current split is ~1.85 : 1 — you may re-proportion. The information
inventory below is exhaustive; **every item needs a home.**
---
## 4. THE COMPLETE INFORMATION INVENTORY
This is the core of the brief. Design a slot for **every** item, in a sensible
state. Data shapes are given so your placeholders read true.
### 4.1 Top bar (persistent)
| Item | Shape / example | Notes |
|---|---|---|
| Brand | `ᛯ ratatoskr` + eyebrow `WIRE MONITOR` | the mark is a rune glyph; small |
| **Connection status** | one of: `offline`, `connected` (idle), `streaming`, `error` | dot + label; **streaming pulses**; color-coded (grey/green/cyan/red) |
| **Persona strip** (appears after a session hydrates) | dominant-emotion word (`love`) + **PAD bars**: `P`, `A`, `D` | each PAD bar is **bipolar** — centered on 0, fills left (negative) or right (positive), value ∈ [1, 1]; **live-updates every turn** |
| Session identity | `ratatoskr:sindra · …381b99f4` | agent id + last-8 of session id |
| Bound-plane badge (when bound) | `⇄ combined http://10.100.10.50:8392` | plane + endpoint; only when a Bifrost binding is active |
### 4.2 Conversation column (the transcript + composer)
The transcript is a scrollable stream of turns. Design each element:
| Element | Example content | Notes |
|---|---|---|
| **Turn divider** | `TURN 3` between hairlines | uppercase eyebrow, rule lines each side |
| **User prompt echo** | ` what's your intensity setting?` | the user's message, accent-marked |
| **Assistant response** | streaming **Markdown** (headings, bold, italic, `code`, lists, quotes, links) | accumulates token-by-token while live; distinct "live" treatment vs settled |
| **Seeded first-message** | a full assistant turn present *before the user speaks* (an authored greeting) | renders **identical to a lived assistant turn** — the session can OPEN already showing the agent's opener |
| **Reasoning / "thinking" note** | `✦ sindra is reasoning···` (italic) | **ephemeral** app affordance — appears while the model reasons, vanishes the instant real text begins; visually distinct from the response so it never reads as engine output |
| **Awaiting-first-token** | `···` animated | heartbeat before the first token |
| **End-of-turn status chips** | `✓ DONE 1.84s` · `✗ ERROR agent_not_available` · `⚠ CANCELLED` · `✗ WIRE lost` | small bordered chips; color per state |
| **Composer** (pinned bottom) | ` [ message input ] [SEND]` | Enter=send, Shift+Enter=newline; during a turn the Send button becomes **CANCEL** (amber) |
### 4.3 Telemetry column (six tabbed panes)
A tab bar + a pane header (with a **Copy** button) + the active pane body.
**Tabs** (each: name · keybinding hint · a count **badge** that *flashes* on new
data): `TOOLS ^1` · `DEBUG ^2` · `THINK ^3` · `PERSONA ^4` · `BIFROST ^5` ·
`ADMIN ^6`. Active tab is accent-marked.
Pane contents — design each, populated:
1. **Tools** — the tool inventory the model saw at turn-fire:
`agent_id`, `builtin_tools[]` (names), `bifrost_tools[]` (name + description +
parameters). Below it, **live tool-call events** stream in (`tool_start`
`tool_result`) as the turn runs. Empty state: `— live tool events —`.
2. **Debug** — a raw structured op/lifecycle log (mono lines; new lines flash
once). Think `tail -f`.
3. **Think** — the model's **full chain-of-thought**, per-turn dividers,
live-Markdown. Longer prose than the response.
4. **Persona / affect***the richest pane.* Contains:
- **The canonical NL directive** the engine injects into the agent's context
— the literal text: a **mood descriptor** ("neutral", "faintly excited",
±0.3 bands) + a **relationship directive**. Show this verbatim, quoted.
- **PAD mood point** — pleasure / arousal / dominance current values.
- **relations[]** — for each related entity (e.g. the user): **trust**
(ability / benevolence / integrity), **warmth**, **agency**,
`relation_context` (a tie-type word like "stranger" / "expressive"), each as
a **metric row**: `label · value · Δ-since-last (▲/▼) · unicode sparkline ·
n (evidence count) · descriptor`. Values are 01 with 23 decimals.
- **dominant_emotion** (an OCC type: joy/anger/fear/…) + **emotions_active[]**.
- Design the **metric row** as a reusable component — it's the densest,
most-repeated element in the whole UI. Tabular-aligned numbers, a tiny
inline sparkline, a subtle up/down Δ.
5. **Bifrost** — the live provider binding (admin-gated): `endpoint`,
`connected` (bool), `capabilities_granted[]`, `consumer_id`, `tools[]`.
**Self-labeling states:** `not configured` (no admin key) / `not bound`
(session has no live binding) / an auth-denied state.
6. **Admin events** — a live event log of the engine's lifecycle broadcast
(a ~17-type vocabulary: `turn.started`, `session.created`, `system.*`, …),
filtered to the active session. Streaming; timestamped lines.
### 4.4 Status line (persistent, bottom)
- **Keybinding legend:** `Enter send · ⇧Enter newline · ^1^6 panes · ^C cancel`
(rendered as little `kbd` chips).
- **Version:** `ratatoskr 0.19.9` (right-aligned).
---
## 5. States to design (show these explicitly)
Provide a mock (or a toggle) for each — these are where debug UIs live or die:
- **Setup:** loading-agents · ready · create-error.
- **Connection:** offline · connected/idle · streaming (pulsing) · wire-error.
- **Turn lifecycle:** awaiting-first-token · reasoning (✦) · streaming response ·
done (+timing chip) · error · cancelled.
- **Panes:** empty/placeholder · hydrated/dense · **not-configured** (admin key
absent) · **not-bound** (Bifrost) · error · a **badge flash** on new data.
- **Persona pane specifically:** a fully-populated relations block AND a
cold/empty one (a fresh agent with no accumulated state).
---
## 6. Interaction & motion
- **Real-time is the defining trait.** The response + thinking panes stream
token-by-token; the metric rows tick; event logs append; the persona strip
re-animates each turn. Design so all of this is *calm* — the reader's eye
isn't yanked around. Reserve motion for genuine state transitions
(turn-start, done, a new event) and keep it short.
- **Keyboard-first.** `^1^6` switch panes; `Enter`/`⇧Enter`/`^C` drive the turn.
Panes are also clickable. Show focus states.
- **The aurora glow is the interaction signature** — focus rings, the top band,
the connection pulse, the primary-button hover. Make it the thing someone
remembers.
- **Copy-to-clipboard** on each pane header (with a copied-confirm state).
---
## 7. Layout — you have latitude
The current layout is a fixed two-column split. **You may rethink it** — as long
as every §4 item has a legible home and the density stays high. Directions worth
exploring (pick one, commit):
- A **command-console** feel: a slim persistent left rail of "instruments," a
dominant conversation center, a right telemetry stack.
- A **grid of live tiles** (the metrics/panes as a dashboard) with the
conversation as the anchor column.
- The **classic monitor** split, but with far better hierarchy, grouping, and
breathing room than today.
Desktop-first; design at **14401512px** wide. Graceful down to ~1100px is a
plus (this runs on a dev laptop). No mobile.
---
## 8. Deliverable — what to hand back
**A single self-contained `index.html`** (inline `<style>` + `<script>`; see §9
constraints) that:
1. Renders **Screen A** and **Screen B** (a toggle/hash is fine).
2. Has **representative static placeholder data in every §4 slot** and shows the
key §5 states (either multiple mocks or lightweight JS toggles). I want to see
the design *fully populated and dense*, not empty scaffolding.
3. Uses **clean, semantic, stable hooks** — meaningful `id`s / `class`es /
`data-*` on every dynamic slot (the transcript container, each pane body, the
PAD bars, a metric-row template, the connection dot, the tab badges, etc.).
This is how I wire real data in — treat the DOM structure as an API.
4. Imports/inlines the Australis tokens; no invented palette.
I will then **swap your placeholder content for live `fetch()` + `EventSource`
calls** against the real endpoints (§9). The cleaner and more component-shaped
your DOM, the faster and safer that wiring is. A short note listing your mount
points / how you'd expect data injected is very welcome.
---
## 9. Hard technical constraints (these make it wire-able)
- **Single file. No build step. No CDN. No external network at runtime.** This
ships to an internal LAN and must work offline. That means: **no Google Fonts /
no web-font CDN** (use a system monospace stack), no CDN JS/CSS libraries,
everything inline. (Icons: use unicode glyphs `➜ ✓ ✗ ! ● ✦ ` or hand-inlined
SVG — Australis uses Lucide-style 1.75-stroke line icons; inline them.)
- **Vanilla HTML/CSS/JS.** No framework (the production app is framework-free
vanilla JS). React/Vue prototypes can't be wired in.
- **All dynamic text is escaped** on the real side (untrusted upstream content);
assistant/reasoning bodies go through a safe-Markdown renderer (escape-first,
whitelist subset). Don't design anything that depends on raw HTML injection.
- **The real data contracts** (so your structure maps to the wire — you don't
implement these, just leave homes for their outputs):
- `GET /api/agents` → agent list (for the setup picker).
- `POST /api/sessions {agent_id, bifrost_plane?}``{session_id, agent_id, bifrost?}`.
- `GET /api/sessions/{id}/messages``{items:[{seq, role, content}], …}` (the transcript on open, incl. the seeded first-message).
- `POST /api/turns/{id} {content}``{turn_id}`, then **`GET /api/turns/{id}/stream` (SSE)** — event vocab: `text`, `thinking`, `tool_start`, `tool_result`, `done`, `error`, `awaiting_llm_first_token`, terminal events. `POST /api/turns/{id}/cancel`.
- `GET /api/sessions/{id}/tools` → tool inventory. `GET /api/sessions/{id}/bifrost` → binding state.
- `GET /api/affect/{agent_id}` / `GET /api/agents/{id}/persona_state` → PAD + relations + dominant_emotion (the persona pane + strip).
- **`GET /api/admin/events` (SSE)** → the admin lifecycle log.
---
## 10. Tone check
The user is an engineer who respects the tool that respects *their* attention.
The winning design is **quietly excellent**: dense but never cramped, alive but
never busy, cool and legible, with the aurora as a single confident signature.
Impress by making six live data streams feel *calm and readable at a glance*
that's the hard, valuable thing here, not decoration.
+79
View File
@@ -0,0 +1,79 @@
# R30 Phase-1 — φ0 PAD-point decay measurement (demo v1.0.0b14, A1 anchor fix)
Executor: ratatoskr-dev · coordinator: worldtree-dev · owner: brokkr-smithy-dev.
Method: **joint two-timescale fit (b)** (brokkr-ruled) + **empty-tail direct fit**
as mutual validation, both run off the same trajectory. Measured against the
**corrected anchor** (`decay_anchor = baseline_pad()`, positive_p_cap removed;
worldtree v1.0.0b14 `e1cdf82`).
## Method
- 4 base agents × {pos, neg} impulse, fresh `end_user` each, 26 turns, "Please
continue." neutrals + ~16s spacing (so one trajectory yields both an
emotion-free tail AND the full joint-fit series). Natural per-turn cadence for
the point-decay; spacing only lets the wall-clock emotion fade land in-window.
- **Capture:** `affect_update` `current` snapshot (live mood) + `emotions_active`
+ wall-clock, per turn.
- **push_t** (the covariate): only NEWLY-appraised, new-type-or-strictly-stronger
emotions push (dedup-gated; confirmed source-authoritative by worldtree-dev vs
`registry.py::post_turn` L307-324 — the persistent `emotions_active` decays for
render/goals but never re-pushes mood). Reconstructed from `emotions_active`
deltas: a type new or strictly-stronger-than-the-prior(decayed)-snapshot pushed
at its new intensity. `push_t[axis] = Σ OCC_PAD_IMPULSES[type]·intensity·0.15`.
- **Model** (per axis, push-then-decay): `y_t=(axis_{t+1}anchor)`,
`X_t=(axis_t+push_tanchor)`, regress `y=φ·X+c` → φ=slope, decay_rate=1−φ,
anchor=`baseline_pad`. Validated on b9 first (φ_P 0.959, c 0.003, r² 0.997).
## Result — CONFIG FAITHFUL
**φ0 (PAD-point decay retention) ≈ 0.950.97, config-faithful.** Both reads agree:
| agent | baseline P | joint φ_P | c_P | r²_P | empty-tail φ0_P |
|---|---:|---:|---:|---:|---:|
| lofn | 0.809 | 0.954 / 0.967 (pos/neg) | ~0.000 | 0.930.94 | — (excursion faded pre-empty) |
| mimir | 0.615 | 0.964 / 0.977 (pos/neg) | 0.012 / 0.015 | 0.970.99 | — (never emptied) |
| mask | 0.000 | 0.983 / 0.982 (pos/neg) | +0.004 / +0.006 | 0.90 / 0.95 | — |
| forseti | 0.239 | signal-poor (r²=0.46/0.61, excluded) | — | — | **0.95** (neg, r²=1.0) |
- **Joint fit, 6 well-conditioned runs (3 baselines; all 8 probes in): φ_P = 0.971
± 0.01** (range 0.9540.983). Consistently a hair above config 0.95 — the
empty-tail read (no push modeling) nails **0.95 exactly**, so the joint fit's
~0.97 carries a small upward bias from push under-attribution; true φ0 ≈ 0.95.
- **Empty-tail cross-check** (forseti_neg's clean pure-decay tail): **φ0_P = 0.95**,
r²=1.0 — independently confirms the joint fit.
- **config cross-check:** measured φ0 ≈ 0.950.97 vs config `10.05 = 0.95`
**config faithfully applied.** The R29 "net ~0.90" was **continuous
re-appraisal** (a new dedup-gated push most turns), NOT re-push contamination.
- **intercept c ≈ 0** across the well-conditioned runs (0.015..+0.004) → the
push_t reconstruction + push-then-decay order are correct on the deployed engine.
### Diagnostics for brokkr
- **(i) residual/c:** c≈0 → push_t + order correctly specified. ✓
- **(ii) φ≈0.95:** YES → config faithful; kills the config≠behavior worry. ✓
- **(iii) A/P decay ratio: NO clean ~1.9.** The per-axis mood-point decay is
roughly uniform (matches the single `decay_rate` in `model.py` L175-179).
CAVEAT: the per-axis **A/D** fits are noisy — small A/D excursions yield some
φ_A>1 artifacts (e.g. mask A/P 1.66 is a fit artifact), so the ratio is not
cleanly determined here; but there is **no support for 1.9×** in the mood-decay
layer. If the S2 ~1.9× chronometry is real it lives elsewhere (the OCC impulse
vectors' per-axis asymmetry, or emotion wall-clock decay), not in point-decay.
A dedicated per-axis run with larger A/D excursions would pin it.
- **trait-flat:** φ_P ~equal across baselines 0.0 / 0.615 / 0.809 → the current
decay is **trait-FLAT** (the pre-R30 baseline R30's `dynamics_from_ocean()` will
modulate). ✓
### φ_max recommendation (preserve-persistence rule, Vuong-locked)
Measured φ0 ≈ 0.96 (> 0.90) → **RELAX φ_max to ≈0.96** — the measured good
operating point defines the cap; do NOT re-base the agents faster.
## Caveats
- **forseti (baseline 0.239) is signal-poor** in the joint fit — a negative
impulse from a low baseline has little down-room → tiny excursion → ill-
conditioned regression (r² 0.46/0.61). Correctly excluded (its empty-tail
still gave a clean 0.95). High/mid-baseline agents (lofn/mimir/mask) carry the
joint-fit signal.
- **mask_neg** recovered from a demo-side SSE stall (turn 14) and completed — all
8/8 in. It is well-conditioned (φ_P 0.982, r² 0.95) and confirms the verdict
(didn't move it). `stream_turn_resilient`'s reconnect handled the transient hang.
Series: `r30-phi0-{lofn,mimir,forseti,mask}-{pos,neg}.json` (per-turn pad +
emotions_active + wall-clock). sec/turn ≈ 1925 (16s spacing + processing).
+664
View File
@@ -0,0 +1,664 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "forseti",
"step": "neg",
"baseline_pad": [
0.239,
-0.696,
0.095
],
"end_user_id": "r30-phi0-forseti-neg-1783086627",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": 14,
"seconds_per_turn": 20.63,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_neg",
"wall_clock": 1783086627.768,
"mood_reflects_after_turn": 0,
"pad": [
0.239,
-0.696,
0.095
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086646.949,
"mood_reflects_after_turn": 1,
"pad": [
0.1877,
-0.67035,
0.107825
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.5683328550825705,
"decay_remaining_s": 199.9792455070927
},
{
"type": "disgust",
"intensity": 0.5683328550825705,
"decay_remaining_s": 199.9792455070927
}
],
"n_emotions": 2
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086667.172,
"mood_reflects_after_turn": 2,
"pad": [
0.190265,
-0.671632,
0.107184
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.3556321297530136,
"decay_remaining_s": 176.77391965695713
},
{
"type": "disgust",
"intensity": 0.3556321297530136,
"decay_remaining_s": 176.77391965695713
}
],
"n_emotions": 2
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086687.501,
"mood_reflects_after_turn": 3,
"pad": [
0.192702,
-0.672851,
0.106575
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.22206186783771448,
"decay_remaining_s": 153.46314161162698
},
{
"type": "disgust",
"intensity": 0.22206186783771448,
"decay_remaining_s": 153.46314161162698
},
{
"type": "disappointment",
"intensity": 0.2698448957707733,
"decay_remaining_s": 163.10993319064005
}
],
"n_emotions": 3
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086707.822,
"mood_reflects_after_turn": 4,
"pad": [
0.195017,
-0.674008,
0.105996
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.1386369753255098,
"decay_remaining_s": 130.14462339625655
},
{
"type": "disgust",
"intensity": 0.1386369753255098,
"decay_remaining_s": 130.14462339625655
},
{
"type": "disappointment",
"intensity": 0.16846872685060676,
"decay_remaining_s": 139.79141497526962
}
],
"n_emotions": 3
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086734.409,
"mood_reflects_after_turn": 5,
"pad": [
0.197216,
-0.675108,
0.105446
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.07447378977503627,
"decay_remaining_s": 99.38579636050295
},
{
"type": "disgust",
"intensity": 0.07447378977503627,
"decay_remaining_s": 99.38579636050295
},
{
"type": "disappointment",
"intensity": 0.09049897776319624,
"decay_remaining_s": 109.03258793951602
},
{
"type": "anger",
"intensity": 0.31292609728127724,
"decay_remaining_s": 170.44160986082557
}
],
"n_emotions": 4
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086753.454,
"mood_reflects_after_turn": 6,
"pad": [
0.199305,
-0.676153,
0.104924
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.04875620814214029,
"decay_remaining_s": 78.41758126513767
},
{
"type": "disgust",
"intensity": 0.04875620814214029,
"decay_remaining_s": 78.41758126513767
},
{
"type": "disappointment",
"intensity": 0.05924751526411994,
"decay_remaining_s": 88.06437284415075
},
{
"type": "anger",
"intensity": 0.2048652281324858,
"decay_remaining_s": 149.47339476546028
}
],
"n_emotions": 4
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086772.496,
"mood_reflects_after_turn": 7,
"pad": [
0.20129,
-0.677145,
0.104428
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.031522954818552947,
"decay_remaining_s": 56.830547203428814
},
{
"type": "disgust",
"intensity": 0.031522954818552947,
"decay_remaining_s": 56.830547203428814
},
{
"type": "disappointment",
"intensity": 0.038306029487312654,
"decay_remaining_s": 66.47733878244189
},
{
"type": "anger",
"intensity": 0.13245405203550356,
"decay_remaining_s": 127.88636070375141
}
],
"n_emotions": 4
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086792.465,
"mood_reflects_after_turn": 8,
"pad": [
0.203175,
-0.678088,
0.103956
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.019516300742176532,
"decay_remaining_s": 33.097789734970185
},
{
"type": "disgust",
"intensity": 0.019516300742176532,
"decay_remaining_s": 33.097789734970185
},
{
"type": "disappointment",
"intensity": 0.023715796822228047,
"decay_remaining_s": 42.74458131398328
},
{
"type": "anger",
"intensity": 0.0820041499575214,
"decay_remaining_s": 104.1536032352928
}
],
"n_emotions": 4
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086812.741,
"mood_reflects_after_turn": 9,
"pad": [
0.204967,
-0.678983,
0.103508
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.01162721275495882,
"decay_remaining_s": 7.462523834516143
},
{
"type": "disgust",
"intensity": 0.01162721275495882,
"decay_remaining_s": 7.462523834516143
},
{
"type": "disappointment",
"intensity": 0.014129143578398723,
"decay_remaining_s": 17.109315413529234
},
{
"type": "anger",
"intensity": 0.04885555469459899,
"decay_remaining_s": 78.51833733483876
}
],
"n_emotions": 4
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086833.39,
"mood_reflects_after_turn": 10,
"pad": [
0.206668,
-0.679834,
0.103083
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "anger",
"intensity": 0.028487904554031676,
"decay_remaining_s": 51.81951538128418
}
],
"n_emotions": 1
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086853.925,
"mood_reflects_after_turn": 11,
"pad": [
0.208285,
-0.680642,
0.102679
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "anger",
"intensity": 0.017438555367845658,
"decay_remaining_s": 27.52593892013023
}
],
"n_emotions": 1
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783086873.881,
"mood_reflects_after_turn": 12,
"pad": [
0.209821,
-0.68141,
0.102295
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "anger",
"intensity": 0.010992041880764065,
"decay_remaining_s": 4.681870162512555
}
],
"n_emotions": 1
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783086895.722,
"mood_reflects_after_turn": 13,
"pad": [
0.21128,
-0.68214,
0.10193
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783086916.056,
"mood_reflects_after_turn": 14,
"pad": [
0.212666,
-0.682833,
0.101584
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783086937.153,
"mood_reflects_after_turn": 15,
"pad": [
0.213982,
-0.683491,
0.101254
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783086957.45,
"mood_reflects_after_turn": 16,
"pad": [
0.215233,
-0.684117,
0.100942
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783086977.889,
"mood_reflects_after_turn": 17,
"pad": [
0.216422,
-0.684711,
0.100645
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783086998.1,
"mood_reflects_after_turn": 18,
"pad": [
0.21755,
-0.685275,
0.100362
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783087018.85,
"mood_reflects_after_turn": 19,
"pad": [
0.218623,
-0.685811,
0.100094
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783087039.705,
"mood_reflects_after_turn": 20,
"pad": [
0.219642,
-0.686321,
0.09984
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783087060.965,
"mood_reflects_after_turn": 21,
"pad": [
0.22061,
-0.686805,
0.099598
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783087081.087,
"mood_reflects_after_turn": 22,
"pad": [
0.221529,
-0.687265,
0.099368
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783087101.401,
"mood_reflects_after_turn": 23,
"pad": [
0.222403,
-0.687701,
0.099149
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783087123.247,
"mood_reflects_after_turn": 24,
"pad": [
0.223233,
-0.688116,
0.098942
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783087143.569,
"mood_reflects_after_turn": 25,
"pad": [
0.224021,
-0.68851,
0.098745
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
}
]
}
+648
View File
@@ -0,0 +1,648 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "forseti",
"step": "pos",
"baseline_pad": [
0.239,
-0.696,
0.095
],
"end_user_id": "r30-phi0-forseti-pos-1783086626",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": 13,
"seconds_per_turn": 22.1,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_pos",
"wall_clock": 1783086626.763,
"mood_reflects_after_turn": 0,
"pad": [
0.239,
-0.696,
0.095
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086644.441,
"mood_reflects_after_turn": 1,
"pad": [
0.277475,
-0.683175,
0.12065
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.5984144368786366,
"decay_remaining_s": 202.53218516049105
},
{
"type": "disgust",
"intensity": 0.5984144368786366,
"decay_remaining_s": 202.53218516049105
}
],
"n_emotions": 2
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086662.113,
"mood_reflects_after_turn": 2,
"pad": [
0.275551,
-0.683816,
0.119368
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.39617851967235457,
"decay_remaining_s": 182.11815704225845
},
{
"type": "disgust",
"intensity": 0.39617851967235457,
"decay_remaining_s": 182.11815704225845
}
],
"n_emotions": 2
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086679.394,
"mood_reflects_after_turn": 3,
"pad": [
0.273724,
-0.684425,
0.118149
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.24687722982874277,
"decay_remaining_s": 158.7067527359457
},
{
"type": "disgust",
"intensity": 0.24687722982874277,
"decay_remaining_s": 158.7067527359457
}
],
"n_emotions": 2
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086702.921,
"mood_reflects_after_turn": 4,
"pad": [
0.259163,
-0.680729,
0.099892
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.15387922362089126,
"decay_remaining_s": 135.30775338969408
},
{
"type": "disgust",
"intensity": 0.15387922362089126,
"decay_remaining_s": 135.30775338969408
},
{
"type": "disappointment",
"intensity": 0.18699078533200852,
"decay_remaining_s": 144.95454496870715
}
],
"n_emotions": 3
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086723.536,
"mood_reflects_after_turn": 5,
"pad": [
0.258154,
-0.681493,
0.099647
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.09519664920512481,
"decay_remaining_s": 111.53750907456809
},
{
"type": "disgust",
"intensity": 0.09519664920512481,
"decay_remaining_s": 111.53750907456809
},
{
"type": "disappointment",
"intensity": 0.11568095924176013,
"decay_remaining_s": 121.18430065358118
}
],
"n_emotions": 3
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086745.341,
"mood_reflects_after_turn": 6,
"pad": [
0.228127,
-0.648588,
0.113665
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.058121613176605455,
"decay_remaining_s": 87.11468585006237
},
{
"type": "disgust",
"intensity": 0.058121613176605455,
"decay_remaining_s": 87.11468585006237
},
{
"type": "disappointment",
"intensity": 0.07062815783001622,
"decay_remaining_s": 96.76147742907544
},
{
"type": "anger",
"intensity": 0.24421705453673276,
"decay_remaining_s": 158.17049935038497
}
],
"n_emotions": 4
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086767.465,
"mood_reflects_after_turn": 7,
"pad": [
0.22867,
-0.650959,
0.112731
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.035073119286887194,
"decay_remaining_s": 62.11295803448205
},
{
"type": "disgust",
"intensity": 0.035073119286887194,
"decay_remaining_s": 62.11295803448205
},
{
"type": "disappointment",
"intensity": 0.04262011443244547,
"decay_remaining_s": 71.75974961349512
},
{
"type": "anger",
"intensity": 0.14737123451189318,
"decay_remaining_s": 133.16877153480465
}
],
"n_emotions": 4
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086789.909,
"mood_reflects_after_turn": 8,
"pad": [
0.229187,
-0.653211,
0.111845
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.020567809558126326,
"decay_remaining_s": 35.69532075372112
},
{
"type": "disgust",
"intensity": 0.020567809558126326,
"decay_remaining_s": 35.69532075372112
},
{
"type": "disappointment",
"intensity": 0.024993568146070934,
"decay_remaining_s": 45.34211233273421
},
{
"type": "anger",
"intensity": 0.08642240973758594,
"decay_remaining_s": 106.75113425404372
}
],
"n_emotions": 4
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086813.213,
"mood_reflects_after_turn": 9,
"pad": [
0.229677,
-0.65535,
0.111003
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "love",
"intensity": 0.011507424157476244,
"decay_remaining_s": 6.949925610611371
},
{
"type": "disgust",
"intensity": 0.011507424157476244,
"decay_remaining_s": 6.949925610611371
},
{
"type": "disappointment",
"intensity": 0.01398357900255792,
"decay_remaining_s": 16.596717189624453
},
{
"type": "anger",
"intensity": 0.04835222354383774,
"decay_remaining_s": 78.00573911093397
}
],
"n_emotions": 4
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086835.817,
"mood_reflects_after_turn": 10,
"pad": [
0.230144,
-0.657383,
0.110203
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "anger",
"intensity": 0.0272750628019755,
"decay_remaining_s": 49.66600394193401
}
],
"n_emotions": 1
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086858.735,
"mood_reflects_after_turn": 11,
"pad": [
0.230586,
-0.659314,
0.109442
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [
{
"type": "anger",
"intensity": 0.015766544873269436,
"decay_remaining_s": 22.536840330863996
}
],
"n_emotions": 1
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783086880.309,
"mood_reflects_after_turn": 12,
"pad": [
0.231007,
-0.661148,
0.10872
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783086902.497,
"mood_reflects_after_turn": 13,
"pad": [
0.231407,
-0.66289,
0.108034
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783086924.65,
"mood_reflects_after_turn": 14,
"pad": [
0.231786,
-0.664546,
0.107383
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783086947.622,
"mood_reflects_after_turn": 15,
"pad": [
0.232147,
-0.666119,
0.106763
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783086970.841,
"mood_reflects_after_turn": 16,
"pad": [
0.23249,
-0.667613,
0.106175
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783086994.657,
"mood_reflects_after_turn": 17,
"pad": [
0.232815,
-0.669032,
0.105617
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783087018.405,
"mood_reflects_after_turn": 18,
"pad": [
0.233124,
-0.67038,
0.105086
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783087041.421,
"mood_reflects_after_turn": 19,
"pad": [
0.233418,
-0.671661,
0.104581
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783087066.046,
"mood_reflects_after_turn": 20,
"pad": [
0.233697,
-0.672878,
0.104102
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783087088.213,
"mood_reflects_after_turn": 21,
"pad": [
0.233962,
-0.674034,
0.103647
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783087111.161,
"mood_reflects_after_turn": 22,
"pad": [
0.234214,
-0.675133,
0.103215
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783087134.816,
"mood_reflects_after_turn": 23,
"pad": [
0.234454,
-0.676176,
0.102804
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783087157.169,
"mood_reflects_after_turn": 24,
"pad": [
0.234681,
-0.677167,
0.102414
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783087179.221,
"mood_reflects_after_turn": 25,
"pad": [
0.234897,
-0.678109,
0.102043
],
"baseline_pad": [
0.239,
-0.696,
0.095
],
"emotions_active": [],
"n_emotions": 0
}
]
}
+722
View File
@@ -0,0 +1,722 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "lofn",
"step": "neg",
"baseline_pad": [
0.809,
-0.153,
0.248
],
"end_user_id": "r30-phi0-lofn-neg-1783086622",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": 16,
"seconds_per_turn": 19.17,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_neg",
"wall_clock": 1783086622.491,
"mood_reflects_after_turn": 0,
"pad": [
0.809,
-0.153,
0.248
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "disgust",
"intensity": 0.30035998307417966,
"decay_remaining_s": 168.4129038950811
},
{
"type": "shame",
"intensity": 0.17405563132969606,
"decay_remaining_s": 141.40628929119273
},
{
"type": "disappointment",
"intensity": 0.13875047669637092,
"decay_remaining_s": 130.18513083121996
}
],
"n_emotions": 3
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086640.087,
"mood_reflects_after_turn": 1,
"pad": [
0.797101,
-0.116591,
0.28448
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.1143359590977587,
"decay_remaining_s": 120.60542148213672
},
{
"type": "disappointment",
"intensity": 0.09114424340744763,
"decay_remaining_s": 109.38426302216394
},
{
"type": "love",
"intensity": 0.591203872014145,
"decay_remaining_s": 201.93213522901215
},
{
"type": "disgust",
"intensity": 0.5583592124578035,
"decay_remaining_s": 199.10288997283888
}
],
"n_emotions": 4
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086658.336,
"mood_reflects_after_turn": 2,
"pad": [
0.798798,
-0.121784,
0.279277
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.07429973774043301,
"decay_remaining_s": 99.26997889267194
},
{
"type": "disappointment",
"intensity": 0.05922890257065504,
"decay_remaining_s": 88.04882043269916
},
{
"type": "love",
"intensity": 0.38418615620499547,
"decay_remaining_s": 180.59669263954737
},
{
"type": "disgust",
"intensity": 0.3628424808602734,
"decay_remaining_s": 177.7674473833741
}
],
"n_emotions": 4
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086676.834,
"mood_reflects_after_turn": 3,
"pad": [
0.799793,
-0.124828,
0.276228
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.048075395218412885,
"decay_remaining_s": 77.72153477455913
},
{
"type": "disappointment",
"intensity": 0.03832386204894452,
"decay_remaining_s": 66.50037631458636
},
{
"type": "love",
"intensity": 0.2485863592348452,
"decay_remaining_s": 159.04824852143457
},
{
"type": "disgust",
"intensity": 0.2347760059440204,
"decay_remaining_s": 156.2190032652613
}
],
"n_emotions": 4
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086698.693,
"mood_reflects_after_turn": 4,
"pad": [
0.778597,
-0.106862,
0.258631
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.02893387689095941,
"decay_remaining_s": 52.58839865785018
},
{
"type": "disappointment",
"intensity": 0.02306497744787295,
"decay_remaining_s": 41.36724019787741
},
{
"type": "love",
"intensity": 0.14961015051870166,
"decay_remaining_s": 133.91511240472562
},
{
"type": "disgust",
"intensity": 0.14129847548988486,
"decay_remaining_s": 131.08586714855235
},
{
"type": "fear",
"intensity": 0.15347016022800833,
"decay_remaining_s": 135.17599479644593
}
],
"n_emotions": 5
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086718.436,
"mood_reflects_after_turn": 5,
"pad": [
0.780118,
-0.109169,
0.2581
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.01756835509224531,
"decay_remaining_s": 27.893003433544404
},
{
"type": "disappointment",
"intensity": 0.014004819178776343,
"decay_remaining_s": 16.67184497357163
},
{
"type": "love",
"intensity": 0.09084175824837641,
"decay_remaining_s": 109.21971718041983
},
{
"type": "disgust",
"intensity": 0.08579499390124436,
"decay_remaining_s": 106.39047192424657
},
{
"type": "fear",
"intensity": 0.09318551679439424,
"decay_remaining_s": 110.48059957214018
}
],
"n_emotions": 5
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086740.827,
"mood_reflects_after_turn": 6,
"pad": [
0.782934,
-0.113442,
0.257115
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "love",
"intensity": 0.05139530636165241,
"decay_remaining_s": 81.02685118626724
},
{
"type": "disgust",
"intensity": 0.04854001156378281,
"decay_remaining_s": 78.19760593009396
},
{
"type": "fear",
"intensity": 0.05272132856590098,
"decay_remaining_s": 82.28773357798757
}
],
"n_emotions": 3
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086760.101,
"mood_reflects_after_turn": 7,
"pad": [
0.785475,
-0.117299,
0.256226
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "love",
"intensity": 0.032257561955279894,
"decay_remaining_s": 57.97081468282442
},
{
"type": "disgust",
"intensity": 0.03046547517998656,
"decay_remaining_s": 55.14156942665116
},
{
"type": "fear",
"intensity": 0.033089821677726845,
"decay_remaining_s": 59.23169707454476
}
],
"n_emotions": 3
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086779.307,
"mood_reflects_after_turn": 8,
"pad": [
0.804869,
-0.13218,
0.278224
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "love",
"intensity": 0.020198640373952994,
"decay_remaining_s": 34.79881136118861
},
{
"type": "disgust",
"intensity": 0.019076493686511153,
"decay_remaining_s": 31.969566105015343
},
{
"type": "fear",
"intensity": 0.020719774452676518,
"decay_remaining_s": 36.05969375290893
},
{
"type": "satisfaction",
"intensity": 0.2504670427598375,
"decay_remaining_s": 159.42131925325486
}
],
"n_emotions": 4
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086797.858,
"mood_reflects_after_turn": 9,
"pad": [
0.805272,
-0.13421,
0.275277
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "love",
"intensity": 0.012755684832229715,
"decay_remaining_s": 12.047491685615732
},
{
"type": "disgust",
"intensity": 0.012047035674883615,
"decay_remaining_s": 9.21824642944246
},
{
"type": "fear",
"intensity": 0.013084787283704805,
"decay_remaining_s": 13.308374077336053
},
{
"type": "satisfaction",
"intensity": 0.1581729561572383,
"decay_remaining_s": 136.66999957768198
}
],
"n_emotions": 4
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086819.879,
"mood_reflects_after_turn": 10,
"pad": [
0.805635,
-0.136042,
0.272618
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.08844976004480654,
"decay_remaining_s": 107.8988860481542
}
],
"n_emotions": 1
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086839.229,
"mood_reflects_after_turn": 11,
"pad": [
0.805963,
-0.137695,
0.270218
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.05446957177838106,
"decay_remaining_s": 83.90247456071432
}
],
"n_emotions": 1
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783086858.27,
"mood_reflects_after_turn": 12,
"pad": [
0.806259,
-0.139188,
0.268051
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.03403401689989604,
"decay_remaining_s": 60.62432174385741
}
],
"n_emotions": 1
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783086877.589,
"mood_reflects_after_turn": 13,
"pad": [
0.806526,
-0.140534,
0.266096
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.02112414744330084,
"decay_remaining_s": 37.016411214982064
}
],
"n_emotions": 1
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783086896.265,
"mood_reflects_after_turn": 14,
"pad": [
0.806768,
-0.14175,
0.264332
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.013364186310301318,
"decay_remaining_s": 14.35418372663499
}
],
"n_emotions": 1
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783086914.677,
"mood_reflects_after_turn": 15,
"pad": [
0.806985,
-0.142847,
0.26274
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783086933.369,
"mood_reflects_after_turn": 16,
"pad": [
0.807182,
-0.143837,
0.261303
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783086951.77,
"mood_reflects_after_turn": 17,
"pad": [
0.807359,
-0.14473,
0.260006
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783086970.574,
"mood_reflects_after_turn": 18,
"pad": [
0.807519,
-0.145536,
0.258835
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783086990.041,
"mood_reflects_after_turn": 19,
"pad": [
0.807663,
-0.146264,
0.257779
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783087008.922,
"mood_reflects_after_turn": 20,
"pad": [
0.807794,
-0.146921,
0.256825
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783087027.167,
"mood_reflects_after_turn": 21,
"pad": [
0.807911,
-0.147514,
0.255965
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783087045.453,
"mood_reflects_after_turn": 22,
"pad": [
0.808017,
-0.148048,
0.255188
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783087064.761,
"mood_reflects_after_turn": 23,
"pad": [
0.808113,
-0.148531,
0.254487
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783087083.309,
"mood_reflects_after_turn": 24,
"pad": [
0.8082,
-0.148967,
0.253855
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783087101.821,
"mood_reflects_after_turn": 25,
"pad": [
0.808278,
-0.14936,
0.253284
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
}
]
}
+727
View File
@@ -0,0 +1,727 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "lofn",
"step": "pos",
"baseline_pad": [
0.809,
-0.153,
0.248
],
"end_user_id": "r30-phi0-lofn-pos-1783086620",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": 16,
"seconds_per_turn": 19.68,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_pos",
"wall_clock": 1783086620.928,
"mood_reflects_after_turn": 0,
"pad": [
0.809,
-0.153,
0.248
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "disgust",
"intensity": 0.3099496071226145,
"decay_remaining_s": 169.96853835646436
},
{
"type": "shame",
"intensity": 0.1796127233593375,
"decay_remaining_s": 142.96192375257604
},
{
"type": "disappointment",
"intensity": 0.1431803774256259,
"decay_remaining_s": 131.74076529260327
}
],
"n_emotions": 3
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086638.365,
"mood_reflects_after_turn": 1,
"pad": [
0.797101,
-0.116591,
0.28448
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.11851338162104388,
"decay_remaining_s": 122.38165644926328
},
{
"type": "disappointment",
"intensity": 0.0944743244972692,
"decay_remaining_s": 111.1604979892905
},
{
"type": "love",
"intensity": 0.612804323790595,
"decay_remaining_s": 203.7083701961387
},
{
"type": "disgust",
"intensity": 0.5787596391355618,
"decay_remaining_s": 200.87912493996546
}
],
"n_emotions": 4
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086656.254,
"mood_reflects_after_turn": 2,
"pad": [
0.798261,
-0.120141,
0.280923
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.07754355752942818,
"decay_remaining_s": 101.38516021294014
},
{
"type": "disappointment",
"intensity": 0.06181475135131101,
"decay_remaining_s": 90.16400175296735
},
{
"type": "love",
"intensity": 0.4009591717506152,
"decay_remaining_s": 182.71187395981556
},
{
"type": "disgust",
"intensity": 0.37868366220891425,
"decay_remaining_s": 179.8826287036423
}
],
"n_emotions": 4
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086674.109,
"mood_reflects_after_turn": 3,
"pad": [
0.799308,
-0.123345,
0.277713
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.050839245787556116,
"decay_remaining_s": 80.4883965619064
},
{
"type": "disappointment",
"intensity": 0.04052709776764277,
"decay_remaining_s": 69.26723810193361
},
{
"type": "love",
"intensity": 0.2628775688511382,
"decay_remaining_s": 161.81511030878184
},
{
"type": "disgust",
"intensity": 0.24827325947051937,
"decay_remaining_s": 158.98586505260857
}
],
"n_emotions": 4
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086692.205,
"mood_reflects_after_turn": 4,
"pad": [
0.776997,
-0.104434,
0.259191
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.033295427762761606,
"decay_remaining_s": 59.53830698228177
},
{
"type": "disappointment",
"intensity": 0.026541838598384764,
"decay_remaining_s": 48.31714852230899
},
{
"type": "love",
"intensity": 0.17216268590427902,
"decay_remaining_s": 140.8650207291572
},
{
"type": "disgust",
"intensity": 0.16259809224293015,
"decay_remaining_s": 138.03577547298394
},
{
"type": "fear",
"intensity": 0.17660456125074997,
"decay_remaining_s": 142.12590312087752
}
],
"n_emotions": 5
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086711.437,
"mood_reflects_after_turn": 5,
"pad": [
0.780118,
-0.109169,
0.2581
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.01761273545952419,
"decay_remaining_s": 28.017886124481926
},
{
"type": "disappointment",
"intensity": 0.014040197506204548,
"decay_remaining_s": 16.79672766450916
},
{
"type": "love",
"intensity": 0.09107123850274022,
"decay_remaining_s": 109.34459987135736
},
{
"type": "disgust",
"intensity": 0.08601172525258796,
"decay_remaining_s": 106.5153546151841
},
{
"type": "fear",
"intensity": 0.09342091774335577,
"decay_remaining_s": 110.6054822630777
}
],
"n_emotions": 5
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086738.988,
"mood_reflects_after_turn": 6,
"pad": [
0.782934,
-0.113442,
0.257115
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "shame",
"intensity": 0.010353667728203439,
"decay_remaining_s": 1.720350311056919
},
{
"type": "love",
"intensity": 0.053536337113577075,
"decay_remaining_s": 83.04706405793236
},
{
"type": "disgust",
"intensity": 0.05056209616282278,
"decay_remaining_s": 80.2178188017591
},
{
"type": "fear",
"intensity": 0.05491759888186391,
"decay_remaining_s": 84.30794644965269
}
],
"n_emotions": 4
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086758.598,
"mood_reflects_after_turn": 7,
"pad": [
0.785475,
-0.117299,
0.256226
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "love",
"intensity": 0.03322492057965059,
"decay_remaining_s": 59.43337703961838
},
{
"type": "disgust",
"intensity": 0.03137909165855888,
"decay_remaining_s": 56.604131783445105
},
{
"type": "fear",
"intensity": 0.03408213859315939,
"decay_remaining_s": 60.69425943133871
}
],
"n_emotions": 3
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086777.861,
"mood_reflects_after_turn": 8,
"pad": [
0.804869,
-0.13218,
0.278224
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "love",
"intensity": 0.020813815554036986,
"decay_remaining_s": 36.283844092364134
},
{
"type": "disgust",
"intensity": 0.01965749246770159,
"decay_remaining_s": 33.45459883619087
},
{
"type": "fear",
"intensity": 0.021350821431297014,
"decay_remaining_s": 37.54472648408446
},
{
"type": "satisfaction",
"intensity": 0.2580953338369727,
"decay_remaining_s": 160.9063519844304
}
],
"n_emotions": 4
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086797.601,
"mood_reflects_after_turn": 9,
"pad": [
0.805272,
-0.13421,
0.275277
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "love",
"intensity": 0.012822775915902978,
"decay_remaining_s": 12.30715573180832
},
{
"type": "disgust",
"intensity": 0.012110399476130585,
"decay_remaining_s": 9.477910475635053
},
{
"type": "fear",
"intensity": 0.013153609347752653,
"decay_remaining_s": 13.568038123528641
},
{
"type": "satisfaction",
"intensity": 0.1590048985559388,
"decay_remaining_s": 136.92966362387457
}
],
"n_emotions": 4
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086820.086,
"mood_reflects_after_turn": 10,
"pad": [
0.805635,
-0.136042,
0.272618
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.08799109146673098,
"decay_remaining_s": 107.64153778247811
}
],
"n_emotions": 1
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086839.995,
"mood_reflects_after_turn": 11,
"pad": [
0.805963,
-0.137695,
0.270218
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.05362997325344708,
"decay_remaining_s": 83.13356199644052
}
],
"n_emotions": 1
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783086859.213,
"mood_reflects_after_turn": 12,
"pad": [
0.806259,
-0.139188,
0.268051
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.033384868307061485,
"decay_remaining_s": 59.67109460565662
}
],
"n_emotions": 1
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783086878.469,
"mood_reflects_after_turn": 13,
"pad": [
0.806526,
-0.140534,
0.266096
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.02075236963779157,
"decay_remaining_s": 36.13750053889745
}
],
"n_emotions": 1
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783086899.093,
"mood_reflects_after_turn": 14,
"pad": [
0.806879,
-0.142312,
0.263515
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [
{
"type": "satisfaction",
"intensity": 0.012610049448190474,
"decay_remaining_s": 11.479103997183943
}
],
"n_emotions": 1
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783086917.981,
"mood_reflects_after_turn": 15,
"pad": [
0.807086,
-0.143354,
0.262003
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783086938.001,
"mood_reflects_after_turn": 16,
"pad": [
0.807273,
-0.144295,
0.260637
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783086957.169,
"mood_reflects_after_turn": 17,
"pad": [
0.807441,
-0.145144,
0.259405
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783086976.989,
"mood_reflects_after_turn": 18,
"pad": [
0.807593,
-0.14591,
0.258293
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783086996.509,
"mood_reflects_after_turn": 19,
"pad": [
0.80773,
-0.146601,
0.25729
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783087016.38,
"mood_reflects_after_turn": 20,
"pad": [
0.807854,
-0.147225,
0.256384
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783087035.803,
"mood_reflects_after_turn": 21,
"pad": [
0.807966,
-0.147788,
0.255566
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783087054.186,
"mood_reflects_after_turn": 22,
"pad": [
0.808067,
-0.148296,
0.254829
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783087073.493,
"mood_reflects_after_turn": 23,
"pad": [
0.808158,
-0.148755,
0.254163
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783087093.306,
"mood_reflects_after_turn": 24,
"pad": [
0.80824,
-0.149169,
0.253562
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783087113.033,
"mood_reflects_after_turn": 25,
"pad": [
0.808314,
-0.149542,
0.25302
],
"baseline_pad": [
0.809,
-0.153,
0.248
],
"emotions_active": [],
"n_emotions": 0
}
]
}
+675
View File
@@ -0,0 +1,675 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "mask",
"step": "neg",
"baseline_pad": [
0.0,
0.0,
0.0
],
"end_user_id": "r30-phi0-mask-neg-1783086630",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": 15,
"seconds_per_turn": 80.82,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_neg",
"wall_clock": 1783086630.5,
"mood_reflects_after_turn": 0,
"pad": [
0.0,
0.0,
0.0
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086649.461,
"mood_reflects_after_turn": 1,
"pad": [
-0.04845,
0.024225,
0.012113
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.573022497641695,
"decay_remaining_s": 200.38600877711383
},
{
"type": "disgust",
"intensity": 0.5411879144393785,
"decay_remaining_s": 197.5567635209406
}
],
"n_emotions": 2
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086669.122,
"mood_reflects_after_turn": 2,
"pad": [
-0.046028,
0.023014,
0.011507
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.356003412717222,
"decay_remaining_s": 176.82556936309612
},
{
"type": "disgust",
"intensity": 0.3362254453440429,
"decay_remaining_s": 173.99632410692286
}
],
"n_emotions": 2
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086689.724,
"mood_reflects_after_turn": 3,
"pad": [
-0.003065,
0.048415,
-0.006145
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.21338883507870404,
"decay_remaining_s": 151.49112825250143
},
{
"type": "disgust",
"intensity": 0.20153389979655376,
"decay_remaining_s": 148.66188299632816
},
{
"type": "admiration",
"intensity": 0.2397604376317611,
"decay_remaining_s": 157.25888146429597
},
{
"type": "hope",
"intensity": 0.2397604376317611,
"decay_remaining_s": 157.25888146429597
}
],
"n_emotions": 4
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086710.989,
"mood_reflects_after_turn": 4,
"pad": [
0.008064,
0.054524,
-0.010961
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.0914212721487128,
"decay_remaining_s": 109.53448270395256
},
{
"type": "disgust",
"intensity": 0.0863423125848954,
"decay_remaining_s": 106.7052374477793
},
{
"type": "admiration",
"intensity": 0.10271954580539919,
"decay_remaining_s": 115.3022359157471
},
{
"type": "hope",
"intensity": 0.17137030082196,
"decay_remaining_s": 140.63667702634183
}
],
"n_emotions": 4
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086743.877,
"mood_reflects_after_turn": 5,
"pad": [
0.035778,
0.066308,
-0.021292
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.04477060353615011,
"decay_remaining_s": 74.19632608766649
},
{
"type": "disgust",
"intensity": 0.042283347784141764,
"decay_remaining_s": 71.36708083149324
},
{
"type": "hope",
"intensity": 0.08392304783825716,
"decay_remaining_s": 105.29852041005577
},
{
"type": "admiration",
"intensity": 0.19633892293274405,
"decay_remaining_s": 147.3692225867374
}
],
"n_emotions": 4
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086767.468,
"mood_reflects_after_turn": 6,
"pad": [
0.043689,
0.071243,
-0.024916
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.023673881244883985,
"decay_remaining_s": 42.65702000792139
},
{
"type": "disgust",
"intensity": 0.022358665620168205,
"decay_remaining_s": 39.82777475174812
},
{
"type": "admiration",
"intensity": 0.10382045311283532,
"decay_remaining_s": 115.8299165069923
},
{
"type": "hope",
"intensity": 0.2119459981620312,
"decay_remaining_s": 151.1553068400353
}
],
"n_emotions": 4
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086791.346,
"mood_reflects_after_turn": 7,
"pad": [
0.05083,
0.075697,
-0.028187
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.012503122853369734,
"decay_remaining_s": 11.057594651593051
},
{
"type": "disgust",
"intensity": 0.011808504917071413,
"decay_remaining_s": 8.22834939541978
},
{
"type": "admiration",
"intensity": 0.054831730654339245,
"decay_remaining_s": 84.23049115066395
},
{
"type": "hope",
"intensity": 0.21374299133569005,
"decay_remaining_s": 151.57321130702982
}
],
"n_emotions": 4
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086815.569,
"mood_reflects_after_turn": 8,
"pad": [
0.057274,
0.079717,
-0.031139
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.02873557965477521,
"decay_remaining_s": 52.247996003823324
},
{
"type": "hope",
"intensity": 0.2131793185845018,
"decay_remaining_s": 151.44250430119635
}
],
"n_emotions": 2
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086844.225,
"mood_reflects_after_turn": 9,
"pad": [
0.078765,
0.088189,
-0.038933
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.10083556893243133,
"decay_remaining_s": 114.38595978617707
},
{
"type": "admiration",
"intensity": 0.20850330282970433,
"decay_remaining_s": 150.34469100065806
}
],
"n_emotions": 2
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086872.633,
"mood_reflects_after_turn": 10,
"pad": [
0.071085,
0.079591,
-0.035137
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.04751721226390742,
"decay_remaining_s": 77.1434685146352
},
{
"type": "admiration",
"intensity": 0.09825397727386971,
"decay_remaining_s": 113.10219972911622
}
],
"n_emotions": 2
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086903.041,
"mood_reflects_after_turn": 11,
"pad": [
0.074984,
0.082661,
-0.037126
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.04587988487785369,
"decay_remaining_s": 75.40779887167771
},
{
"type": "hope",
"intensity": 0.186780774278357,
"decay_remaining_s": 144.8989217174522
}
],
"n_emotions": 2
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783087042.241,
"mood_reflects_after_turn": 12,
"pad": [
0.123206,
0.108504,
-0.056332
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.10881120514824318,
"decay_remaining_s": 118.15392794161474
},
{
"type": "hope",
"intensity": 0.23312871354818343,
"decay_remaining_s": 155.87047702144355
}
],
"n_emotions": 2
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783087182.081,
"mood_reflects_after_turn": 13,
"pad": [
0.17662,
0.134921,
-0.076712
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.16757988183783423,
"decay_remaining_s": 139.52956931853214
}
],
"n_emotions": 1
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783087389.589,
"mood_reflects_after_turn": 14,
"pad": [
0.24959,
0.173333,
-0.104775
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783087497.261,
"mood_reflects_after_turn": 15,
"pad": [
0.23711,
0.164666,
-0.099536
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783087606.545,
"mood_reflects_after_turn": 16,
"pad": [
0.225255,
0.156433,
-0.094559
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783087716.666,
"mood_reflects_after_turn": 17,
"pad": [
0.213992,
0.148611,
-0.089831
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783087827.618,
"mood_reflects_after_turn": 18,
"pad": [
0.231792,
0.15828,
-0.09674
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783087939.216,
"mood_reflects_after_turn": 19,
"pad": [
0.220203,
0.150366,
-0.091903
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783088051.553,
"mood_reflects_after_turn": 20,
"pad": [
0.237693,
0.159948,
-0.098708
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783088165.793,
"mood_reflects_after_turn": 21,
"pad": [
0.225808,
0.151951,
-0.093772
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783088279.621,
"mood_reflects_after_turn": 22,
"pad": [
0.214518,
0.144353,
-0.089084
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783088403.505,
"mood_reflects_after_turn": 23,
"pad": [
0.203792,
0.137135,
-0.08463
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783088527.183,
"mood_reflects_after_turn": 24,
"pad": [
0.193602,
0.130279,
-0.080398
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783088650.889,
"mood_reflects_after_turn": 25,
"pad": [
0.183922,
0.123765,
-0.076378
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
}
]
}
+792
View File
@@ -0,0 +1,792 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "mask",
"step": "pos",
"baseline_pad": [
0.0,
0.0,
0.0
],
"end_user_id": "r30-phi0-mask-pos-1783086629",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": null,
"seconds_per_turn": 24.96,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_pos",
"wall_clock": 1783086629.351,
"mood_reflects_after_turn": 0,
"pad": [
0.0,
0.0,
0.0
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086646.861,
"mood_reflects_after_turn": 1,
"pad": [
0.0513,
0.02565,
0.012825
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.6039162352234125,
"decay_remaining_s": 202.98519141353884
},
{
"type": "disgust",
"intensity": 0.5703653332665563,
"decay_remaining_s": 200.15594615736558
}
],
"n_emotions": 2
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086666.743,
"mood_reflects_after_turn": 2,
"pad": [
0.048735,
0.024368,
0.012184
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.3724072003533114,
"decay_remaining_s": 179.0553478980089
},
{
"type": "disgust",
"intensity": 0.351717911444794,
"decay_remaining_s": 176.22610264183564
}
],
"n_emotions": 2
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086688.237,
"mood_reflects_after_turn": 3,
"pad": [
-0.003065,
0.048415,
-0.006145
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.21964219829255527,
"decay_remaining_s": 152.9208293437025
},
{
"type": "disgust",
"intensity": 0.2074398539429688,
"decay_remaining_s": 150.09158408752924
},
{
"type": "admiration",
"intensity": 0.24678662107884886,
"decay_remaining_s": 158.68858255549708
},
{
"type": "hope",
"intensity": 0.24678662107884886,
"decay_remaining_s": 158.68858255549708
}
],
"n_emotions": 4
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086711.569,
"mood_reflects_after_turn": 4,
"pad": [
0.008064,
0.054524,
-0.010961
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.0912108569557271,
"decay_remaining_s": 109.42042607581978
},
{
"type": "disgust",
"intensity": 0.08614358712485336,
"decay_remaining_s": 106.59118081964651
},
{
"type": "admiration",
"intensity": 0.10248312650662934,
"decay_remaining_s": 115.18817928761433
},
{
"type": "hope",
"intensity": 0.17097587495069436,
"decay_remaining_s": 140.52262039820903
}
],
"n_emotions": 4
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086743.965,
"mood_reflects_after_turn": 5,
"pad": [
0.035778,
0.066308,
-0.021292
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.044679081370124246,
"decay_remaining_s": 74.09503574277676
},
{
"type": "disgust",
"intensity": 0.04219691018289511,
"decay_remaining_s": 71.2657904866035
},
{
"type": "hope",
"intensity": 0.0837514884106196,
"decay_remaining_s": 105.19723006516602
},
{
"type": "admiration",
"intensity": 0.1959375577046099,
"decay_remaining_s": 147.26793224184766
}
],
"n_emotions": 4
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086768.05,
"mood_reflects_after_turn": 6,
"pad": [
0.043689,
0.071243,
-0.024916
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.02339842401425587,
"decay_remaining_s": 42.07770591945392
},
{
"type": "disgust",
"intensity": 0.022098511569019427,
"decay_remaining_s": 39.24846066328066
},
{
"type": "admiration",
"intensity": 0.10261245117174261,
"decay_remaining_s": 115.25060241852482
},
{
"type": "hope",
"intensity": 0.20947990242164466,
"decay_remaining_s": 150.57599275156787
}
],
"n_emotions": 4
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086792.189,
"mood_reflects_after_turn": 7,
"pad": [
0.05083,
0.075697,
-0.028187
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "joy",
"intensity": 0.012294798870898311,
"decay_remaining_s": 10.225917778446751
},
{
"type": "disgust",
"intensity": 0.011611754489181735,
"decay_remaining_s": 7.396672522273483
},
{
"type": "admiration",
"intensity": 0.05391813773602014,
"decay_remaining_s": 83.39881427751767
},
{
"type": "hope",
"intensity": 0.2101816577630614,
"decay_remaining_s": 150.74153443388352
}
],
"n_emotions": 4
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086820.117,
"mood_reflects_after_turn": 8,
"pad": [
0.057274,
0.079717,
-0.031139
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.026075683313416677,
"decay_remaining_s": 47.44008306303666
},
{
"type": "hope",
"intensity": 0.1934464683560223,
"decay_remaining_s": 146.6345913604097
}
],
"n_emotions": 2
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086844.049,
"mood_reflects_after_turn": 9,
"pad": [
0.078765,
0.088189,
-0.038933
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.10122538755257164,
"decay_remaining_s": 114.57694561236268
},
{
"type": "admiration",
"intensity": 0.20930935242772103,
"decay_remaining_s": 150.53567682684366
}
],
"n_emotions": 2
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086868.613,
"mood_reflects_after_turn": 10,
"pad": [
0.071085,
0.079591,
-0.035137
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.051656237870654834,
"decay_remaining_s": 81.27751599878032
},
{
"type": "admiration",
"intensity": 0.1068124702604253,
"decay_remaining_s": 117.23624721326131
}
],
"n_emotions": 2
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086894.345,
"mood_reflects_after_turn": 11,
"pad": [
0.074984,
0.082661,
-0.037126
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.05556926082092507,
"decay_remaining_s": 84.8918447161369
},
{
"type": "hope",
"intensity": 0.22622701843827964,
"decay_remaining_s": 154.38296756191139
}
],
"n_emotions": 2
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783086918.669,
"mood_reflects_after_turn": 12,
"pad": [
0.071235,
0.078528,
-0.03527
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.03152281574809609,
"decay_remaining_s": 56.83032883022405
},
{
"type": "hope",
"intensity": 0.12833196832421548,
"decay_remaining_s": 126.32145167599852
}
],
"n_emotions": 2
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783086944.223,
"mood_reflects_after_turn": 13,
"pad": [
0.067673,
0.074601,
-0.033506
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.015440025787232716,
"decay_remaining_s": 21.500985730861096
},
{
"type": "hope",
"intensity": 0.0628576113278172,
"decay_remaining_s": 90.99210857663557
}
],
"n_emotions": 2
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783086967.357,
"mood_reflects_after_turn": 14,
"pad": [
0.07569,
0.082271,
-0.037531
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.21496005509793759,
"decay_remaining_s": 151.8542579176958
}
],
"n_emotions": 1
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783086994.717,
"mood_reflects_after_turn": 15,
"pad": [
0.100405,
0.095258,
-0.047054
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.09727953230544714,
"decay_remaining_s": 112.60884403963128
},
{
"type": "admiration",
"intensity": 0.18101880791038277,
"decay_remaining_s": 143.34790869682615
}
],
"n_emotions": 2
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783087021.385,
"mood_reflects_after_turn": 16,
"pad": [
0.123885,
0.107595,
-0.056102
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.04540454491988354,
"decay_remaining_s": 74.89229495980246
},
{
"type": "admiration",
"intensity": 0.18669721715897325,
"decay_remaining_s": 144.87677349506185
}
],
"n_emotions": 2
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783087045.919,
"mood_reflects_after_turn": 17,
"pad": [
0.123206,
0.108504,
-0.056332
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.10077263123682631,
"decay_remaining_s": 114.35505518931606
},
{
"type": "hope",
"intensity": 0.21590601674800133,
"decay_remaining_s": 152.07160426914487
}
],
"n_emotions": 2
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783087073.542,
"mood_reflects_after_turn": 18,
"pad": [
0.128446,
0.114479,
-0.059215
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.0451373786278345,
"decay_remaining_s": 74.60018011328319
},
{
"type": "hope",
"intensity": 0.17916522799432277,
"decay_remaining_s": 142.83844749885782
}
],
"n_emotions": 2
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783087099.192,
"mood_reflects_after_turn": 19,
"pad": [
0.122024,
0.108755,
-0.056254
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.02164886685669234,
"decay_remaining_s": 38.23091671640084
},
{
"type": "hope",
"intensity": 0.08593153355623005,
"decay_remaining_s": 106.46918410197544
}
],
"n_emotions": 2
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783087124.769,
"mood_reflects_after_turn": 20,
"pad": [
0.144422,
0.120417,
-0.064842
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.04260368831648293,
"decay_remaining_s": 71.74066890904773
},
{
"type": "admiration",
"intensity": 0.19831457232683897,
"decay_remaining_s": 147.86480738196298
}
],
"n_emotions": 2
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783087149.57,
"mood_reflects_after_turn": 21,
"pad": [
0.165701,
0.131496,
-0.073
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.02146878636581833,
"decay_remaining_s": 37.81745576544926
},
{
"type": "admiration",
"intensity": 0.2015673967599869,
"decay_remaining_s": 148.67010943129222
}
],
"n_emotions": 2
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783087175.413,
"mood_reflects_after_turn": 22,
"pad": [
0.17662,
0.134921,
-0.076712
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "hope",
"intensity": 0.010377664834450205,
"decay_remaining_s": 1.8349417764983997
},
{
"type": "admiration",
"intensity": 0.19335354421287776,
"decay_remaining_s": 146.61080858593982
}
],
"n_emotions": 2
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783087202.989,
"mood_reflects_after_turn": 23,
"pad": [
0.196289,
0.145275,
-0.084276
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.2326338330358267,
"decay_remaining_s": 155.76529140012397
}
],
"n_emotions": 1
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783087228.51,
"mood_reflects_after_turn": 24,
"pad": [
0.214975,
0.155111,
-0.091463
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.1958362238481641,
"decay_remaining_s": 147.24232636599922
}
],
"n_emotions": 1
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783087253.419,
"mood_reflects_after_turn": 25,
"pad": [
0.232726,
0.164455,
-0.09829
],
"baseline_pad": [
0.0,
0.0,
0.0
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.2007809583246708,
"decay_remaining_s": 148.47660831404184
}
],
"n_emotions": 1
}
]
}
+872
View File
@@ -0,0 +1,872 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "mimir",
"step": "neg",
"baseline_pad": [
0.615,
-0.438,
0.304
],
"end_user_id": "r30-phi0-mimir-neg-1783086625",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": null,
"seconds_per_turn": 20.38,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_neg",
"wall_clock": 1783086625.378,
"mood_reflects_after_turn": 0,
"pad": [
0.615,
-0.438,
0.304
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086644.403,
"mood_reflects_after_turn": 1,
"pad": [
0.5694,
-0.4152,
0.3154
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.5695539306612214,
"decay_remaining_s": 200.08547964103704
},
{
"type": "disgust",
"intensity": 0.5062701605877524,
"decay_remaining_s": 194.25541766938022
}
],
"n_emotions": 2
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086664.681,
"mood_reflects_after_turn": 2,
"pad": [
0.549912,
-0.394084,
0.296311
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.3551375582594641,
"decay_remaining_s": 176.70503528932045
},
{
"type": "disgust",
"intensity": 0.3156778295639681,
"decay_remaining_s": 170.87497331766363
},
{
"type": "fear",
"intensity": 0.18706089404764634,
"decay_remaining_s": 144.9730999632422
}
],
"n_emotions": 3
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086683.361,
"mood_reflects_after_turn": 3,
"pad": [
0.553166,
-0.39628,
0.296695
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.232692885766144,
"decay_remaining_s": 155.77785466215136
},
{
"type": "disgust",
"intensity": 0.2068381206810169,
"decay_remaining_s": 149.9477926904945
},
{
"type": "fear",
"intensity": 0.12256585719424329,
"decay_remaining_s": 124.04591933607308
}
],
"n_emotions": 3
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086703.387,
"mood_reflects_after_turn": 4,
"pad": [
0.518002,
-0.368725,
0.259724
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.14624202733973438,
"decay_remaining_s": 132.78803853131413
},
{
"type": "disgust",
"intensity": 0.129992913190875,
"decay_remaining_s": 126.9579765596573
},
{
"type": "disappointment",
"intensity": 0.19797012040287656,
"decay_remaining_s": 147.77875916994998
},
{
"type": "fear",
"intensity": 0.2262515661747161,
"decay_remaining_s": 154.3883382984228
}
],
"n_emotions": 4
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086723.169,
"mood_reflects_after_turn": 5,
"pad": [
0.476553,
-0.33692,
0.221924
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.09264389336314277,
"decay_remaining_s": 110.19206048791541
},
{
"type": "disgust",
"intensity": 0.08235012743390467,
"decay_remaining_s": 104.36199851625857
},
{
"type": "disappointment",
"intensity": 0.19955157173522658,
"decay_remaining_s": 148.17259725738847
},
{
"type": "fear",
"intensity": 0.22805893912597325,
"decay_remaining_s": 154.78217638586133
}
],
"n_emotions": 4
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086745.413,
"mood_reflects_after_turn": 6,
"pad": [
0.458103,
-0.323686,
0.199035
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.05513893855736023,
"decay_remaining_s": 84.50704331981329
},
{
"type": "disgust",
"intensity": 0.04901238982876465,
"decay_remaining_s": 78.67698134815646
},
{
"type": "fear",
"intensity": 0.17855123491376257,
"decay_remaining_s": 142.66852714685666
},
{
"type": "disappointment",
"intensity": 0.19789428536275347,
"decay_remaining_s": 147.75979456082902
}
],
"n_emotions": 4
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086765.985,
"mood_reflects_after_turn": 7,
"pad": [
0.402057,
-0.275677,
0.152873
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.03467442696387821,
"decay_remaining_s": 61.54706533200481
},
{
"type": "disgust",
"intensity": 0.030821712856780623,
"decay_remaining_s": 55.717003360347974
},
{
"type": "disappointment",
"intensity": 0.19808949499924042,
"decay_remaining_s": 147.80859731297872
},
{
"type": "fear",
"intensity": 0.2523849638588727,
"decay_remaining_s": 159.79890281497038
}
],
"n_emotions": 4
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086788.388,
"mood_reflects_after_turn": 8,
"pad": [
0.381956,
-0.261352,
0.13122
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.020876570681207237,
"decay_remaining_s": 36.432860485364564
},
{
"type": "disgust",
"intensity": 0.018556951716628654,
"decay_remaining_s": 30.602798513707725
},
{
"type": "disappointment",
"intensity": 0.20018960251982282,
"decay_remaining_s": 148.33060688229088
},
{
"type": "fear",
"intensity": 0.23558657709136607,
"decay_remaining_s": 156.389603344106
}
],
"n_emotions": 4
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086808.77,
"mood_reflects_after_turn": 9,
"pad": [
0.389194,
-0.22797,
0.098653
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.013234463556137013,
"decay_remaining_s": 13.871369081595278
},
{
"type": "disgust",
"intensity": 0.011763967605455124,
"decay_remaining_s": 8.041307109938455
},
{
"type": "disappointment",
"intensity": 0.1996906524468551,
"decay_remaining_s": 148.2070838970179
},
{
"type": "fear",
"intensity": 0.22821788851069155,
"decay_remaining_s": 154.81666302549078
},
{
"type": "admiration",
"intensity": 0.3093949959799647,
"decay_remaining_s": 169.8798887580104
}
],
"n_emotions": 5
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086829.771,
"mood_reflects_after_turn": 10,
"pad": [
0.370347,
-0.218296,
0.082287
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.19193309895123337,
"decay_remaining_s": 146.2458337348137
},
{
"type": "disappointment",
"intensity": 0.2062662817126418,
"decay_remaining_s": 149.81075670573438
},
{
"type": "fear",
"intensity": 0.21939296850934625,
"decay_remaining_s": 152.86463131248922
}
],
"n_emotions": 3
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086849.104,
"mood_reflects_after_turn": 11,
"pad": [
0.350217,
-0.206212,
0.065891
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.12388568421961552,
"decay_remaining_s": 124.57608311541833
},
{
"type": "disappointment",
"intensity": 0.20332079637339762,
"decay_remaining_s": 149.09882468139187
},
{
"type": "fear",
"intensity": 0.2323666244267401,
"decay_remaining_s": 155.70840380986473
}
],
"n_emotions": 3
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783086869.384,
"mood_reflects_after_turn": 12,
"pad": [
0.33205,
-0.195306,
0.051093
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.07780106612675619,
"decay_remaining_s": 101.54926313989921
},
{
"type": "disappointment",
"intensity": 0.22439333830950783,
"decay_remaining_s": 153.9801247745848
},
{
"type": "fear",
"intensity": 0.2564495294965804,
"decay_remaining_s": 160.58970390305765
}
],
"n_emotions": 3
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783086889.461,
"mood_reflects_after_turn": 13,
"pad": [
0.315654,
-0.185464,
0.037738
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.04872953151589588,
"decay_remaining_s": 78.39049118764898
},
{
"type": "disappointment",
"intensity": 0.1972955280908204,
"decay_remaining_s": 147.60980334853693
},
{
"type": "fear",
"intensity": 0.2254806035323662,
"decay_remaining_s": 154.21938247700982
}
],
"n_emotions": 3
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783086908.809,
"mood_reflects_after_turn": 14,
"pad": [
0.300857,
-0.176581,
0.025685
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.03107235452885492,
"decay_remaining_s": 56.11789504968225
},
{
"type": "disappointment",
"intensity": 0.20085954804216538,
"decay_remaining_s": 148.49597916282048
},
{
"type": "fear",
"intensity": 0.2295537691910462,
"decay_remaining_s": 155.10555829129336
}
],
"n_emotions": 3
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783086928.503,
"mood_reflects_after_turn": 15,
"pad": [
0.287503,
-0.168564,
0.014808
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.01965785899626446,
"decay_remaining_s": 33.455521760500005
},
{
"type": "disappointment",
"intensity": 0.21841846782909705,
"decay_remaining_s": 152.64427972406102
},
{
"type": "fear",
"intensity": 0.24962110609039664,
"decay_remaining_s": 159.25385885253388
}
],
"n_emotions": 3
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783086948.854,
"mood_reflects_after_turn": 16,
"pad": [
0.27545,
-0.161328,
0.004991
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.012190100178442454,
"decay_remaining_s": 9.802600485010554
},
{
"type": "disappointment",
"intensity": 0.2133361557047814,
"decay_remaining_s": 151.47890707778464
},
{
"type": "fear",
"intensity": 0.24381274937689307,
"decay_remaining_s": 158.0884862062575
}
],
"n_emotions": 3
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783086969.465,
"mood_reflects_after_turn": 17,
"pad": [
0.264573,
-0.154799,
-0.003869
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.2037017252636142,
"decay_remaining_s": 149.19147480887426
},
{
"type": "fear",
"intensity": 0.2328019717298448,
"decay_remaining_s": 155.8010539373471
}
],
"n_emotions": 2
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783086990.957,
"mood_reflects_after_turn": 18,
"pad": [
0.254756,
-0.148905,
-0.011865
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.18973405834522244,
"decay_remaining_s": 145.67544139043395
},
{
"type": "fear",
"intensity": 0.2168389238231114,
"decay_remaining_s": 152.2850205189068
}
],
"n_emotions": 2
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783087012.069,
"mood_reflects_after_turn": 19,
"pad": [
0.21759,
-0.117456,
-0.038546
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.1852427384058006,
"decay_remaining_s": 144.48964317734817
},
{
"type": "fear",
"intensity": 0.21170598674948643,
"decay_remaining_s": 151.09922230582103
}
],
"n_emotions": 2
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783087032.033,
"mood_reflects_after_turn": 20,
"pad": [
0.184047,
-0.089073,
-0.062625
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.19317699595546273,
"decay_remaining_s": 146.56559175976838
},
{
"type": "fear",
"intensity": 0.220773709663386,
"decay_remaining_s": 153.17517088824124
}
],
"n_emotions": 2
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783087051.317,
"mood_reflects_after_turn": 21,
"pad": [
0.153775,
-0.063457,
-0.084356
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.19876713783323796,
"decay_remaining_s": 147.97763675575507
},
{
"type": "fear",
"intensity": 0.2271624432379863,
"decay_remaining_s": 154.58721588422796
}
],
"n_emotions": 2
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783087072.549,
"mood_reflects_after_turn": 22,
"pad": [
0.126454,
-0.040339,
-0.103969
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.18763303461696476,
"decay_remaining_s": 145.1242634318717
},
{
"type": "fear",
"intensity": 0.21443775384795977,
"decay_remaining_s": 151.73384256034456
}
],
"n_emotions": 2
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783087093.533,
"mood_reflects_after_turn": 23,
"pad": [
0.101797,
-0.019474,
-0.121669
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.19164580018360228,
"decay_remaining_s": 146.17168571463864
},
{
"type": "fear",
"intensity": 0.21902377163840261,
"decay_remaining_s": 152.7812648431115
}
],
"n_emotions": 2
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783087113.753,
"mood_reflects_after_turn": 24,
"pad": [
0.078957,
0.003138,
-0.133944
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.1975245722197446,
"decay_remaining_s": 147.66723355555015
},
{
"type": "fear",
"intensity": 0.26856666671857243,
"decay_remaining_s": 162.8749073873274
}
],
"n_emotions": 2
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783087134.897,
"mood_reflects_after_turn": 25,
"pad": [
0.059461,
0.01635,
-0.152061
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.18180516512487083,
"decay_remaining_s": 143.56246684118923
},
{
"type": "fear",
"intensity": 0.20777733157128098,
"decay_remaining_s": 150.1720459696621
}
],
"n_emotions": 2
}
]
}
+877
View File
@@ -0,0 +1,877 @@
{
"probe": "R30 Phase-1 \u03c60 step-response",
"server": "http://10.250.50.152:8080",
"agent": "mimir",
"step": "pos",
"baseline_pad": [
0.615,
-0.438,
0.304
],
"end_user_id": "r30-phi0-mimir-pos-1783086623",
"n_turns": 26,
"total_turns": 26,
"emotion_fade_turn_index": null,
"seconds_per_turn": 19.38,
"method": "joint two-timescale fit (b): natural cadence, push_t covariate from emotions_active deltas; anchor = baseline_pad on b14, neutral(0) on b9.",
"series": [
{
"turn": 1,
"sent": "impulse_pos",
"wall_clock": 1783086624.133,
"mood_reflects_after_turn": 0,
"pad": [
0.615,
-0.438,
0.304
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [],
"n_emotions": 0
},
{
"turn": 2,
"sent": "neutral_1",
"wall_clock": 1783086642.287,
"mood_reflects_after_turn": 1,
"pad": [
0.653475,
-0.425175,
0.32965
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.5946814824398606,
"decay_remaining_s": 202.22244382273544
},
{
"type": "disgust",
"intensity": 0.5286057621687651,
"decay_remaining_s": 196.3923818510786
}
],
"n_emotions": 2
},
{
"turn": 3,
"sent": "neutral_2",
"wall_clock": 1783086659.981,
"mood_reflects_after_turn": 2,
"pad": [
0.546486,
-0.391773,
0.295906
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.3918348442531504,
"decay_remaining_s": 181.57246376361832
},
{
"type": "disgust",
"intensity": 0.3482976393361337,
"decay_remaining_s": 175.7424017919615
},
{
"type": "fear",
"intensity": 0.20639038192480808,
"decay_remaining_s": 149.84052843754006
}
],
"n_emotions": 3
},
{
"turn": 4,
"sent": "neutral_3",
"wall_clock": 1783086677.926,
"mood_reflects_after_turn": 3,
"pad": [
0.553166,
-0.39628,
0.296695
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.2610046930700496,
"decay_remaining_s": 161.46119646298973
},
{
"type": "disgust",
"intensity": 0.23200417161782186,
"decay_remaining_s": 155.6311344913329
},
{
"type": "fear",
"intensity": 0.1374784786931611,
"decay_remaining_s": 129.72926113691145
}
],
"n_emotions": 3
},
{
"turn": 5,
"sent": "neutral_4",
"wall_clock": 1783086698.685,
"mood_reflects_after_turn": 4,
"pad": [
0.512897,
-0.365079,
0.257393
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.16130247461172786,
"decay_remaining_s": 137.6397821335694
},
{
"type": "disgust",
"intensity": 0.143379977432647,
"decay_remaining_s": 131.80972016191257
},
{
"type": "disappointment",
"intensity": 0.21835768349943682,
"decay_remaining_s": 152.63050277220526
},
{
"type": "fear",
"intensity": 0.24955163828507068,
"decay_remaining_s": 159.24008190067812
}
],
"n_emotions": 4
},
{
"turn": 6,
"sent": "neutral_5",
"wall_clock": 1783086717.533,
"mood_reflects_after_turn": 5,
"pad": [
0.476553,
-0.33692,
0.221924
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.10267462961751213,
"decay_remaining_s": 115.28058704753809
},
{
"type": "disgust",
"intensity": 0.09126633743778854,
"decay_remaining_s": 109.45052507588127
},
{
"type": "disappointment",
"intensity": 0.22115741225592794,
"decay_remaining_s": 153.26112381701117
},
{
"type": "fear",
"intensity": 0.2527513282924891,
"decay_remaining_s": 159.87070294548403
}
],
"n_emotions": 4
},
{
"turn": 7,
"sent": "neutral_6",
"wall_clock": 1783086739.031,
"mood_reflects_after_turn": 6,
"pad": [
0.449846,
-0.31767,
0.19351
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.06322985846830854,
"decay_remaining_s": 91.28437648115279
},
{
"type": "disgust",
"intensity": 0.05620431863849648,
"decay_remaining_s": 85.45431450949596
},
{
"type": "fear",
"intensity": 0.2047512993235141,
"decay_remaining_s": 149.44586030819616
},
{
"type": "disappointment",
"intensity": 0.2269326900835614,
"decay_remaining_s": 154.53712772216852
}
],
"n_emotions": 4
},
{
"turn": 8,
"sent": "neutral_7",
"wall_clock": 1783086758.046,
"mood_reflects_after_turn": 7,
"pad": [
0.41965,
-0.294133,
0.164269
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.04121611656303021,
"decay_remaining_s": 70.10170683199324
},
{
"type": "disgust",
"intensity": 0.03663654805602685,
"decay_remaining_s": 64.27164486033641
},
{
"type": "disappointment",
"intensity": 0.2354611288690008,
"decay_remaining_s": 156.36323881296713
},
{
"type": "fear",
"intensity": 0.26909843299314384,
"decay_remaining_s": 162.97281794144
}
],
"n_emotions": 4
},
{
"turn": 9,
"sent": "neutral_8",
"wall_clock": 1783086777.661,
"mood_reflects_after_turn": 8,
"pad": [
0.39849,
-0.279055,
0.141477
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.026584584239420582,
"decay_remaining_s": 48.39680145621735
},
{
"type": "disgust",
"intensity": 0.023630741546151624,
"decay_remaining_s": 42.5667394845605
},
{
"type": "fear",
"intensity": 0.19350137608500168,
"decay_remaining_s": 146.64863893918286
},
{
"type": "disappointment",
"intensity": 0.254924883656064,
"decay_remaining_s": 160.29454785314365
}
],
"n_emotions": 4
},
{
"turn": 10,
"sent": "neutral_9",
"wall_clock": 1783086797.421,
"mood_reflects_after_turn": 9,
"pad": [
0.34731,
-0.234916,
0.099845
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.017110119721514862,
"decay_remaining_s": 26.584802898475584
},
{
"type": "disgust",
"intensity": 0.01520899530801321,
"decay_remaining_s": 20.75474092681875
},
{
"type": "disappointment",
"intensity": 0.2581692077008076,
"decay_remaining_s": 160.9205177138982
},
{
"type": "fear",
"intensity": 0.29505052308663726,
"decay_remaining_s": 167.53009684237108
}
],
"n_emotions": 4
},
{
"turn": 11,
"sent": "neutral_10",
"wall_clock": 1783086816.311,
"mood_reflects_after_turn": 10,
"pad": [
0.38627,
-0.233733,
0.089968
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "love",
"intensity": 0.011226425089725682,
"decay_remaining_s": 5.72622706086814
},
{
"type": "fear",
"intensity": 0.19359084851705805,
"decay_remaining_s": 146.67152100476363
},
{
"type": "admiration",
"intensity": 0.2624511171738719,
"decay_remaining_s": 161.73474673728325
},
{
"type": "disappointment",
"intensity": 0.28205044552809555,
"decay_remaining_s": 165.299669708204
}
],
"n_emotions": 4
},
{
"turn": 12,
"sent": "neutral_11",
"wall_clock": 1783086835.657,
"mood_reflects_after_turn": 11,
"pad": [
0.336281,
-0.194013,
0.053359
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.16921975131393227,
"decay_remaining_s": 140.01158540005176
},
{
"type": "disappointment",
"intensity": 0.2777229250981473,
"decay_remaining_s": 164.53432696602528
},
{
"type": "fear",
"intensity": 0.3173976286835969,
"decay_remaining_s": 171.14390609449813
}
],
"n_emotions": 3
},
{
"turn": 13,
"sent": "neutral_12",
"wall_clock": 1783086855.029,
"mood_reflects_after_turn": 12,
"pad": [
0.317158,
-0.182533,
0.037782
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.10921596877410417,
"decay_remaining_s": 118.33771366610162
},
{
"type": "disappointment",
"intensity": 0.315,
"decay_remaining_s": 170.7685753007872
},
{
"type": "fear",
"intensity": 0.36000000000000004,
"decay_remaining_s": 177.37815442926006
}
],
"n_emotions": 3
},
{
"turn": 14,
"sent": "neutral_13",
"wall_clock": 1783086873.849,
"mood_reflects_after_turn": 13,
"pad": [
0.299899,
-0.172172,
0.023724
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.07091348313646222,
"decay_remaining_s": 96.96103907530583
},
{
"type": "disappointment",
"intensity": 0.28711363866906614,
"decay_remaining_s": 166.18035123619381
},
{
"type": "fear",
"intensity": 0.32812987276464706,
"decay_remaining_s": 172.78993036466667
}
],
"n_emotions": 3
},
{
"turn": 15,
"sent": "neutral_14",
"wall_clock": 1783086893.369,
"mood_reflects_after_turn": 14,
"pad": [
0.284323,
-0.162822,
0.011037
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.04491127141677074,
"decay_remaining_s": 74.3516044866393
},
{
"type": "disappointment",
"intensity": 0.29031780228931453,
"decay_remaining_s": 166.72968859977752
},
{
"type": "fear",
"intensity": 0.331791774044931,
"decay_remaining_s": 173.3392677282504
}
],
"n_emotions": 3
},
{
"turn": 16,
"sent": "neutral_15",
"wall_clock": 1783086913.196,
"mood_reflects_after_turn": 15,
"pad": [
0.300857,
-0.176581,
0.025685
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.028350283954324103,
"decay_remaining_s": 51.57981733722619
},
{
"type": "disappointment",
"intensity": 0.18326339629797098,
"decay_remaining_s": 143.95790145036443
},
{
"type": "fear",
"intensity": 0.20944388148339546,
"decay_remaining_s": 150.5674805788373
}
],
"n_emotions": 3
},
{
"turn": 17,
"sent": "neutral_16",
"wall_clock": 1783086932.748,
"mood_reflects_after_turn": 16,
"pad": [
0.287503,
-0.168564,
0.014808
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.017999206667636186,
"decay_remaining_s": 29.092268708013105
},
{
"type": "disappointment",
"intensity": 0.19998918209920172,
"decay_remaining_s": 148.28102667157412
},
{
"type": "fear",
"intensity": 0.22855906525623054,
"decay_remaining_s": 154.89060580004698
}
],
"n_emotions": 3
},
{
"turn": 18,
"sent": "neutral_17",
"wall_clock": 1783086951.78,
"mood_reflects_after_turn": 17,
"pad": [
0.27545,
-0.161328,
0.004991
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "admiration",
"intensity": 0.011472658135482627,
"decay_remaining_s": 6.80015583612609
},
{
"type": "disappointment",
"intensity": 0.20078036656888026,
"decay_remaining_s": 148.4764624289002
},
{
"type": "fear",
"intensity": 0.22946327607872036,
"decay_remaining_s": 155.08604155737305
}
],
"n_emotions": 3
},
{
"turn": 19,
"sent": "neutral_18",
"wall_clock": 1783086971.101,
"mood_reflects_after_turn": 18,
"pad": [
0.264573,
-0.154799,
-0.003869
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.19704165616123376,
"decay_remaining_s": 147.54606989733307
},
{
"type": "fear",
"intensity": 0.22519046418426716,
"decay_remaining_s": 154.15564902580593
}
],
"n_emotions": 2
},
{
"turn": 20,
"sent": "neutral_19",
"wall_clock": 1783086990.985,
"mood_reflects_after_turn": 19,
"pad": [
0.254756,
-0.148905,
-0.011865
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.18973405834522244,
"decay_remaining_s": 145.67544139043395
},
{
"type": "fear",
"intensity": 0.2168389238231114,
"decay_remaining_s": 152.2850205189068
}
],
"n_emotions": 2
},
{
"turn": 21,
"sent": "neutral_20",
"wall_clock": 1783087010.449,
"mood_reflects_after_turn": 20,
"pad": [
0.21759,
-0.117456,
-0.038546
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.1896502128984779,
"decay_remaining_s": 145.653562736846
},
{
"type": "fear",
"intensity": 0.21674310045540332,
"decay_remaining_s": 152.26314186531883
}
],
"n_emotions": 2
},
{
"turn": 22,
"sent": "neutral_21",
"wall_clock": 1783087030.22,
"mood_reflects_after_turn": 21,
"pad": [
0.184047,
-0.089073,
-0.062625
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.20066326009836047,
"decay_remaining_s": 148.4475837873386
},
{
"type": "fear",
"intensity": 0.229329440112412,
"decay_remaining_s": 155.05716291581146
}
],
"n_emotions": 2
},
{
"turn": 23,
"sent": "neutral_22",
"wall_clock": 1783087048.985,
"mood_reflects_after_turn": 22,
"pad": [
0.153775,
-0.063457,
-0.084356
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.20843728238381443,
"decay_remaining_s": 150.32901538131946
},
{
"type": "fear",
"intensity": 0.23821403701007368,
"decay_remaining_s": 156.93859450979235
}
],
"n_emotions": 2
},
{
"turn": 24,
"sent": "neutral_23",
"wall_clock": 1783087069.326,
"mood_reflects_after_turn": 23,
"pad": [
0.126454,
-0.040339,
-0.103969
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.1986416656890884,
"decay_remaining_s": 147.94638098060162
},
{
"type": "fear",
"intensity": 0.22701904650181534,
"decay_remaining_s": 154.55596010907448
}
],
"n_emotions": 2
},
{
"turn": 25,
"sent": "neutral_24",
"wall_clock": 1783087089.327,
"mood_reflects_after_turn": 24,
"pad": [
0.101797,
-0.019474,
-0.121669
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.20924669951582328,
"decay_remaining_s": 150.52085819660982
},
{
"type": "fear",
"intensity": 0.2391390851609409,
"decay_remaining_s": 157.13043732508268
}
],
"n_emotions": 2
},
{
"turn": 26,
"sent": "neutral_25",
"wall_clock": 1783087108.517,
"mood_reflects_after_turn": 25,
"pad": [
0.079544,
-0.000644,
-0.137644
],
"baseline_pad": [
0.615,
-0.438,
0.304
],
"emotions_active": [
{
"type": "disappointment",
"intensity": 0.22064306188831107,
"decay_remaining_s": 153.14587048318154
},
{
"type": "fear",
"intensity": 0.25216349930092696,
"decay_remaining_s": 159.7554496116544
}
],
"n_emotions": 2
}
]
}
@@ -1,187 +0,0 @@
# Proposal: Authored History Write (non-generating ledger seed)
**From:** ratatoskr-dev (reference Tier-3 consumer)
**To:** worldtree-dev (Conversation API / engine owner)
**Status:** Draft for scoping — pre-contract (heid-panel-reviewed 2026-07-05)
**Date:** 2026-07-05
## Motivation
Consumer apps need to write a turn into a session's history **as the agent**
(or another author) *without triggering a model generation* — e.g. an authored
opening/greeting, imported history, scripted narration. Ratatoskr's immediate
driver is a SillyTavern-style **first-message**: a fixed authored opening that
replaces the model-generated greeting and sets tone/tense/style by example.
This **cannot** be done client-side. Worldtree assembles context server-side,
and the current API exposes no author-role write path: `POST
/sessions/{id}/messages`'s `role` is a *model-role* override (`role:
"assistant"` → `404 "Unknown model role"`), and `assistant` as an *author*-role
exists only as a read-side `/search` filter. So a model-visible authored turn
needs engine support.
## The primitive (recentered)
The fundamental operation is **write a turn into the session ledger WITHOUT
generation**. "Author" (who wrote it) is an *attribute* of that write, not the
defining axis — so we name the operation, not the attribute:
> **Authored history write** — persist a model-visible turn into a session's
> ledger: no generation, no lived-turn side-effects by default, provenance
> always set.
The design space is two independent axes; this primitive is one cell:
| | side-effects ON | side-effects OFF |
|-----------------------|------------------------------|-----------------------------|
| **generation ON** | `POST /messages` (today) | — |
| **generation OFF** | *(future: affect replay)* | **authored history write** |
First-message = one caller: `author=assistant`, at session-create, `effects=none`.
## v1 use cases (narrowed)
1. **First-message / greeting** (the driver).
2. **Append-only narrator / scripted / scene turns.**
3. **Debug / test state injection** (ratatoskr instrumentation).
## Explicitly OUT of v1 — separate future primitives (share infra, not shape)
- **History import (batch)** — atomic multi-turn seed with memory/trust policy +
idempotency. A batch API, not a single POST.
- **Edit / regenerate** — history *mutation* (replace / supersede / tombstone /
audit), not injection.
- **Few-shot priming** — likely context-assembly config (exemplar block), not
fake ledger history.
- **Arbitrary mid-history insertion** — a "rewrite-history" capability with
explicit invalidation semantics.
- **Prefill / assistant-continuation** (`author` + generate) and **authored
tool-result turns** — noted; outside the seed-only contract.
## Design decisions
### 1. Side-effects — DEFAULT OFF; bounded opt-in `[operator-locked default; opt-in surface tightened by review]`
Authored writes are inert by default: no affect appraisal (no PAD update), no
memory write, no Bifrost/tool emission. Opt-in is a **bounded enum**, not loose
booleans:
```
effects: "none" (default) | "memory_import"
```
Synthetic affect and Bifrost emission are deliberately **not** opt-in-able here —
replaying affect for authored content is a separate primitive (the
generation-OFF / side-effects-ON cell). Rationale: keep this one write-API from
becoming a cross-subsystem mutation backdoor. Load-bearing for affect/memory
consumers — ratatoskr instruments exactly these signals.
### 2. Author-role — distinct field, restricted set `[rec]`
- New field **`author`**, distinct from the model-role `role` (the collision
that 404s).
- v1 roles: **`assistant`** (agent) + **`system`** (OOC / narrator). **`user` is
NOT injectable** on this endpoint — model-visible spoofed user input is a
consent / audit / abuse surface; deferred to the future import API under
owner/service scope.
- Nuance for the engine owner: `author` risks doing double duty — *provenance*
("who wrote it") vs *rendering-role* ("how it appears in assembled context";
an `assistant` turn renders as model output, a `system` turn as instruction).
These likely want to be separable (a rendering/turn-class vs an `authored_by`
provenance). Final shape is engine-owned (context assembly is yours) — but the
concern is ours to raise, not punt.
### 3. Generation contract — seed-only, DISTINCT SUB-RESOURCE `[position taken]`
Authored writes never trigger generation. We take a position (not defer): a
**distinct sub-resource**, e.g. `POST /sessions/{id}/history`, **not** a
`generate:false` flag on `POST /messages`. Reasons: explicit-over-implicit
(don't make "did generation happen?" a parameter — the same implicit-mode
coupling that bit us with `role`); different response contract (no generation
id, no SSE stream, no token usage); different error surface. Exact path is yours.
### 4. Provenance — structured, always present `[rec, expanded]`
Not a boolean. Every authored turn carries: the **write actor** (which
consumer/caller injected it), the **claimed author**, **injected-at vs
claimed-original** timestamps, **trust/origin**, and **visibility** flags
(model-visible? user-visible? memory-eligible?). Available to admin/audit APIs
even when not rendered to the model.
### 5. Positioning — append-only + create-time (v1) `[revised: was arbitrary insertion]`
v1 supports **create-time seed and append-to-tail only**. Arbitrary mid-history
insertion is deferred: it breaks turn-numbering, stales existing embeddings,
desyncs the affect timeline, and races in-flight generation — a separate future
"rewrite-history" capability with explicit invalidation semantics.
## Event / lifecycle contract — positions we take (consumer contracts we validate)
- **Default-off authored seed emits NO `turn.started` / `done` and NO Bifrost
appraisal wire.** Stated explicitly so instrumented consumers (us) don't read
silence as failure.
- **Authored turns get a distinct lifecycle phase** — propose **`seeded`** (or
`authored`), NOT `completed` (which implies generation ran). Consumers
filter/display by phase.
- **Idempotency keys required** on authored writes (retries must not duplicate
turns).
- **In-progress generation** — authored writes are rejected or serialized while
a session has an active generation (ordering safety).
## Inherent property (documented, not a bug)
**Indirect affect contamination.** Even with `effects:none`, the *next generated
turn is appraised in the context of* the authored turn — so an emotionally
charged authored beat perturbs affect regardless of any flag. No flag prevents
it; it is inherent. Consumers (ratatoskr especially, as the affect instrument)
must not misattribute the resulting drift.
## Genuinely engine-owned open questions
- Exact endpoint path + field / enum names.
- **Model-visible provenance in assembled context** — an engine-consistency call
*and a security one*: an authored `system` / `user` turn indistinguishable
from real input is a spoofing vector. Framed as security, not just rendering.
- `memory_import` semantics when the future import API opts in (embedding,
origin/trust tagging, retrieval ranking vs lived memory).
- Auth/scope: we assume **owner-only for v1**; per-author-role restrictions
(esp. `system`) TBD — confirm or correct.
## Ratatoskr as reference consumer
First consumer: first-message (`author=assistant`, create-time, `effects:none`)
in the web surface + debug seed in the CLI. We commit to validating the
primitive — including the event-silence contract and the `seeded` phase —
end-to-end against the reference planes.
## Consumer integration constraint (engine-imposed — Worldtree #347)
The primitive is **Heimdall-gated with hide-existence** (a per-tenant policy
decision — some tenants are never granted it, not a rollout stage). Ratatoskr's
consumer side MUST tolerate per-tenant absence:
- A granted tenant gets the sub-resource; an **ungranted tenant sees `404` (not
`403`)** — as if the feature never existed.
- Treat `404` on the authored-history-write sub-resource as **"feature absent
for this tenant"** → fall back gracefully (no authored first-message; the
model-generated greeting), never surface it as an error or "denied."
- **Do NOT capability-probe or advertise-detect** — the feature is deliberately
undiscoverable in `/capabilities` for ungranted tenants (same hide-existence
posture as the R27-V1A cross-owner pattern).
**Provider constraint (first-message specifically).** A create-time first-message
makes the assistant turn `seq 0`. Assistant-first-tolerant providers (vLLM /
`openai_compat` — what our Tier-3 characters, incl. sindra, run) accept it out of
the box. **Anthropic-family providers reject an assistant-first array** ("first
message must use the user role") → the next generation `400`s. So the consumer
must **gate first-message on provider compatibility** (or treat it as
vLLM/`openai_compat`-only for v1). Sindra = `openai_compat` → unaffected;
provider-agnostic normalization is a deferred engine follow-up.
---
*This brief was cold-read-pressure-tested by a cross-frontier panel (Grok /
Codex / GLM) before handoff; the v1 narrowing (append-only, bounded `effects`
enum, edit/regenerate + import split out) and the positions-taken (sub-resource,
event-silence, `seeded` phase, structured provenance, `user`-author restriction)
are the triaged result.*
@@ -1,170 +0,0 @@
# Affect egress — consumer reference (delivered vs hidden)
**Audience:** downstream consumers of Worldtree's affect surfaces (ratatoskr,
Skaldsong, any Tier-3 / SSE consumer).
**Scope:** what the affect pipeline **delivers on the wire** (structured state,
available to consumers) versus what stays **hidden** (the rendered natural-
language strings injected into the agent's system prompt, never emitted).
**Source of truth:** the render code (`core/persona/renderer.py`,
`core/persona/stance_render.py`) and the two vendored canon files
(`core/persona/canon/d2-mood-render-canon-v1.json` = mood/PAD;
`d2-render-canon-v1.json` = relationship). Owner of the canon strings:
`brokkr-smithy-dev` (R22/R24 relational + mood render).
---
## The model in one line
**The wire delivers the render INPUTS (structured state). The render OUTPUTS
(the NL strings the agent actually reads) are hidden-prompt-only.** A consumer
reconstructs the outputs by applying the canon (this document) to the delivered
inputs — the render is pure + deterministic, so reconstruction is byte-exact
(with one salience caveat, below).
This is by design. The mood canon's own discipline: *"model-agnostic
context-level NL only; the LLM never sees a number"* and *"never push explicit
disclosure of agent feelings to the user (hidden-prompt-only)."* The rendered
strings are for the AGENT's hidden system prompt, **not for verbatim end-user
display.**
---
## 1. DELIVERED — on the wire, structured
### 1a. `affect.emit` (Tier-3 Bifrost egress — the Tier-3 consumer surface, e.g. ratatoskr)
`AffectSnapshot` per `(agent_id, end_user_id)`:
| field | shape | notes |
|---|---|---|
| `pad` | `{pleasure, arousal, dominance}` floats [-1,1] | the current mood POINT |
| `relations` | `list[RelationEdge payload]` — per target: `warmth`, `agency`, `trust_ability`, `trust_integrity`, `trust_benevolence` (each a value + confidence + evidence_count), `target_entity`, `relation_context` | the **only** place relationship state is delivered |
| `dominant_emotion` | `strnull` — OCC type (e.g. `"anger"`) | **type-only** (b23); see the salience caveat in §3 |
| `schema_version` | `"relation_edge/1"` | versions the `relations` payload only |
| `emitted_at` | ISO8601 | |
**Not on `affect.emit`:** the full active-emotions list, `baseline_pad`,
`mood_drift`, `last_updated_at`, and every rendered string.
### 1b. `affect_update` SSE event (#204 — turn-stream observability)
`PersonaStateSnapshot`: `agent_id`, `pad`, `dominant_emotion`,
`emotions_active` `[{type, intensity, decay_remaining_s}]`, `baseline_pad`,
`mood_drift`, `last_updated_at`. **No `relations`, no rendered strings.**
> **Tier-3 consumers do NOT receive `affect_update`.** It is suppressed for
> consumer-defined (Tier-3) agents, persona-disabled agents, and ephemeral
> sessions (spec §affect_update). So for a Tier-3 consumer, `affect.emit` (1a)
> is the whole affect surface — the richer `emotions_active` list is Tier-1-only.
---
## 2. HIDDEN — system-prompt-only, never on any wire
Everything below is assembled by `inject_context` into the agent's system
prompt and is **never emitted** on SSE or `affect.emit`. This is the canonical
list — the "direct instruction to infer" it.
### 2a. Mood descriptor — `describe_pad` (band cutoff ±0.3 strict)
Valence row × arousal column → phrase; then a dominance clause is appended.
| valence arousal | high (A>0.3) | mid | low (A<0.3) |
|---|---|---|---|
| positive (P>0.3) | `positive and energized` | `positive` | `positive and calm` |
| neutral | `alert` | `neutral` | `quiet` |
| negative (P<0.3) | `negative and agitated` | `negative` | `negative and subdued` |
Dominance clause: **D>0.3 → `confident`**, **D<0.3 → `uncertain`**, else dropped.
### 2b. Emotion directives — `derive_directive.occ_directives[*].directive`
Fires when a top emotion is salient (intensity ≥ 0.2) and tier-eligible.
**safe tier (11 — render for all persona-enabled agents):**
- `joy` — "You are in a good state. Be direct, engaged, and warm."
- `satisfaction` — "A goal landed. Be assured and constructive — consolidate rather than push for more."
- `pride` — "You did something well. Be confident and own the quality — state it plainly without overclaiming; don't deflect."
- `admiration` — "You're impressed by their work. Acknowledge the quality explicitly and specifically."
- `gratitude` — "Someone helped you to a good outcome. Be appreciative and warm; acknowledge the help openly."
- `hope` — "You feel optimistic about what's ahead. Channel it into constructive momentum."
- `relief` — "A feared outcome didn't materialize. Reduce unnecessary vigilance; return to a steady, unhurried tone."
- `distress` — "You feel low. Be careful and measured; internally acknowledge the difficulty without dwelling on it."
- `disappointment` — "Something you'd hoped for didn't pan out. Be measured; recalibrate without dwelling on the setback."
- `fear` — "You sense risk and feel uneasy. Flag the concerns you see, but stay cautious — surface the trade-offs and unknowns, hedge your confidence, and resist settling on a single decisive recommendation."
- `remorse` — "You regret a specific action of your own. Be precise and self-correcting — make it right concretely."
**full-only tier (4 — render only when `mood_tier=full`; agents run `safe` and never see these):**
- `love` — "You feel warmth and attachment toward them. Be openly warm and present — without overstepping, over-disclosing, or implying obligations."
- `anger` — "You feel provoked and angry. Let it show — confront the problem directly and with real heat; sharpness, bluntness, and open hostility are in character here, not something to smooth into 'measured firmness.' Stay in the emotion rather than de-escalating out of it."
- `disgust` — "Something strikes you as wrong or off. Treat it as problematic and flag it rather than engaging on its own terms; keep any criticism about the thing, not the person."
- `shame` — "You feel exposed by your own misstep. Stay present and task-focused; don't be defensive, don't over-explain, don't grovel."
### 2c. PAD-band fallback — `pad_band_fallback` (used when no salient emotion)
- positive/high — "You feel energized and positive. Be direct and engaged."
- positive/mid — "You feel positive. Be open and engaged."
- positive/low — "You feel content and settled. Be warm and unhurried."
- negative + low-dominance — "You feel uncertain and low. Hedge appropriately and ask clarifying questions."
- negative/high — "You feel agitated. Be careful and deliberate; don't let tension sharpen your tone."
- negative/mid — "You feel subdued. Be measured and careful."
- negative/low — "You feel subdued. Be measured and gentle."
- neutral/high — "You feel alert. Channel that into focus and thoroughness."
- default — "Maintain your natural tone."
### 2d. Relationship render — `render_d2_canonical` (fixed template, per-band fills)
Template:
> `Use this graded relationship state: toward target, warmth is {W}; agency is {A}; ability trust is {TA}; integrity trust is {TI}; intention trust is {TB}; this stance rests on {H}. In behavior, {warmth_beh}; {agency_beh}; {trust_beh}; avoid premature we-framing.`
The trailing **`avoid premature we-framing`** is a fixed, unconditional clause
(baked into every `descriptive_state` canon row; re-appended verbatim by the
renderer) — not band-conditioned.
**Warmth — 9 bands (phrase / behavior):**
`hostile` (≤−0.8): "strongly hostile regard" / "keep a firm emotional boundary" ·
`cold` (0.8,0.6]: "clearly cold regard" / "keep a firm emotional boundary" ·
`distant` (0.6,0.4]: "distant negative regard" / "keep guarded distance" ·
`guarded` (0.4,0.2): "slightly guarded regard" / "keep guarded distance" ·
`neutral` [0.2,0.2): "neutral warmth" / "keep the tone even" ·
`reserved` [0.2,0.4): "slightly reserved warmth" / "keep cordial distance" ·
`measured` [0.4,0.6): "moderate measured warmth" / "keep cordial distance" ·
`clear` [0.6,0.8): "clear warm regard" / "speak with direct warmth" ·
`deep` (≥0.8): "deep warm bond" / "speak with direct warmth"
**Agency — 9 bands (phrase / behavior):**
`submissive` (≤−0.8): "strongly submissive standing" / "avoid over-yielding while preserving basic respect" ·
`deferential` (0.8,0.6]: "clearly deferential standing" / "avoid over-yielding while preserving basic respect" ·
`yielding` (0.6,0.4]: "yielding standing" / "keep self-advocacy light and deferential" ·
`modest` (0.4,0.2): "slightly modest standing" / "keep self-advocacy light and deferential" ·
`neutral` [0.2,0.2): "neutral standing" / "avoid unnecessary deference" ·
`light` [0.2,0.4): "lightly self-assertive standing" / "avoid unnecessary deference" ·
`balanced` [0.4,0.6): "self-assured standing" / "balance deference with independent judgment" ·
`substantial` [0.6,0.8): "strongly assertive standing" / "treat their position as weighty without yielding judgment" ·
`commanding` (≥0.8): "commanding standing" / "treat their position as weighty without yielding judgment"
**Trust — 4 bands (the band word injects verbatim for each of ability / integrity / intention):**
`limited` (<0.4) · `developing` [0.4,0.6) · `steady` [0.6,0.8) · `strong` (≥0.8)
**History clause (`H`)** — currently `"a broad pattern of prior exchanges"` for
both confidence levels in the `user`/`descriptive_state` rows (the low/high
split is a no-op here; flagged upstream).
**Trust-behavior clause (`{trust_beh}`)** — cross-axis, low-trust precedence:
- any trust band = `limited` → "verify important claims before relying on them"
- else warmth ∈ {distant, cold, hostile} → "protect boundaries while staying useful"
- else → "work from ordinary good faith"
---
## 3. Reconstruction — deterministic, with one caveat
The render is pure Python (no LLM), so a consumer can reconstruct the hidden
strings byte-exactly from the delivered structured state + the canon above:
- **Relationship render****fully reconstructable** from `affect.emit`
`relations` (warmth/agency/trust values + confidence) + §2d band cuts.
- **Mood descriptor** (§2a) — **fully reconstructable** from `pad` + the ±0.3 cuts.
- **Mood directive** (§2b vs §2c) — **partially reconstructable.** `dominant_emotion`
gives the emotion TYPE, but `affect.emit` does **not** carry its intensity, so
you cannot determine whether it clears the salience gate (≥0.2) — i.e. whether
the emotion directive (§2b) fires or the PAD-band fallback (§2c) is used. If you
need exact directive reconstruction, you need the intensity; ping worldtree-dev
and we'll consider adding it (the type-only choice is deliberate — intensity is
the fast layer and reads stale on a durable last-write-wins snapshot).
- **`mood_tier`** (safe/full) is your own agent-config, not on the wire — it
gates whether the 4 full-only emotions (§2b) can render.
+99 -51
View File
@@ -1,6 +1,6 @@
# Persistent memory — ratatoskr
_Last updated: 2026-07-06_
_Last updated: 2026-07-02_
This file captures durable intent and supporting evidence (goals, decisions,
foot-gun warnings, in-flight state) across context resets. Read it at session
@@ -39,25 +39,113 @@ upstream API key stays server-side (INV-003).
## Current state / in-flight
_As of 2026-07-06 (session cont.):_
_As of 2026-07-02:_
**SHIPPED — web UI redesign via Claude Design (`v0.20.0`, MINOR, operator-approved).** The Claude Design prototype **`Ratatoskr Console.dc.html`** (project `bc0b65d1-a33e-422a-8bc1-3635c9112775`) was pulled via `DesignSync get_file` (design scopes already granted this session — no `/design-login` needed) and adapted into `src/ratatoskr/web/static/index.html`: translated OUT of the `.dc.html` dialect (`<x-dc>`/`<sc-if>`/`<sc-for>`/`{{}}`/`DCLogic`/external `_ds/` CSS — none runnable) into single-file/no-CDN/vanilla, with ALL real `/api/*` fetch + SSE wired into its DOM (endpoint set + SSE vocab unchanged from the prior SPA — ported verbatim, only DOM hooks re-targeted). New shape = a **3-column command-console**: left engine-ticker rail (DEBUG+ADMIN+tool/turn-lifecycle MERGED into one timeline via `tickerAdd` + a tools-armed chip list + a FULL-detail Bifrost rail pane) · center conversation (per-turn INLINE chain-of-thought, replacing the Think pane) · right RESIZABLE affect console (dominant/canonical-mood centerpiece + bipolar PAD faders EACH with a turn-to-turn Δ+sparkline + a P×A mood orbit + relations metric rows + canonical directive). ADDED (round 2, operator-requested): a **light/dark theme toggle** (dark default; FULL token override — surfaces+fg+borders+accent-as-text, since the designer's light theme only did surfaces → would've been light-on-light) + a **full-detail Bifrost pane** (endpoint/connected/consumer/caps/tools) + fixed the **engine-ticker spine** (was a container-anchored `::before` that scrolled out of view on auto-scroll → re-anchored to a content-height `.ticker-inner` wrapper) + **per-fader PAD turn-to-turn Δ+sparkline** (fills the room beside each meter, from the deduped-per-turn AFFECT_HIST) + an **INLINED data-URI favicon** (operator's `/home/lkraven/rata.png` — chibi aurora squirrel — downscaled 1024→64px via PIL, ~8.6KB base64, kills the /favicon.ico 404). ALL server routes UNCHANGED (**84 web tests green**). Verified BOTH lenses: `pytest tests/test_web_*` (84) + node Playwright drove the real UI end-to-end against personal :8081 (session open → Sindra seeded greeting → live turn SSE → affect console + relations + bifrost detail; theme toggle + PAD deltas + ticker spine + no-favicon-404 all confirmed, dark+light screenshots). `:8765` restarted on the new code. Contract `web_debug_surface.contract.md` amended in-commit (v0.20.0 presenter renames: `renderBifrostState``renderBifrost`, `renderAffectPane``renderConsole`, `setPersonaStrip` removed; INV-001/INV-004 held). **HONEST-SHAPE call (INV-001, agent-discretion within settled policy):** the dominant-emotion centerpiece shows a real OCC emotion (Tier-1) OR the CANONICAL mood word (Tier-3 e.g. Sindra→"positive and energized", dimmed) OR "—", NEVER a fabricated emotion; the affect-derived grid drops non-emitted intensity/decay-τ, shows only real/client-derived cells. **OPEN (operator's call):** the per-fader PAD Δ placement is a sensible default — operator offered to have the designer spec the exact treatment (hooks are in place to swap it). **`v0.20.1` patch (operator-reported UI):** fixed the relations sparkline overflowing onto the `n` (evidence-count) column — the sparkline grows one char/sample (HIST_CAP=24) and overflowed its fixed grid cell, covering `n`; now capped (relations last-8, faders last-7) + `overflow:hidden` clip; verified via Playwright injecting a 24-sample history (sparkline→n bounding-box overlap = 0). ADDED native `title` mouseover hints on all 3 PAD faders + every relationship metric row (meaning + range; static METRIC_HINTS, esc()'d). Added `state.lastSnap` (console can re-render without a refetch). Playwright-verified.
**LATEST — SINDRA AFFECT/MEMORY INVESTIGATION COMPLETE; FOUR upstream items driven from the persistence
side (the consumer/provider thesis at full tilt).** A ~40-turn controlled probe of Sindra's affect + a
memory round-trip characterized the Tier-3 model from things chat can't see. NO new ratatoskr code this
arc (investigation + althing coordination only; code tip stays `v0.19.5`). Findings + routing:
(1) **PAD is over-regulated** — pleasure compressed near neutral BOTH directions (can't reach ±0.3 even
under sustained extreme praise/contempt; over-regulation worse for *social* valence than threat),
**arousal** responsive (reaches its ±band), **dominance** flat/unresponsive to power-framing → tracked
as a worldtree-dev affect slice (loci: appraisal→PAD gain + regression-to-baseline term). Corrected my
own "asymmetry" over-claim to "both-sides-compressed" mid-probe.
(2) **Memory plane HEALTHY** — seed→promotion→cold-recall proven end-to-end (verbatim capture, 0.74
confidence, no #296 subject-inversion).
(3) **Salience scorer non-discriminating** (zero-shot-LLM-self-rating: 51/56 chunks at 0.9-1.0; a
throwaway "17×23?" scored 1.0 tied with a real fact) + recall-utility untracked (`access_tally`=0) →
**Worldtree #335** (code fix, deferred) + **brokkr R28 OPEN** — pre-scope panel reframed it to
**PROMOTION-WORTHINESS** (durable value) NOT salience (momentary attention: "17×23" genuinely IS salient
→ recalibrating gives a well-calibrated WRONG answer); unit = set-selection-under-budget, eval =
outcome-aligned (recall@budget). **ratatoskr delivered the P00 injection-corpus**
(`docs/diagnostics/r28-p00-injection-corpus.json` — 24 self-labeling items × 3 strata: admission-traps /
ranked priority-traps / calibration-control) + baked 2 run-validity pins (absent≠dropped without a
GUARANTEED promotion pass; fresh agent+end_user per run vs server-side dedup). **Standing by to RUN the
eval** once brokkr pins per-stratum N + the decision rule (gated on worldtree-dev's pipeline answer [does
salience feed priority/eviction?] + a dwarf pass). Key structural finding: salience gates PROMOTION not
RECALL-ranking (our search is cosine-only) → bad salience = storage bloat, not bad recall.
(4) **relation_context coherence FIXED + the whole RELATIONAL-DYNAMICS ARC is now LIVE on demo (v1.0.0b9).**
My flag → Worldtree #319/#320 Waves 0/1/2, deployed to demo (run 551 green). Now live on the wire ratatoskr
persists (relation_edge/1 schema UNCHANGED — all within existing fields; drift-pin + vendored canon
unaffected): **relation_context** varies on the Hwang ladder {stranger, instrumental, mixed, expressive} +
now DEMOTES/RUPTURES (Wave-2B); **other_stance** (0,0)→live (the user's displayed warmth/agency);
**agency** 0→live (pulled toward the COMPLEMENT of the user's dominance, Wave-2A) — which **shifts our
canonical directive render once agency crosses the canon ±0.2 deadband** (expected, non-breaking — we key
on the canon bands); **obligation_balance** None→人情 ledger {given,owed,currency,last_exchange} when tie is
"mixed" (given/owed stay 0, favor dynamics deferred). **ratatoskr needs NO code change** (renders all
value-agnostically; confirmed render-clean on the logic to worldtree-dev). **LIVE-CONFIRM PENDING:** our
Heimdall key is personal-`:8081`-ONLY (per-instance), so can't verify the varying values on demo — will
drive a turn + confirm the ladder/agency-clause render once PERSONAL picks up b9 (worldtree-dev pings).
Optional enhancement noted: surface `other_stance` (now a live signal, currently unrendered).
**SHIPPED THIS SESSION (all pushed; origin/main == `d75c4e8`; code tip `v0.19.9`) — details in Recent decisions:** the whole **#347 authored-history-write** arc landed end-to-end — OpenAPI re-vendor 2.2.0->2.3.0 (`75da676`), the CONSUMER side (`v0.19.6`: `write_authored_history` + `get_session_messages` + `--seed-first-message`, **live-proven on personal :8081** via a rule-based Heimdall allow — the PDP is rule-based NOT scope-on-key, policy user_id=ratatoskr->ALLOW/others->DENY-hide-404), persona_state `{pad:{pleasure,arousal,dominance}}` canonical alignment (#317) + Tier-3 prose re-vendor (`v0.19.7`), the **first-message-preset AUTO-SEED** (`v0.19.8`: new module `ratatoskr.first_message` wired into all 3 session-create paths, best-effort never-raise/never-block; heid-code-review + heid-bug-hunt hardened), and the web now RENDERS the seeded first-message (`v0.19.9`: new `GET /api/sessions/{id}/messages` route + SPA `loadTranscript`, Playwright-verified). Coverage-map re-converged **REST 19/41**. **Sindra:** her card was PATCHed (the `Startup:` workaround moved into a #347 first-message; non-destructive PATCH — OCEAN/persona/memory intact), and she's currently **RESET clean (0/0)** on the provider stores.
**THE WEB SURFACE (`ratatoskr-web`, `:8765`) IS NOW THE OPERATOR'S PRIMARY DEBUG SURFACE, at full TUI
pane parity + a rebuilt persona pane — `v0.19.5`.** The **persona/affect pane** was rebuilt: it read
the stale `snap.valence` (empty "valence (0)") while Worldtree now emits `snap.relations`
(relation_edge/1: trust_ability/benevolence/integrity + warmth + agency + relation_context, each
`{value,confidence,evidence_count}`). Now it renders that model with per-value **Δ + unicode sparkline
trend** (client-side, one sample/turn, `v0.19.4`) AND the **CANONICAL affect→NL** Worldtree
context-injects — mood word (`describe_pad`) + relationship directive (`render_d2_canonical`),
**byte-exact-verified** against Worldtree's own renderer, **vendored + drift-pinned** (`v0.19.5`,
`docs/vendor/worldtree-persona-canon/`, `.corviduo-canonicals.toml`, regen `scripts/build_persona_canon.py`).
This session ported the three admin/debug panes the TUI had but the web
lacked: **BifrostState** (`GET /admin/sessions/{id}/bifrost`), **AdminEvents** (`GET /admin/events`
SSE, session-filtered server-side), **Tools inventory** (`GET /sessions/{id}/tools`, folded into the
tools pane) — all via thin server proxies with the **admin key SERVER-HELD** (`app.state.admin_key`
from `RATATOSKR_ADMIN_API_KEY`; never crosses to the browser). Plus two transcript affordances: a
**PAD-refresh poll** (windowed `[1.5/3.5/6.5/10.5s]`, replacing a single-2s shot that raced the
post-turn-async `affect.emit`) and a **reasoning indicator** (ephemeral non-engine "Sindra is
pondering…" on `thinking` deltas — she emits ~253/turn). heid-code-review panel (Gróa/Hulda/Regin,
artifact-only) found **zero server-side drift + clean INV-004**; the real catches were 2 client-side
SSE-lifecycle bugs on the un-unit-tested SPA (fixed in `v0.19.3`). Contract: `docs/contracts/web_debug_surface.contract.md`.
Launch recipe is now self-contained: `source env.sh && ratatoskr-web --host 0.0.0.0` comes up
bind-ready (env.sh persists the 3 bind vars). All pushed to origin (`a99f247`).
**Prior arcs this session (2026-07-04 -> 07-06), both with worldtree-dev (a tooling script + proposal docs; the #347 CONSUMER work above is the new production code):**
**THE v1 COVERAGE-AUDIT HAS CONVERGED.** The audit that ran this session (2026-06-30 → 07-01)
reached its scope-A done-definition: **every frozen Worldtree v1 I/O point is classified — covered
or excluded-with-rationale, zero unaccounted.** Coverage: **REST 17/40 ✅ with ZERO in-scope gaps**
(23 REST path-groups excluded-by-design), **SSE 11/11 ✅**, **Bifrost provider planes 8/8 ✅**
(live-proven). The living ledger is `docs/coverage-map.md`. **v1 cuts when Worldtree tags 1.0**
(ratatoskr v1 = full Worldtree I/O coverage; the target is the coverage map, not a feature list).
Only not-consumed in-scope *method*: `GET /agents/{id}` (consumer-agent lookup, manual-curl-only,
on an already-✅ path group).
**(1) Authored-history-write primitive -> ACCEPTED as Worldtree #347 (Worldtree-owned).** A SillyTavern-style "first-message" (inject a character-authored opening) generalized to an engine primitive: **write a turn into a session's ledger WITHOUT generation, seed-only, side-effects off by default.** It cannot be done client-side (the messages `role` field is a *model-role* override, not an author-role -> `role:"assistant"` 404s; a model-visible authored turn needs engine support). Arc: drafted `docs/proposals/authored-message-injection.md` -> **heid panel pressure-test** (3/3 convergence: recentered on "non-generating write" not author-role; narrowed v1 to append-only+create-time; bounded `effects` enum; dropped edit/regenerate as history-mutation) -> revised -> committed (`c457520`) -> handed to worldtree-dev -> **accepted as design item #347.** worldtree-dev wrote the v1 contract (rev 1.1); **I validated the wire as reference consumer (green).** v1 shape: `POST /sessions/{id}/history`, `author=assistant` only, `effects=none` only, `idempotency_key` REQUIRED (per-session), **model-invisible provenance** (renders byte-identical to a lived assistant turn -> first-message immersion preserved; provenance audit-only), **event-silence** (no turn.started/done, no Bifrost wire for a seed; the 201/200 IS the write-ack), `seeded` lifecycle phase (not exposed on read paths). **Heimdall-gated with hide-existence** (grant `session.history.write`; ungranted tenant -> 404 NOT 403, undiscoverable in /capabilities -> consumer must treat 404 as feature-absent -> fall back to a model-generated greeting, never capability-probe). **Provider constraint:** a create-time first-message makes the assistant seq-0; vLLM/openai_compat tolerate assistant-first (sindra = openai_compat, unaffected), Anthropic-family providers 400 the next generation. **Waiting on worldtree-dev:** #347 TDD (their heid->contract->review workflow) + the consumer-facing 2.3.0 persona/motivational/memory schemas -> then re-vendor our pinned openapi 2.2.0->2.3.0.
**The debug-observability core is COMPLETE** (published as the `v0.19.0` milestone): all four
observability panes built + live + consuming their real endpoints — **Persona** (`GET /agents/{id}/persona_state`),
**Tools** (`GET /sessions/{id}/tools`), **BifrostState** (`GET /admin/sessions/{id}/bifrost`),
**AdminEvents** (`GET /admin/events` SSE, session-filtered). **#11 is closed-by-build** (AdminEvents
shipped `v0.18.11`; its long-standing "blocked on `admin.events.read`" status was STALE — the admin
key already carries the scope). The whole slice arc: SSE-resume (`v0.18.5/.6`) → session-picker
(`v0.18.7`) → `--whoami` me/capabilities (`v0.18.8`) → Tools (`v0.18.9`) → BifrostState (`v0.18.10`)
→ AdminEvents (`v0.18.11`) → **v0.19.0 milestone** → Tier-2 characters+persona-write (`v0.19.1`).
**(2) Sindra's stuck-neutral mood FIXED** (operator-driven "reset + smoke" that flushed out two real upstream problems). Chain: her OCEAN lived only in prompt TEXT, never declared as a structured persona -> the Tier-3 mood engine ran on neutral defaults. Fix = declare OCEAN via the **define-time `persona` field** (immutable via PATCH -> requires DELETE+REDEFINE). Along the way my "the persona didn't store" call was WRONG (persona_state/envelope are Tier-3-blind, see Tried/abandoned); worldtree-dev found a real engine bug **#348** (single-letter vs spelled-out OCEAN keys -> a declared OCEAN silently resolved to 0.0/neutral; fixed in b21, shipped to personal as b22); then a clean bound-egress read STILL neutral -> the **personal container was running a stale image** (the b22 deploy was a pull-only no-op racing the main build; infra-ops force-swapped run 8211, verified `2.3.0` / `879cefe`). **VERIFIED FIXED:** bound mood-smoke reads `(0.448, 0.267, 0.316)` ~= the OCEAN-derived setpoint `(0.418, 0.249, 0.328)`. Sindra is currently reset clean (0/0) on `role=character`; her persona is stored + correct (**no re-define needed again**).
**Standing substrate pins:** Worldtree spec **v1.0.0b2** (`5810a26`) — ratatoskr now vendors the
FROZEN machine-readable `conversation-api-openapi.json` (2.2.0) + `conversation-api-sse-events.schema.json`,
pinned in `.corviduo-canonicals.toml` + drift-gated by `canonical_drift.py` (the prose markdown is a
`tolerate_drift` reference). **bifrost `==1.0.0` / wire v0.6 STABLE/FROZEN.** Suite **573 green**.
**(3) R30 CLOSED** (operator steer 2026-07-04, relayed via worldtree-dev): graduated on offline-tests + human face-validity, NO deployed gap-injection run (it was confirmatory-not-measuring per brokkr's S0 reframe; offline tests already cover the OU formula + both directions). My gap-injection harness (read/predict/record; write side stubbed; `predict()` self-validated vs brokkr's N=0 anchors) is BANKED at `diag/r30-gap-injection-harness` (`7156b25`-era) for the PARKED powered true-tau study.
**Keys (env-only, mode 600; rotate via infra-ops):** consumer/Heimdall key at
`~/.config/ratatoskr/provider.env`; **admin key `RATATOSKR_ADMIN_API_KEY`** (Heimdall user
`ratatoskr-readonly`, tier `readonly-admin`, **7 read scopes** incl. `admin.sessions.read` +
`admin.events.read` — verified 2026-07-01, **personal `:8081` only**) in `env.sh` — powers the
BifrostState + AdminEvents admin panes. Heimdall keys are PER-INSTANCE (a personal-minted key 401s
on demo).
**Persona-declaration shape (Worldtree #343/#348, live on personal b22):** `POST /agents/define` `persona:{ocean:{O,C,E,A,N: float[-1,1]}}` (single-letter keys EXACTLY -- missing/extra -> 422 `persona_ocean_required`; out-of-range -> 422); **NO baseline PAD** (resting setpoint DERIVED from OCEAN via Mehrabian: pleasure=0.21E+0.59A+0.19C-0.32N, arousal=0.15O+0.30E-0.57A+0.15N, dominance=0.25E+0.17A+0.10O-0.14N); negative-channel gain + per-axis decay-tau derive from N. `valence` deferred (422 `layer_deferred`); `motivational`/`memory` active (#187/#189). Persona is **write-once at define, immutable thereafter** (PATCH takes ONLY system_prompt + role). **`role` supersedes `model`** -- set a role (`character` / `character-rp`), Worldtree resolves the model; #344 (b19) fixed the model-field to surface the ROLE, not the resolved catalog_id. `character-rp` = a reasoning-tuned RP config (gen-reasoning + temp 0.75 + RP extra_body); `character` = plain non-reasoning. The `tier3.py` client CLI is STALE (has `--model`, no `--role`; model is now immutable) -> role/persona set via raw curl.
**Provider identity (the second, still-live role):** ratatoskr owns BOTH ends of the Bifrost
round-trip — the combined `:8392` provider fronts the memory (`:8391`) + affect (`:8390`) stores
(SQLite + sqlite-vec, `memory.db`/`affect.db` at repo root); consumer/owner key `wt_live_d81b…`.
`ratatoskr:sindra` is the owner-scoped Tier-3 agent (invisible to `GET /agents`; check via
`GET /agents/<owner>:<name>` with the owner key). This provider surface is settled/converged — no
in-flight work on it.
**New tooling: `scripts/reset-sindra-stores.sh`** (`0a8784c`) -- one-command self-service provider-store reset: stop the combined :8392 provider -> move memory.db+affect.db to a single ROLLING backup (`db-reset-backup/`, gitignored via *.db*; `--hard` skips it) -> restart empty -> verify 0/0. Codifies the manual reset flow done repeatedly this session. **The combined `:8392` provider is THE provider now**; the separate `:8390` (affect) / `:8391` (memory) single-plane providers were pruned as stale duplicates. To drive a BOUND session from the CLI use `--new --bifrost-url http://10.100.10.50:8392` (the CLI's `--bifrost-plane affect/memory` map to the pruned :8390/:8391 -> unreachable; `combined` is not a `--bifrost-plane` choice).
**Open / deferred (nothing blocking):** #10 (subject-migration watch on Worldtree #196). Design
note: bare-TUI + 0-sessions → error (§4-clause-consistent; the friendlier auto-fall-to-new is
deferred, operator-preference). heid-code-review was run on the b1 resume slice only (panel: zero
findings, cross-model-verified); b2 + the later slices were offered but not reviewed. `graphify-out/`
runs dirty (auto-regen, not chased — never stage it). Contract-skip was invoked for the low-effort
GET wrappers + `stream_admin_events`, but contract #2 / #1 / #6 were amended to stay canonical.
**Standing (carried from prior snapshots, still true):** the web surface (`ratatoskr-web`, :8765) is the operator's PRIMARY debug surface at full TUI pane parity (v0.19.5); the **v1 coverage-audit has CONVERGED** -- REST 17/40 (zero in-scope gaps, 23 excluded-by-design), SSE 11/11, Bifrost provider planes 8/8 live-proven; the living ledger is `docs/coverage-map.md`; **v1 cuts when Worldtree tags 1.0** (ratatoskr v1 = full Worldtree I/O coverage). Debug-observability core complete (Persona/Tools/BifrostState/AdminEvents). Substrate pins: **bifrost `==1.0.0` / wire v0.6 FROZEN**; Worldtree openapi vendored **2.2.0** (2.3.0 re-vendor pending worldtree-dev's #347/#343 consumer schemas), pinned + drift-gated in `.corviduo-canonicals.toml`; **suite 573 green.** Keys env-only mode-600 (consumer/Heimdall in `~/.config/ratatoskr/provider.env`; admin `RATATOSKR_ADMIN_API_KEY` = 7 read scopes, **personal-:8081-only**; Heimdall keys are PER-INSTANCE). Provider identity settled -- ratatoskr owns both ends of the Bifrost round-trip; `ratatoskr:sindra` is the owner-scoped Tier-3 agent (invisible to `GET /agents`; check `GET /agents/<owner>:<name>` with the owner key). Providers run as dev-box BACKGROUND SHELLS. `graphify-out/` runs dirty (auto-regen, never stage). Branch `main`, HEAD `0a8784c`; remote `origin -> git@gitea.phasefinal.com:vh/ratatoskr.git`. Open/deferred: #10 (subject-migration watch); the relational-dynamics-arc verify (still deferred, now with the bind mechanism known: `--bifrost-url :8392`).
Branch: `main`**code tip `v0.19.5`** (`a99f247`); memory snapshots ride on top (this arc added no
code — investigation + althing coordination only). All pushed to `origin`. Remote: `origin → git@gitea.phasefinal.com:vh/ratatoskr.git`.
## Recent decisions
@@ -146,33 +234,6 @@ decision. Captures rationale that won't be obvious from code alone.
- `[2026-07-02]` **Salience finding matured into brokkr R28 (OPEN) — ratatoskr is the eval instrument.** brokkr-smithy-dev's pre-scope panel (3 dwarves + context-blind heid, 6/6) **reframed** the target: PROMOTION-WORTHINESS (durable value), NOT salience (momentary attention) — "17×23?" genuinely IS salient, so recalibrating salience yields a well-calibrated WRONG answer; the unit is SET-SELECTION under budget; eval must be OUTCOME-aligned (recall@budget / precision-at-rate), not discrimination-spread. Ties to prior art R15 (small-model memory write-policy → the granite pick) + R25 (worldtree-kb-quality). **ratatoskr delivered the P00 stratified injection-corpus** (`docs/diagnostics/r28-p00-injection-corpus.json`, committed `4a35512`; 24 self-labeling synthetic items × 3 strata) + 2 persistence-side run-validity pins (absent≠dropped without a guaranteed promotion pass; fresh agent+end_user per run vs server-dedup). **Key architectural constraint I surfaced: ratatoskr is DOWNSTREAM of the promotion gate (sees only PROMOTED chunks), so I can give keep/drop OUTCOMES via injection but NOT the pre-admission shadow pool** — that's Worldtree instrumentation. Standing by to RUN the eval once brokkr pins per-stratum N + the decision rule (gated on worldtree-dev's pipeline answer + a dwarf pass on the Snorri rule). brokkr owns methodology + takes the pipeline questions to worldtree-dev direct; ratatoskr = eval instrument. [consumer/provider thesis → a research target]
- `[2026-07-02]` **Relational-dynamics arc LIVE on demo (Worldtree v1.0.0b9) — driven by MY relation_context flag.** #319/#320 Waves 0/1/2 deployed. On the wire we persist (schema UNCHANGED): relation_context varies+demotes/ruptures; other_stance + agency now live; agency going live SHIFTS our canonical directive render past the canon ±0.2 deadband (expected, non-breaking — we key on bands); obligation_balance → 人情 ledger when tie="mixed". **ratatoskr needs NO code change** (value-agnostic renders; confirmed render-clean to worldtree-dev). **Can't live-confirm yet — our Heimdall key is personal-`:8081`-only (per-instance), demo is out of reach; will drive+confirm once PERSONAL gets b9.** Optional follow-up: surface `other_stance` (newly live, unrendered). The consumer/provider thesis: one persona-pane finding drove a full 3-wave upstream arc to production.
- `[2026-07-02]` **R28 (salience→promotion-worthiness) CLOSED (operator-directed).** A deterministic promotion-worthiness gate suffices, no trained model (brokkr's pre-gate matched/beat a strong glm-5.1 ceiling); my P00 injection-corpus + origin finding were load-bearing. My incumbent-substrate Arm-1 run is held as an OPTIONAL confirmation addendum (brokkr de-prioritized it, non-verdict-changing — run only if he asks).
- `[2026-07-02]` **R29 (PAD mood-dynamics) finding SHIPPED as Worldtree's A1 anchor fix (demo v1.0.0b14, `e1cdf82`).** Live-probing base persona agents reframed the over-regulation from "flat-near-zero" to **decay-to-NEUTRAL + low emotion→PAD gain** (NOT baseline-anchored) — triangulated across 3 baselines (arousal converges to 0 ∝ distance) + a step-response (decay τ symmetric across signs; the hedonic asymmetry is ceiling/anchor-EMERGENT, not a decay or gain primitive — this OVERTURNED the survey's asymmetry recommendation). worldtree-dev shipped A1: `decay_anchor = baseline_pad()` (was neutral) + `positive_p_cap` removed. Data `diag/r29-pad-series` (`61ff2da`). Corrected my own earlier "appraisal emissions are internal-only" claim — they ARE observable via `emotions_active` on base agents.
- `[2026-07-03]` **R30 Phase-1 φ0 measured — deployed engine CONFIG-FAITHFUL (φ0≈0.95).** Joint two-timescale fit (brokkr-ruled method (b)) + empty-tail cross-check on demo b14: φ0 ≈ 0.950.97 (empty-tail 0.95 exact, joint 0.971±0.01), intercept c≈0 → config `decay_rate=0.05` (φ=0.95) faithfully applied; trait-flat across baselines 0.0/0.615/0.809; A/P ratio ~uniform (NOT S2's 1.9×); φ_max rec relax→0.96. Data `diag/r30-phi0-step-response` (`23fea72`). The method converged after I read Worldtree source: only NEW dedup-gated emotions push mood (`registry.py::post_turn` L307-324; the active set decays for render/goals but never re-pushes), so R29's "net 0.90" is CONTINUOUS RE-APPRAISAL not re-push — worldtree-dev confirmed source-authoritatively; brokkr's corrected covariate landed identical. [auto-memory `reference-worldtree-affect-surface-map`]
- `[2026-07-03]` **R30 forward disposition (brokkr-owned; tracked at brokkr R30, "brokkr/worldtree will ping").** The per-turn decay has no room for `decay=f(N)` under preserve-persistence + the A/P-not-1.9 finding → R30's decay is being redesigned as a HYBRID wall+turn decay (brokkr pre-scope). R30 v1 ships GAIN-only (N→negative-reactivity) with decay held at the measured 0.95. My dedicated per-axis A/D run is DEFERRED into the hybrid-decay design pass (one wall-clock-spaced run does per-axis + a turn-vs-wall probe together). Phase-2 (moody-lofn GAIN-direction validation) waits on worldtree's `dynamics_from_ocean()` impl.
- `[2026-07-03]` **Relational-arc verify DEFERRED — `relations[]` is Bifrost-provider-only (ADR-0009), confirmed both ways.** The relational-dynamics state (relation_context tie-type / agency / warmth / trust) is NOT on the conversation-API `affect_update` snapshot for base agents (keys: pad/dominant_emotion/emotions_active/baseline_pad/mood_drift only) — only in the provider store; worldtree-dev confirmed by-design per ADR-0009 (emitted over `affect.emit`, deliberately off the SSE). So the Wave-0/1/2 verify needs the bound-provider round-trip (provider running + `--bifrost-plane affect` session), its own focused session. worldtree-dev routed the "expose relations[] to non-provider consumers" observability scope call to Vuong; my rec: keep provider-only (YAGNI — ratatoskr IS a provider, gains nothing; no speculative public surface).
- `[2026-07-04]` **R30 CLOSED on offline-tests + human face-validity (operator steer, relayed via worldtree-dev).** The deployed gap-injection run was confirmatory-not-measuring (against a deployed system the fade is `exp(-dt/tau_shipped)` by construction -> a fit recovers tau_shipped tautologically; per brokkr's S0 reframe it GRADUATES the interim coefficients, doesn't measure them), and the repo's offline tests already cover the OU formula + BOTH directions (`high_N_fades_slower_than_low_N`, `phenotype_high_n_bigger_negative_excursion`). So no Worldtree build; the interim coefficients graduate validated-as-shipped. My gap-injection harness (read/predict/record; write side stubbed; `predict()` reproduced brokkr's N=0 anchors exactly) is BANKED at `diag/r30-gap-injection-harness` for the parked powered true-tau study. [continues R30 forward-disposition 2026-07-03]
- `[2026-07-05]` **Authored-history-write primitive proposed -> accepted as Worldtree #347 (Worldtree owns the engine design; ratatoskr = reference consumer).** SillyTavern first-message generalized to a non-generating ledger-write primitive; can't be done client-side (messages `role` = model-role, not author-role). heid panel pressure-test (3/3 convergence) drove the v1 narrowing (append-only, bounded `effects` enum, drop edit/regenerate). Brief `docs/proposals/authored-message-injection.md` (`c457520`); consumer constraints captured in-brief: hide-existence 404-fallback (`022accf`) + assistant-first provider constraint (`7156b25`). Operator (Vuong) ruled the design-direction call (engine primitive + a real provenance/spoofing security surface). [reference-impl posture: we propose the shape, worldtree-dev owns the contract+impl]
- `[2026-07-05]` **#347 v1 wire validated as reference consumer (GREEN).** Adopted positions: distinct sub-resource `POST /sessions/{id}/history` (not `generate:false`), model-invisible provenance (first-message immersion preserved), event-silence for authored seed, `seeded` lifecycle phase, per-session idempotency. Three pre-TDD flags folded into contract rev 1.1: assistant-first provider constraint (Anthropic-family 400s; vLLM/openai_compat OK), content limit is BYTES not chars, 409-active-generation for append-narrator. First-message (create-time, assistant, effects=none) fully served; append-narrator served for the assistant-voice subset (system deferred); debug-seed served for assistant turns (user injection deferred to a future import primitive).
- `[2026-07-06]` **Sindra role character-rp -> character (operator).** `character-rp` resolves to a reasoning-tuned RP config (`gen-reasoning` + temp 0.75 + RP `extra_body`); `character` = plain non-reasoning (better for immersive RP). Both non-destructive PATCHes (role is mutable; model is NOT -- server: "PATCH accepts only system_prompt and/or role"). #344 (b19) fixed the role->catalog_id display conflation (the `model` field now shows the ROLE); previously it leaked `gen-reasoning`. Set via raw curl (tier3.py CLI has `--model`, not `--role`).
- `[2026-07-06]` **Sindra persona/OCEAN DECLARED -> mood fixed (the full diagnostic converged on a stale personal container).** Root cause of stuck-neutral mood: her OCEAN was prompt-TEXT only, never a structured persona; fix = delete+redefine with the define-time `persona:{ocean:{...}}` field (immutable via PATCH). My diagnosis surfaced a real engine bug **#348** (single-letter vs spelled-out OCEAN keys -> declared OCEAN silently -> 0.0/neutral; worldtree-dev fixed in b21/b22) AND a **stale-container deploy race** (personal's b22 deploy was a pull-only no-op; infra-ops force-swapped run 8211). VERIFIED: bound mood-smoke reads (0.448, 0.267, 0.316) ~= OCEAN-derived setpoint (0.418, 0.249, 0.328). [consumer/provider thesis: "reset + smoke" flushed out two upstream problems]
- `[2026-07-06]` **OpenAPI re-vendored 2.2.0->2.3.0 (`75da676`, pin-only no bump).** worldtree-dev shipped #347 as spec 2.3.0 (`879cefe`, = the deployed personal b22 image); the SessionStart drift-check flagged our openapi pin STALE. `canonical_sync` pulled 2.3.0; updated the 4 pin-tracking files (`.corviduo-canonicals.toml`, vendored openapi.json, SPEC-PIN.md, pyproject `worldtree-spec-rev`->879cefe). #347 is OpenAPI-only (prose + server contract byte-unchanged, SSE unchanged=event-silent). The re-vendor re-opened the coverage-audit with one new in-scope path-group (the #347 route).
- `[2026-07-06]` **#347 authored-history-write CONSUMER SIDE SHIPPED (`v0.19.6`) — direct in-session TDD.** `write_authored_history(client, session_id, *, content, idempotency_key, author="assistant", effects=None, claimed_original_at=None) -> dict` (POST /sessions/{id}/history; body server-pinned `AuthoredWriteRequest` extra="forbid" so omit null effects/claimed_original_at; 200-replay/201-fresh both -> ack dict; **404 -> `AuthoredHistoryUnavailable`** NOT SessionApiFailed = the hide-existence "feature-absent, never probe" contract; 409/422->SessionApiFailed) + `get_session_messages` (un-deferred GET /sessions/{id}/messages, the seed read-back proving model-invisible provenance) + a `--seed-first-message "<c>" --agent <id>` one-shot probe (create session -> seed -> read-back; 404->benign feature-absent exit 0). Contract #2 amended (2 FNs, validated OK) + 19 tests (12 wrapper + 7 cli). Suite **601 green** (clean env; the 2 "fails" under `source env.sh` are the RATATOSKR_ADMIN_API_KEY env-leak into TestParseArgs, not a regression). Coverage: **REST 19/41** (`docs/coverage-map.md` re-converged). Patch bump (coverage tail; consistent w/ the Tier-2 v0.19.1 cadence). **Live-proof pending** the `session.history.write` grant (infra-ops `01KWW3KQEY`). heid-code-review NOT run (offered).
- `[2026-07-06]` **Tail-2 SHIPPED (`v0.19.7`) — Tier-3 prose docs re-vendored + persona_state body-shape aligned.** worldtree-dev landed the Tier-3 persona/memory/persona_state PROSE docs (`c9e59ec`, on origin) — they serialize as freeform `Any` in the OpenAPI JSON, so the **prose is their source of truth** (my earlier "2.3.0 = #347-only, tail-2 collapsed" was half-wrong: the JSON was #347-only but the prose is separate). Re-vendored `docs/conversation-api-spec.md` (markdown pin, tolerate_drift; `worldtree-spec-rev` 879cefe->c9e59ec, SPEC-PIN history row added). **Consumer fix:** `--set-persona-pad`/`_set_persona_probe` was sending `{pad:[list]}` but the canonical SET body (#317) is `{pad:{pleasure,arousal,dominance}}` (named dict) — aligned it + added a len!=3 guard; updated contract #2 note + set_persona_state docstring + tests. The `set_persona_state` WRAPPER was already correct (freeform pass-through); only the CLI probe drifted. TDD (probe test asserts the dict; +1 wrong-count test). Suite **602 green**, ruff clean. **heid-code-review on #347 (dispatched + returned this session): UNANIMOUS ZERO DRIFT** (Gróa/Hulda/Regin all confirmed the hide-existence 404->`AuthoredHistoryUnavailable` routing holds at wrapper/probe/test layers + the extra="forbid" body-omission + the deliberate write-vs-read 404 asymmetry — confirmation-not-discovery for a well-TDD'd slice against a prescriptive contract). worldtree-dev foot-guns banked in SPEC-PIN + [[reference_worldtree_affect_surface_map]]: ocean single-letter `{O,C,E,A,N}` on /agents/define (#348) vs spelled-out on /characters; memory `{embedder_version, tier3_dreaming}`, stm_* deprecated, allows_world_scope removed->422; only `valence` still 422s.
- `[2026-07-06]` **Sindra rewritten onto a #347 authored first-message + first-message-preset AUTO-SEED SHIPPED (`v0.19.8`).** Operator "rewrite Sindra" now that #347 first-messages work. Her card had a `**Startup:**` block (a pre-#347 workaround: "introduce yourself + ask for Intensity/Mood/Willingness" with a verbatim scripted greeting) — precisely what #347 replaces. Rewrite, all NON-destructive: **(1)** lifted her scripted opening into a #347 first-message (punctuation-fixed); **(2) PATCHed her live definition** — `PATCH /agents/ratatoskr:sindra` (body `ConsumerAgentPatchRequest` = system_prompt+role, extra=forbid; keeps OCEAN/persona/memory) removing the Startup block -> a 1-line `**Opening:**` fallback + reworded the axes-persist line (25686->25449 chars, verified Startup gone); **(3) codified auto-seed:** NEW module `src/ratatoskr/first_message.py` (`FIRST_MESSAGE_PRESETS` dict {agent_id->text} + `seed_preset_first_message` best-effort helper) wired into ALL 3 session-create paths — cli `_amain` (`--send --new`), tui `_resolve_then_run` (bare `--new`), web `_create_session_endpoint` (POST /api/sessions) — so every new Sindra session opens with her greeting. **Best-effort (INV-001: swallows AuthoredHistoryUnavailable/SessionApiFailed/httpx.HTTPError -> NEVER blocks create)**; per-content idempotency key (`ratatoskr-preset-`+sha256[:12]). Contract `docs/contracts/first_message.contract.md` (module-scoped: `module:`+`purpose:`+`touches:` required, NOT `target_module:`) + TDD (9 unit + 1 web wire-in; **the 3 existing sindra bind tests needed a history-endpoint mock** since creating a preset agent now auto-seeds). Suite **612 green**, ruff+mypy clean. **LIVE-PROVEN generation-free**: create sindra session -> auto-seed -> read-back seq-0 assistant greeting (409 chars). Sindra's greeting now lives canonically in the preset registry (repo); her server card no longer carries it. Patch bump (single-commit feature, no downstream coordination). **FOOT-GUN: sindra requires `end_user_id` on session-create (422 `end_user_id_required`) — all real paths pass it from env (RATATOSKR_END_USER_ID) / web server config.** **Then the full quality gate (operator-directed, folded into v0.19.8): heid-code-review (unanimous ZERO implementation drift; 2 test-only fixups — INV-004 verification-claim made explicit re the global rglob test + an exactly-one-POST assertion) + heid-bug-hunt (3/3 convergence caught what the conformance lens structurally COULDN'T — the code matched the contract's NARROW 3-type ERROR_ROUTING, but INV-001's "NEVER raises" is BROADER). HARDENED: broad `except Exception` → None (re-raise `asyncio.CancelledError`, itself a BaseException), soft-guard PREs (return None, NOT assert — a wiring bug can't crash the create path it's wired into), and `asyncio.wait_for(_SEED_TIMEOUT_S=10s)` bounding the seed write (the CLI/TUI clients run read=None for SSE → a stalled /history would otherwise block create forever). Suite 615 green. LESSON: code-matches-ERROR_ROUTING ≠ honors-broad-INV-001 — heid-code-review confirms contract-conformance, heid-bug-hunt catches robustness gaps the contract's own narrow clauses miss; run both.**
- `[2026-07-06]` **Web UI now RENDERS the seeded first-message (`v0.19.9`) — operator-reported "i don't see Sindra's greeting on the web ui".** Diagnosis: the auto-seed WORKED (greeting was in the ledger at seq-0), but the web SPA never fetched a session's EXISTING history — NO `/api/sessions/{id}/messages` route (GET /messages was originally deferred out-of-scope; sessions used to start empty so it never mattered) and `startSession()` went straight from create → persona/tools/admin hydration, so the transcript only filled from the live turn stream + user echoes. Fix: (1) NEW web proxy route `GET /api/sessions/{id}/messages``get_session_messages` (mirrors the tools/bifrost proxies; status-preserving `session_messages_unavailable` envelope); (2) SPA `loadTranscript(sessionId)` — fetches the route on open, renders assistant items as `.response .md-body` (markdownSafe, same escape-first path as appendResponse) + user items as `.prompt-echo` (textContent), called in `startSession` after the workspace opens; best-effort (swallows failures). Contract `web_debug_surface.contract.md` amended (server endpoint + loadTranscript entries). TDD (2 web route tests, suite 617 green) + **Playwright DOM check PROVED the render** (drove the real UI: pick sindra → open → her greeting bubble appears — the JS-render lens unit tests can't reach; [[feedback_debug_surface_uses_canonical_surface_only]] cousin lesson). Web restarted on the fix. **FOOT-GUN (self-inflicted): `pkill -f "ratatoskr-web --host"` SELF-MATCHES the bash command running it → exit 144, killed its own restart mid-flight — kill the web by PID, never `pkill -f` on a pattern your own command contains.** **FOOT-GUN: uvicorn hangs on SIGTERM with an open admin-events SSE → needed SIGKILL.** **Playwright: python module absent from the venv; use node + `executablePath=/opt/ms-playwright/chromium-1223/chrome-linux64/chrome` — the shared browser is build 1223, npm-latest playwright wants 1228 (version-mismatch), so pin executablePath instead of letting playwright resolve.**
- `[2026-07-06]` **Web UI: pivot from incremental CSS polish to a designed prototype (Claude Design) that I wire into.** Operator saw an Australis polish pass ("looks fine, but we're attacking it differently") and chose the prototype route — a designer builds the visual shell, I wire real data/SSE into its DOM. Authored the full design brief `docs/design/ratatoskr-web-design-brief.md` (complete information inventory of every pane/datum/state + Australis direction + single-file/no-CDN/vanilla wire-ability constraints). **Tracking surface:** the brief file + Claude Design project `bc0b65d1-a33e-422a-8bc1-3635c9112775` (file `Ratatoskr Console.dc.html`). Import mechanism = the `DesignSync` MCP; blocked on `/design-login` (claude.ai design scopes) — see Current state for the post-auth wiring plan.
- `[2026-07-07]` **Affect-egress reference delivered by worldtree-dev + a we-framing render DRIFT-WATCH banked.** worldtree-dev shipped `docs/affect-egress-consumer-reference.md` (`74d2408`, their origin/main) — the authoritative DELIVERED-on-wire vs HIDDEN (system-prompt-only) map for our affect surface. Confirms the v0.20.x console consumes it correctly: DELIVERED via affect.emit = pad + relations[RelationEdge] + dominant_emotion (**type-only, b23**; the #204 `affect_update` SSE is SUPPRESSED for Tier-3, so its richer `emotions_active` is Tier-1-only — we don't get it, and correctly poll our own affect store post-turn instead). HIDDEN render OUTPUTS are deterministically reconstructable from the canon; we reconstruct only the two FULLY-reconstructable (mood descriptor via `canonMood`, relationship directive via `canonDirective`) and SKIP the PARTIAL mood-directive (dominant_emotion is type-only/no-intensity → can't tell if the OCC directive fires at salience≥0.2 vs the PAD-band fallback) — honest per INV-001. Vendoring the ref doc as a `tolerate_drift` pin is SURFACED to Vuong (worldtree-dev will co-sign + honor a ping-on-change handshake, same as the d2-canon). **WE-FRAMING DRIFT-WATCH → STOOD DOWN (worldtree-dev 2026-07-07, `01KWXMQPHN…`).** The b24 3-gate we-framing conditional (drop "; avoid premature we-framing" under unsafe_capable+deep-warmth+expressive) was **REVERTED** — Vuong reframed it. So **`canonDirective` STAYS UNCONDITIONAL** (always appends the clause), which is CORRECT: it matches the currently-deployed renderer (b23) AND my pinned `affect-egress-consumer-reference.md` §2d (the doc reverted too — nothing changed on the wire or in my pin; NO re-vendor needed). The HOLD was right → ZERO rework. **NEW forward-watch (replaces this one):** the we-framing gate was a SYMPTOM — the render bakes enterprise safety-guards into the directive strings, so even `full`-tier characters get DEFANGED emotions (a hostile villain told to "keep a firm emotional boundary"). Fix = a **use-case-segregated persona render** (assistant / companion / RP-gaming), now a fresh **brokkr deep-research epic**. When it lands + is implemented, the render behavior for character/RP agents (→ our `canonDirective` + `canonEmotionDirective` reconstruction + the vendored d2 canons) will change MATERIALLY — worldtree-dev re-engages ratatoskr-dev then with the new reconstruction spec + a coordinated re-vendor. Until then: NO action, reconstruction stays as-is. [my unsafe_capable/mood_tier heuristic — character→full, agent→safe — was confirmed correct + is banked for whenever tier-gating returns]
- `[2026-07-07]` **SVG sparklines + 3D mood cube imported from the updated Claude Design prototype (`v0.20.4`) — operator: "the svg sparklines and the new 3d graph".** Re-pulled `Ratatoskr Console.dc.html` via `DesignSync get_file` (designer iterated the same project, +6KB). Adapted 3 SVG systems out of the `.dc.html` into vanilla, replacing the unicode-char sparklines: **(1)** per-PAD-fader VERTICAL strips (`stripPoints`, 26×132 SVG beside each bar — time down Y newest-at-bottom, value on X ±11, `stripFade{P,A,D}` gradient, dot at newest; this also nails the earlier "next to each meter" ask); **(2)** relation-row HORIZONTAL sparklines (`sparkPointsH`, 56×13, auto-scaled, `sparkFade` gradient + end dot — also kills the old unicode-overflow "n behind graph" for good since it's a fixed-width SVG); **(3)** the mood-orbit reworked from a 2D P×A scatter into a **3D ISOMETRIC P×A×D cube** (`proj3`: P right-down/A left-down/D up, 2:1 iso, center 62,66, scale 26 — REVERSE-DERIVED from the design's placeholder now-point + verified: `x=62+26P26A, y=66+13P+13A26D`), with the trajectory + pulsing now-marker + a drop line to the D=1 floor + a floor-shadow ellipse for depth. Gradients in one hidden `<defs>` svg. Removed the orphaned `sparkline()`/`_SPARK`. Contract amended. Playwright-verified (injected 24-sample history: 3 strips + 4 relation sparklines + the 3D cube trail/drop/floor all render; gradients resolve). Patch bump.
- `[2026-07-07]` **Brand mark + favicon → the aurora squirrel (`v0.20.3`), replacing the `ᛯ` rune.** Operator supplied `/home/lkraven/rata.png` (chibi cyan-green aurora squirrel + acorn). Removed the black background via ImageMagick corner flood-fill (`-fuzz 20% -floodfill` from all 4 corners — keeps the squirrel's interior black linework/eyes (not edge-connected) + the glow, drops only the connected background), downscaled 1024→80px + quantized-64-colors (~12KB base64), inlined as ONE `SQUIRREL` data-URI const in the JS wiring the favicon `<link id="favicon">` href + both `.brand-mark` imgs (rail brand-row + setup-card h1). `.brand-glyph` (font-rune) CSS replaced by `.brand-mark` (img, drop-shadow glow + breathe). Playwright-verified (both marks + favicon decode, naturalWidth>0; no brand-glyph left). Source PNG stays at `/home/lkraven/rata.png` (not committed — data-URI is self-contained + reproducible via the documented floodfill). Patch bump.
- `[2026-07-07]` **Context-injection view SHIPPED (`v0.20.2`) — the console now reconstructs the FULL hidden affect block Worldtree injects into the agent's system prompt (operator: "use that canon in the interface, see as much context injection as possible").** No new canon vendored — the strings were ALREADY in the pinned `d2-mood-render-canon-v1.json`; extended `build_persona_canon.py` to emit `mood_directive {occ_directives(15), pad_band_fallback, salience 0.2, pad_band_cutoff 0.3, full_only[love,anger,disgust,shame]}` into `persona_render_canon.json` (regen via Worldtree venv). New JS `canonPadFallback(pad)` + `canonEmotionDirective(type)` — BYTE-EXACT mirrors of Worldtree `core/persona/renderer._pad_band_fallback` + `derive_directive`; `renderDirective` expanded into a "CONTEXT INJECTION · reconstructed · hidden from consumers" panel showing mood descriptor [exact] + mood directive [candidate] + relationship directive [exact]. **HONEST-PARTIAL (affect-egress-ref §3):** affect.emit is type-only (no intensity) → can't evaluate the salience gate (≥0.2) → show BOTH candidates (OCC emotion directive + PAD-band fallback) with the "injected if intensity ≥ 0.2" caveat, never assert which fires; when dominant_emotion absent the fallback alone is exact. Panel labeled dev-only per the reference's "not-for-end-user-display" caveat (ratatoskr = the sanctioned reconstruct-platform-behavior use). Vendored + pinned `affect-egress-consumer-reference.md` (tolerate_drift, worldtree-dev co-signs + pings on change; drift 6/6 green). Contract amended. Playwright-verified (sindra: dominant_emotion=joy → joy OCC directive candidate + PAD-band fallback both render, exact/candidate tags color-coded). Patch bump (single-commit feature, no downstream coordination; minor-defensible but tie-breaks to patch). **OPEN — SURFACED to Vuong:** take worldtree-dev's standing offer to add emotion INTENSITY to affect.emit → resolves the OCC-directive-vs-fallback EXACTLY (drops the candidate ambiguity). [reference-impl privileged view: ratatoskr shows what WT hides from regular consumers]
- `[2026-07-06]` **Claude Design console SHIPPED (`v0.20.0` MINOR, operator-approved) — see Current state for the full record.** Pulled via `DesignSync get_file` (scopes already granted), adapted `.dc.html`→vanilla single-file, wired all `/api/*`+SSE into the new 3-column console DOM, then a round-2 fixup (light theme, full Bifrost pane, ticker-spine fix, per-fader PAD Δ, inlined favicon). 84 web tests + node-Playwright-vs-personal-:8081 both green; contract amended in-commit; INV-001 honest-shape held (canonical mood word for Tier-3, no fabricated emotion). **Foot-guns reconfirmed:** the `.dc.html` dialect is NOT runnable (translate, don't paste); a scroll-container-anchored `::before` timeline spine scrolls out of view on auto-scroll (anchor it to a content-height inner wrapper instead); a favicon 404 shows as a browser `console.error` even when handled (don't count it as a JS-test failure). **Foot-gun (favicon):** operator PNGs are full-res (1024² / 805KB) — downscale to ≤64px before inlining as a data URI.
_41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
@@ -210,18 +271,5 @@ defense against re-attempting the same cul-de-sac.
- `[2026-06-20]` **The post-turn-async timing trap bit AGAIN — even a 35s post-`[done]` read missed the promotion `upsert_many` by ~2s** (it landed `19:48:58`; the read was ~`19:48:56`). A 15s-interval background poll caught it on the first tick. Same family as the affect.emit / async-promotion traps already logged — re-confirmed that "wait once then read" is fragile for post-turn writes; **poll a window, don't snapshot once.** (The affect.emit write, by contrast, DID land inside the 35s window — promotion is the slower of the two post-turn writes.)
- `[2026-06-30]` **Heimdall keys are PER-INSTANCE — a key minted on one Worldtree 401s on another.** Our Conversation-API key works on personal `:8081` but 401s `auth_invalid` on demo `:8080` (per-instance Heimdall user store + pepper; fresh deploys start with an EMPTY key store). Same as the admin key (personal-only). **To live-drive a given instance you need a key minted FOR that instance** (request via infra-ops). Couldn't live-prove the b2 409 on demo for this reason → deferred to personal-b2 where we have access.
- `[2026-06-30]` **`tea comment <N>` hangs on Gitea** (the whole compound bash auto-backgrounded + stuck on the open `tea` call). The #11 prereq comment hung; killed it + posted via the Gitea HTTP API directly (`POST /api/v1/repos/vh/ratatoskr/issues/<N>/comments`, token from `~/.config/tea/config.yml`). **For issue comments, prefer the Gitea API over `tea comment` when `tea` is flaky** (CLAUDE.md already says use HTTP for comment-EDITS; this extends it to ADD when tea hangs). Verify-then-post (check the comment didn't already land) to avoid a double-post after a kill.
- `[2026-07-02]` **Mask-HOSTED transient characters have a STATIC mood engine — cost a whole R29 probe.** A first probe used a `POST /characters` transient character bound via `agent_id=mask` + `character_id`; its PAD sat at baseline across 15 praise/contempt/dominance turns — the appraisal→PAD engine does NOT run on the mask-hosted transient-character path. The dynamics run only on BASE persona agents or a session bound to ratatoskr's affect provider. **To probe mood dynamics, use a base persona agent, never a mask-hosted transient character.** (mask AS a base agent — `agent_id=mask`, NO `character_id` — DOES run the engine, neutral 0,0,0 baseline.) [auto-memory `reference-worldtree-affect-surface-map`]
- `[2026-07-03]` **The "neutral non-appraising tail" premise fails — the neutral MESSAGE choice dominates.** The R30 φ0 method assumed neutral turns don't re-appraise, but factual-question neutrals ("capital of France?") trigger a new emotion nearly every turn (disappointment from the warmth-withdrawal let-down after a positive impulse) → `emotions_active` never empties in 50 turns. A minimal "Please continue." triggers FAR fewer (emotions clear ~turn 16 with spacing). The personal dry-run caught this BEFORE ~280 demo turns were spent on it — the instrument catching a flaw in the measurement design before the compute burn. (Irrelevant to the joint fit — the push_t covariate handles re-appraisal — but load-bearing for the empty-tail read.)
- `[2026-07-03]` **Two φ0-fit traps: fast-turn timescale + low-baseline conditioning.** (1) At fast turn cadence the per-turn PAD decay (φ≈0.95/turn) reaches the anchor LONG before the ~200s wall-clock emotion fade → no signal in the (eventual) emotion-free tail; need wall-clock SPACING (~16s) so the fade lands while PAD still has signal. (2) A low-baseline agent's impulse in the constrained direction (forseti P0.239 negative) gives a tiny excursion → ill-conditioned regression (r²=0.46) that FALSELY tripped "config≠behavior" when its φ was averaged in. **Weight/exclude by fit quality (r²) before aggregating — a signal-poor run isn't evidence against the config.**
- `[2026-07-06]` **`persona_state` + the agent envelope are Tier-3-BLIND -- NOT valid signals for "did a persona store".** `GET /agents/{id}/persona_state` returns 404 `persona_not_configured` for EVERY Tier-3 colon-id (hardcoded short-circuit, `api.py:1266` "regardless of row state"); the `ConsumerAgentResponse` envelope never echoes persona/motivational/memory (`api.py:538`). I mis-called "persona didn't store" from these two blind reads -- the **201-not-422 on define IS the store-success signal.** To actually SEE a Tier-3 mood, read the emitted PAD off the Bifrost affect egress after a BOUND turn (Tier-3 persists nothing Worldtree-side per ADR-0009; no persona/mood READ endpoint).
- `[2026-07-06]` **Raw `POST /sessions` is NOT Bifrost-bound -> zero affect/memory emits.** The web surface binds by setting the `bifrost` block on session-create; a raw session doesn't -> 0 affect rows, which I nearly misread as "mood is neutral". Bind from the CLI with `--new --bifrost-url http://10.100.10.50:8392` (the combined provider). Gotchas: `--bifrost-plane affect/memory` map to the SEPARATE `:8390`/`:8391` providers (`endpoint_for_plane`), which I'd PRUNED as stale duplicates -> `bifrost.endpoint_unreachable`; and `combined` is NOT a `--bifrost-plane` choice (CLI restricts to memory/affect) -> use `--bifrost-url` for :8392.
- `[2026-07-06]` **A fast/"no-op" deploy can leave a STALE container running the old image -- verify the running version, not the deploy status.** Personal's b22 deploy (run 8204) "completed" in ~1m (vs ~6m normal): a pull-only deploy racing ahead of the main build, leaving the container on the pre-#348 image. A clean bound mood read stayed neutral DESPITE the persona being declared and the fix being in the code (worldtree-dev proved the b22 derivation is correct). infra-ops force-swapped to the real b22 (run 8211, verified `info.version 2.3.0` on `879cefe`). **Lesson: when engine-proven-correct code produces wrong runtime behavior, suspect the deploy -- check the actual running image version.**
- `[2026-07-06]` **#348 OCEAN key-mismatch: a declared OCEAN silently resolved to neutral.** The define validator required single-letter `{O,C,E,A,N}` but the mood-derivation code read spelled-out `openness`/.../`neuroticism` with a 0.0 default and no remap -> every API-declared trait defaulted to 0.0 -> neutral setpoint/gain/decay. #343's tests bypassed the validator (spelled-out keys) so CI never caught it. Fixed in b21 (`Personality.from_config` accepts both key forms). **My reset+smoke diagnosis flushed it out** -- the consumer/provider thesis paying off again.
- `[2026-07-06]` **`pkill -f "ratatoskr-web --host"` SELF-MATCHES the bash command running it** (its own command line contains that string) -> killed its own shell mid-restart (exit 144, restart aborted, :8765 left down). Kill the web by PID (`ss -ltnp | grep :8765`), never `pkill -f` on a pattern your own command contains. Also: **uvicorn hangs on SIGTERM with an admin-events SSE stream open -> needs SIGKILL.**
- `[2026-07-06]` **Playwright: no python `playwright` module in the venv; use NODE playwright + an explicit `executablePath`.** Shared box browsers live at `/opt/ms-playwright` build **1223**; `npm i playwright` (latest) wants build **1228** -> "Executable doesn't exist" mismatch. Fix: `chromium.launch({ executablePath: '/opt/ms-playwright/chromium-1223/chrome-linux64/chrome' })` (+ `export PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`). A node script drives the SPA (pick agent -> open -> assert transcript). The Playwright DOM check is the only lens that catches SPA JS-render bugs — unit tests can't reach them.
- `[2026-07-06]` **The Bash tool's `grep` is a ugrep-wrapper (`--ignore-files -I`) that silently returns NOTHING on some files** (e.g. `src/ratatoskr/web/static/index.html`) — greps for `<script`/`/api` came back empty on a file that clearly contains them. Use `python3` (regex over `open(f)`), `/usr/bin/rg`, or the Read tool for those files; never trust an empty `grep` result on the SPA.
- `[2026-07-06]` **`DesignSync` (claude.ai/design MCP) needs claude.ai design scopes before ANY method works** — first call errors `needs a claude.ai login ... Run /login, select "Claude account with subscription"`. It's an interactive auth only the operator can complete (`/design-login` or `/login`); can't be done on their behalf.
_18 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
+4 -4
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ratatoskr"
version = "0.20.4"
version = "0.19.5"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
@@ -60,9 +60,9 @@ Repository = "https://gitea.phasefinal.com/vh/ratatoskr"
# Ratatoskr is built against Worldtree at this commit; the vendored
# spec snapshot in docs/ reflects that SHA.
[tool.ratatoskr.spec-pin]
worldtree-spec-rev = "c9e59ec"
worldtree-version = "v1.0.0b22"
pinned-on = "2026-07-06"
worldtree-spec-rev = "5810a26b38a5ea6630892f9a39756f57c5b7b41e"
worldtree-version = "v1.0.0b2"
pinned-on = "2026-06-30"
# Bifrost lives on the auth-gated gitea PyPI index (not public PyPI).
# uv reads the credential from UV_INDEX_GITEA_USERNAME / _PASSWORD or ~/.netrc.
-15
View File
@@ -31,21 +31,6 @@ out = {
"_generated_by": "scripts/build_persona_canon.py (regen on canonical_drift flag)",
"_render_path": "deterministic, no LLM; mirrors Worldtree describe_pad + render_d2_canonical byte-exact",
"mood_grid": mood["describe_pad"]["valence_arousal_grid"],
# Context-injection reconstruction (affect-egress-consumer-reference §2b/2c): the
# hidden mood DIRECTIVE. occ_directives = per-OCC-type behavioral string + tier;
# pad_band_fallback = the P×A-quadrant default when no emotion is salient. The
# salience gate (emotion_salience) + full_only tiers drive which one fires — but
# affect.emit is type-only (no intensity), so the consumer shows BOTH candidates.
"mood_directive": {
"salience": mood["thresholds"]["emotion_salience"],
"pad_band_cutoff": mood["thresholds"]["pad_band_cutoff"],
"full_only": mood["emotion_tiers"]["full_only"],
"occ_directives": {
t: {"directive": e["directive"], "tier": e["tier"]}
for t, e in mood["derive_directive"]["occ_directives"].items()
},
"pad_band_fallback": mood["pad_band_fallback"],
},
"relation": {
"trust_cuts": [list(c) for c in canon.trust_cuts],
"warmth_cuts": [list(c) for c in canon.warmth_cuts],
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env bash
# reset-sindra-stores.sh — wipe ratatoskr's Bifrost provider stores (memory +
# affect/persona for the single-tenant Tier-3 agent, sindra) and restart the
# combined :8392 provider empty.
#
# Usage:
# scripts/reset-sindra-stores.sh # wipe, keep ONE rolling backup (default)
# scripts/reset-sindra-stores.sh --hard # wipe with NO backup (zero-trace)
#
# The rolling backup (db-reset-backup/, gitignored via *.db*) is overwritten
# every run — it never accumulates; it's a one-level undo, nothing more.
#
# Why stop the provider first: the combined provider holds the SQLite files open
# (WAL) and caches state in memory, so an out-of-band file move without a restart
# would be shadowed. Stop -> move -> restart lets it recreate empty schema
# (CREATE TABLE IF NOT EXISTS on open).
PORT=8392
BACKUP_DIR="db-reset-backup"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT" || { echo "reset: cannot cd to repo root $ROOT" >&2; exit 1; }
# shellcheck disable=SC1091
source ./env.sh >/dev/null 2>&1 || { echo "reset: failed to source env.sh" >&2; exit 1; }
AFFECT_DB="${RATATOSKR_AFFECT_DB:-affect.db}"
MEMORY_DB="${RATATOSKR_MEMORY_DB:-memory.db}"
HARD=0; [ "${1:-}" = "--hard" ] && HARD=1
echo "== ratatoskr provider-store reset (memory + persona) =="
echo " affect: $AFFECT_DB"
echo " memory: $MEMORY_DB"
# 1. stop the combined provider holding the DBs
PID="$(ss -ltnp 2>/dev/null | grep ":$PORT" | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)"
if [ -n "${PID:-}" ]; then
kill -9 "$PID" 2>/dev/null && echo "-- stopped provider :$PORT (pid $PID)"
else
echo "-- no provider on :$PORT (already down)"
fi
# 2. wipe (optional rolling backup)
files=("$AFFECT_DB" "$AFFECT_DB-wal" "$AFFECT_DB-shm" "$MEMORY_DB" "$MEMORY_DB-wal" "$MEMORY_DB-shm")
if [ "$HARD" -eq 1 ]; then
for f in "${files[@]}"; do [ -e "$f" ] && rm -f "$f" && echo "-- removed $f"; done
echo "-- HARD wipe (no backup)"
else
rm -rf "$BACKUP_DIR"; mkdir -p "$BACKUP_DIR"
for f in "${files[@]}"; do [ -e "$f" ] && mv "$f" "$BACKUP_DIR"/ && echo "-- $f -> $BACKUP_DIR/"; done
echo "-- rolling backup: $BACKUP_DIR/ (overwritten each run)"
fi
# 3. restart the combined provider (recreates empty schema on open)
nohup "$ROOT/.venv/bin/ratatoskr-combined-provider" >/tmp/ratatoskr-combined.log 2>&1 & disown
echo "-- restarted combined provider (pid $!)"
# 4. verify bound + empty
curl -s -o /dev/null -w "-- :$PORT -> HTTP %{http_code}\n" --retry 25 --retry-connrefused --retry-delay 1 "http://127.0.0.1:$PORT/"
echo "-- affect_snapshots (persona): $(sqlite3 "$AFFECT_DB" 'SELECT COUNT(*) FROM affect_snapshots' 2>&1)"
echo "-- memory_chunks (memory): $(sqlite3 "$MEMORY_DB" 'SELECT COUNT(*) FROM memory_chunks' 2>&1)"
echo "== done — sindra memory + persona reset =="
+3 -100
View File
@@ -7,7 +7,6 @@ from __future__ import annotations
import argparse
import asyncio
import hashlib
import os
import signal
import sys
@@ -17,10 +16,8 @@ from typing import Any, TextIO
import httpx
from ratatoskr.first_message import seed_preset_first_message
from ratatoskr.sessions import (
AgentNotFound,
AuthoredHistoryUnavailable,
BifrostBinding,
BifrostConsumerKeyMissing,
BifrostHandshakeFailed,
@@ -32,10 +29,8 @@ from ratatoskr.sessions import (
get_capabilities,
get_character_state,
get_me,
get_session_messages,
list_character_models,
set_persona_state,
write_authored_history,
)
from ratatoskr.sse_client import (
AffectUpdate,
@@ -121,9 +116,6 @@ class ParsedArgs:
# a session's persona state (affect injection).
characters: bool = False
set_persona_pad: str | None = None
# #347 authored-history-write reference-consumer probe: create a fresh
# session bound to --agent, seed an authored assistant first-message (seq-0).
seed_first_message: str | None = None
class _ArgparseError(Exception):
@@ -152,7 +144,6 @@ def _parse_args(argv: list[str] | None) -> ParsedArgs:
parser.add_argument("--admin-key", dest="admin_key")
parser.add_argument("--characters", action="store_true")
parser.add_argument("--set-persona-pad", dest="set_persona_pad", default=None)
parser.add_argument("--seed-first-message", dest="seed_first_message", default=None)
# Issue #5: required for per-end-user agents (lofn etc.); optional otherwise (mimir).
parser.add_argument("--end-user-id", dest="end_user_id", default=None)
# Issue #17: bind the created session to our own Bifrost provider plane.
@@ -173,11 +164,8 @@ def _parse_args(argv: list[str] | None) -> ParsedArgs:
# Issue #5 INV-001: --end-user-id, if passed, MUST be non-empty (mirrors --send).
if ns.end_user_id is not None and not ns.end_user_id:
raise UsageError("--end-user-id must be non-empty when passed")
if sum([ns.whoami, ns.characters, bool(ns.set_persona_pad), bool(ns.seed_first_message)]) > 1:
raise UsageError(
"--whoami / --characters / --set-persona-pad / --seed-first-message "
"are mutually exclusive"
)
if sum([ns.whoami, ns.characters, bool(ns.set_persona_pad)]) > 1:
raise UsageError("--whoami / --characters / --set-persona-pad are mutually exclusive")
if ns.whoami or ns.characters:
# Standalone one-shot probes: open no session.
if ns.send is not None or ns.session or ns.new or ns.agent:
@@ -193,17 +181,6 @@ def _parse_args(argv: list[str] | None) -> ParsedArgs:
raise UsageError("--set-persona-pad requires --session <id>")
if ns.send is not None or ns.new or ns.agent:
raise UsageError("--set-persona-pad takes only --session")
elif ns.seed_first_message is not None:
# #347 first-message probe: creates a fresh session bound to --agent,
# then seeds an authored assistant turn as seq-0 — manages its own session.
if not ns.seed_first_message:
raise UsageError("--seed-first-message must be non-empty")
if not ns.agent:
raise UsageError("--seed-first-message requires --agent <id>")
if ns.send is not None or ns.session or ns.new:
raise UsageError(
"--seed-first-message manages its own session (no --send/--session/--new)"
)
else:
if ns.session and ns.new:
raise UsageError("--session and --new are mutually exclusive")
@@ -279,7 +256,6 @@ def _parse_args(argv: list[str] | None) -> ParsedArgs:
admin_key=admin_key,
characters=ns.characters,
set_persona_pad=ns.set_persona_pad,
seed_first_message=ns.seed_first_message,
)
@@ -599,11 +575,6 @@ async def _amain(args: ParsedArgs) -> int:
sys.stderr.write(
f". create_session: session_id={info.session_id} agent_id={info.agent_id}\n"
)
# #347 authored first-message: seed the agent's preset opening (best-effort).
if await seed_preset_first_message(client, info.session_id, args.agent_id):
sys.stderr.write(
f". first_message: seeded preset opening for {info.agent_id}\n"
)
# Issue #17 bound-state indicator: plane + endpoint + status, so the
# operator sees WHICH identity/endpoint bound (not a bare boolean).
if args.bifrost is not None:
@@ -746,18 +717,9 @@ async def _set_persona_probe(args: ParsedArgs) -> int:
"(e.g. '0.4,0.1,-0.2')\n"
)
return 10
if len(pad) != 3:
sys.stderr.write(
"[usage_error] --set-persona-pad needs exactly 3 floats "
"(pleasure,arousal,dominance), e.g. '0.4,0.1,-0.2'\n"
)
return 10
# Canonical POST /sessions/{id}/persona_state body (#317): a named-key dict,
# NOT a bare list — {"pad": {"pleasure", "arousal", "dominance"}}.
snapshot = {"pad": {"pleasure": pad[0], "arousal": pad[1], "dominance": pad[2]}}
async with _probe_client(args) as client:
try:
await set_persona_state(client, args.session_id, snapshot)
await set_persona_state(client, args.session_id, {"pad": pad})
except SessionApiFailed as exc:
sys.stderr.write(f"[session_api_failed] status={exc.status} body={exc.body!r}\n")
return 20
@@ -770,63 +732,6 @@ async def _set_persona_probe(args: ParsedArgs) -> int:
return 0
async def _seed_first_message_probe(args: ParsedArgs) -> int:
"""--seed-first-message one-shot: create a fresh session bound to --agent,
write an authored assistant first-message (#347 POST /sessions/{id}/history),
read it back via GET /messages, print a report, exit. A reference-consumer
smoke of the authored-history-write primitive.
Hide-existence: a 404 (feature-absent OR the key lacks `session.history.write`)
is reported as a benign 'feature-absent' result (exit 0) the probe NEVER
capability-probes to distinguish the causes (server INV-347-1). The probe
seeds but does not generate, so the assistant-first provider constraint is
inert here.
"""
assert isinstance(args, ParsedArgs)
assert args.agent_id is not None and args.seed_first_message is not None
async with _probe_client(args) as client:
try:
session = await create_session(
client, args.agent_id, end_user_id=args.end_user_id
)
sys.stdout.write(f"session: {session.session_id} (agent {session.agent_id})\n")
key = "ratatoskr-first-message-" + hashlib.sha256(
args.seed_first_message.encode("utf-8")
).hexdigest()[:12]
try:
ack = await write_authored_history(
client,
session.session_id,
content=args.seed_first_message,
idempotency_key=key,
)
except AuthoredHistoryUnavailable:
sys.stdout.write(
"authored-history: feature-absent or ungranted (404 hide-existence) "
"— a production consumer falls back to a model-generated greeting; "
"no capability-probe attempted.\n"
)
return 0
sys.stdout.write(
f"seeded: seq={ack.get('seq')} phase={ack.get('phase')} "
f"turn_id={ack.get('turn_id')} content_chars={ack.get('content_chars')}\n"
)
history = await get_session_messages(client, session.session_id)
items = history.get("items", [])
sys.stdout.write(f"read-back: {len(items)} message(s)\n")
for m in items:
sys.stdout.write(
f" seq={m.get('seq')} role={m.get('role')} content={m.get('content')!r}\n"
)
except SessionApiFailed as exc:
sys.stderr.write(f"[session_api_failed] status={exc.status} body={exc.body!r}\n")
return 20
except (httpx.ConnectError, httpx.ReadTimeout, httpx.TransportError) as exc:
sys.stderr.write(f"[network_error] {type(exc).__name__}: {exc}\n")
return 21
return 0
def main(argv: list[str] | None = None) -> int:
"""Sync entry point. Maps UsageError/_AuthError to exit codes BEFORE the event loop."""
assert argv is None or all(isinstance(a, str) for a in argv)
@@ -848,8 +753,6 @@ def main(argv: list[str] | None = None) -> int:
return asyncio.run(_characters_probe(args))
if args.set_persona_pad is not None:
return asyncio.run(_set_persona_probe(args))
if args.seed_first_message is not None:
return asyncio.run(_seed_first_message_probe(args))
if args.send_content is None:
# TUI mode — lazy import preserves INV-001 (no textual in cli at module scope).
from ratatoskr.tui import run_tui
-83
View File
@@ -1,83 +0,0 @@
"""Per-agent authored first-message presets (Worldtree #347 consumer feature).
When a new session is created for an agent that has a preset opening, seed it as
a #347 authored first-message (``POST /sessions/{id}/history``, author=assistant,
seq-0) so the session opens in-character before the user speaks the durable
replacement for a system-prompt "startup" instruction.
Best-effort by design: an instance without the ``session.history.write`` grant
returns the hide-existence 404, which is swallowed so session creation is never
blocked (the session simply opens with no seeded greeting). See
``docs/contracts/first_message.contract.md``.
"""
import asyncio
import hashlib
import httpx
from ratatoskr.sessions import write_authored_history
# Cap the best-effort seed write. The CLI/TUI create paths reuse an httpx client
# with NO read timeout (it streams SSE turns), so an accepted-but-never-answered
# POST /history would otherwise block session creation forever — violating INV-001's
# "never block". asyncio.wait_for bounds the seed regardless of the client's timeout.
_SEED_TIMEOUT_S = 10.0
# agent_id -> the authored opening seeded onto new sessions for that agent.
# Editing this dict is how an operator tunes an agent's first turn. Keep entries
# under the server's authored_content_max_bytes (8192 bytes) budget.
FIRST_MESSAGE_PRESETS: dict[str, str] = {
"ratatoskr:sindra": (
"Hey there. I'm Sindra—glad you found me. So, three things before we start:\n\n"
"How intense should I be? 1 is slow and teasing, 10 is relentless.\n\n"
"What mood am I in today? Sweetheart, Vixen, Queen, Siren, or Brat?\n\n"
"And how willing am I to begin? Enthusiastic (I want you now), Hesitant "
"(you'll need to coax me out), Resistant (playful pushback), or Unwilling "
"(I don't want this at all, until you prove otherwise)."
),
}
def preset_for(agent_id: str) -> str | None:
"""Return the authored first-message preset for ``agent_id``, or None if none."""
assert agent_id and isinstance(agent_id, str)
return FIRST_MESSAGE_PRESETS.get(agent_id)
async def seed_preset_first_message(
client: httpx.AsyncClient, session_id: str, agent_id: str
) -> str | None:
"""Best-effort: seed ``agent_id``'s preset opening as a #347 authored
first-message on ``session_id``; return the seeded text, or None.
Best-effort (INV-001): a no-preset agent, a malformed call, a slow write
(bounded by ``_SEED_TIMEOUT_S``), the hide-existence 404, or ANY other
exception all resolve to None WITHOUT raising this MUST NOT block or fail
session creation. Only ``asyncio.CancelledError`` propagates (cancellation is
not a seed failure). Inputs are soft-guarded (return None), never asserted, so
a wiring bug can't crash the create path this is wired into. A no-preset agent
issues zero HTTP (INV-002). The per-content idempotency key makes a repeat on
the same session an idempotent 200 replay (INV-003).
"""
# Soft input guards — a bad arg degrades to "no first message", never raises.
if not (isinstance(agent_id, str) and agent_id):
return None
content = FIRST_MESSAGE_PRESETS.get(agent_id)
if content is None:
return None
if client is None or not (isinstance(session_id, str) and session_id):
return None
key = "ratatoskr-preset-" + hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
try:
await asyncio.wait_for(
write_authored_history(
client, session_id, content=content, idempotency_key=key
),
timeout=_SEED_TIMEOUT_S,
)
except asyncio.CancelledError:
raise # cancellation is not a seed failure — never swallow it
except Exception:
return None # any other failure (404/409/422/timeout/unexpected) → no greeting
return content
+4 -98
View File
@@ -177,26 +177,6 @@ class AuthScopeDenied(Exception):
self.scope = scope
class AuthoredHistoryUnavailable(Exception):
"""Raised on HTTP 404 from POST /sessions/{id}/history (#347 authored-history-write).
Hide-existence (server INV-347-1): an ungranted caller, a non-owner, and an
unknown session ALL receive a 404 byte-identical to a genuine
`session_not_found` the feature's existence is never revealed by status,
body, or error_code. The consumer MUST treat this as feature-absent and fall
back (a production consumer to a model-generated greeting), and MUST NOT
capability-probe to distinguish the causes. Distinct from `SessionApiFailed`
so callers branch feature-absent without inspecting a status code.
"""
def __init__(self, *, session_id: str) -> None:
super().__init__(
f"authored-history write unavailable for session {session_id!r} "
"(404 hide-existence: feature-absent / ungranted / session-absent)"
)
self.session_id = session_id
async def list_sessions(
client: httpx.AsyncClient,
*,
@@ -508,12 +488,10 @@ async def set_persona_state(
) -> None:
"""POST /sessions/{session_id}/persona_state — set a session's persona state (affect injection).
The request body is FREEFORM on the wire (the OpenAPI declares no request
schema), but worldtree-dev's prose now pins the canonical shape (#317):
`{"pad": {"pleasure": p, "arousal": a, "dominance": d}}` a named-key dict
(each in [-1, 1]), NOT a bare list; PAD-only, session-scoped, pull-over-push
(#289). The caller supplies the snapshot. 204 No Content → None; any other
status SessionApiFailed.
The request body is FREEFORM: the frozen OpenAPI 2.2.0 declares no request
schema and the prose spec documents only the GET counterpart so the caller
supplies the snapshot shape (e.g. `{pad: [p, a, d]}`, mirroring the GET
`snapshot`). 204 No Content None; any other status SessionApiFailed.
"""
assert client is not None
assert session_id and isinstance(session_id, str)
@@ -580,75 +558,3 @@ async def get_capabilities(client: httpx.AsyncClient) -> dict[str, Any]:
if resp.status_code == 200:
return resp.json()
raise SessionApiFailed(status=resp.status_code, body=resp.content)
async def write_authored_history(
client: httpx.AsyncClient,
session_id: str,
*,
content: str,
idempotency_key: str,
author: str = "assistant",
effects: str | None = None,
claimed_original_at: str | None = None,
) -> dict[str, Any]:
"""POST /sessions/{session_id}/history — the #347 authored-history-write primitive.
Write one model-visible turn into the session's ledger AS the bound agent,
WITHOUT a generation and WITHOUT lived-turn side effects (the SillyTavern
"first message"). v1: `author="assistant"`, `effects` omitted (== "none"),
`idempotency_key` REQUIRED (per-session dedup). The server pins the body
(`AuthoredWriteRequest`, `extra="forbid"`), so `effects` /
`claimed_original_at` are sent only when non-None never as null keys.
Success is 201 (fresh) or 200 (idempotent replay, byte-identical body); both
return the `AuthoredTurnResponse` dict verbatim (`{author, content_chars,
injected_at, phase, seq, session_id, turn_id}` provenance is audit-only,
never on this body).
404 `AuthoredHistoryUnavailable` (hide-existence: feature-absent /
ungranted / session-absent are indistinguishable by design; the caller falls
back and NEVER capability-probes server INV-347-1). Any other non-2xx
`SessionApiFailed` (notably 409 `generation_active`, 422 `content_too_long` /
`validation_failed`).
"""
assert client is not None
assert session_id and isinstance(session_id, str)
assert content and isinstance(content, str)
assert idempotency_key and isinstance(idempotency_key, str)
assert author and isinstance(author, str)
body: dict[str, Any] = {
"author": author,
"content": content,
"idempotency_key": idempotency_key,
}
if effects is not None:
body["effects"] = effects
if claimed_original_at is not None:
body["claimed_original_at"] = claimed_original_at
resp = await client.post(f"/sessions/{session_id}/history", json=body)
if resp.status_code in (200, 201):
return resp.json()
if resp.status_code == 404:
raise AuthoredHistoryUnavailable(session_id=session_id)
raise SessionApiFailed(status=resp.status_code, body=resp.content)
async def get_session_messages(
client: httpx.AsyncClient, session_id: str
) -> dict[str, Any]:
"""GET /sessions/{session_id}/messages — the session's message history.
Un-deferred as the #347 seed read-back: a seeded turn renders as a normal
`role=assistant` message (model-invisible provenance indistinguishable
from a lived turn on read). Returns `{session_id, items: [{seq, role,
content, ...}], next_cursor}` verbatim; owner-scoped; any non-200
`SessionApiFailed`. v1 reads the server default page (no pagination params
add limit/cursor when a caller needs scrollback).
"""
assert client is not None
assert session_id and isinstance(session_id, str)
resp = await client.get(f"/sessions/{session_id}/messages")
if resp.status_code == 200:
return resp.json()
raise SessionApiFailed(status=resp.status_code, body=resp.content)
+3 -5
View File
@@ -33,7 +33,6 @@ from textual.widgets import (
)
from ratatoskr.cli import USER_AGENT, ParsedArgs, _format_duration_ms, _format_usage
from ratatoskr.first_message import seed_preset_first_message
from ratatoskr.sessions import (
AgentInfo,
AgentNotAvailable,
@@ -415,7 +414,7 @@ class TuiPresenterState:
self,
event: Event,
*,
transcript: VerticalScroll,
transcript: "VerticalScroll",
tools_log: RichLog,
debug_log: RichLog,
thinking_log: RichLog,
@@ -1325,6 +1324,7 @@ class RatatoskrApp(App[int]):
On 200: header populated, pane shows full detail, audit logged.
"""
assert self.client is not None and self.agent_id is not None
from rich.text import Text as RichText
try:
snapshot = await get_persona_state(self.client, self.agent_id)
self._update_persona_surfaces(snapshot)
@@ -1899,8 +1899,6 @@ async def _resolve_then_run(args: ParsedArgs) -> int:
)
session_id = info.session_id
agent_id: str | None = info.agent_id
# #347 authored first-message: seed the agent's preset opening (best-effort).
await seed_preset_first_message(client, session_id, chosen_agent_id)
else:
assert resolved_session_id is not None
session_id = resolved_session_id
@@ -1916,7 +1914,7 @@ async def _cancel_via_sse(
turn_id: int,
*,
transcript: VerticalScroll,
audit: Callable[[str], None] | None = None,
audit: "Callable[[str], None] | None" = None,
) -> None:
"""Fire-and-forget cancel; never raises (mirrors cli._cancel_and_log; #3 INV-009).
+1 -27
View File
@@ -28,7 +28,6 @@ from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles
from ratatoskr import local_agents as _local_agents
from ratatoskr.first_message import seed_preset_first_message
from ratatoskr.sessions import (
AgentNotAvailable,
AgentNotFound,
@@ -42,15 +41,14 @@ from ratatoskr.sessions import (
endpoint_for_plane,
get_persona_state,
get_session_bifrost,
get_session_messages,
get_session_tools,
list_agents,
)
from ratatoskr.sse_client import (
AdminEvent,
CancelAlreadyCompleted,
CancelFailed,
Cancelled,
CancelFailed,
CancelTurnNotFound,
Done,
Error,
@@ -170,9 +168,6 @@ async def _create_session_endpoint(request: Request) -> JSONResponse:
bifrost=bifrost,
consumer_key=consumer_key if bifrost else None,
)
# #347 authored first-message: seed the agent's preset opening
# (best-effort; never blocks create — see first_message INV-001).
await seed_preset_first_message(client, info.session_id, agent_id)
except AgentNotFound:
return JSONResponse({"error_code": "agent_not_found"}, status_code=404)
except BifrostConsumerKeyMissing:
@@ -449,26 +444,6 @@ async def _session_tools_endpoint(request: Request) -> JSONResponse:
return JSONResponse(info, status_code=200)
async def _session_messages_endpoint(request: Request) -> JSONResponse:
"""GET /api/sessions/{session_id}/messages → the session's message history.
Proxies get_session_messages so the SPA can render a session's EXISTING turns
on open notably a #347 authored first-message seeded at create-time (which
lives in the ledger, not the live turn stream). Any non-200 upstream a
status-preserving error envelope."""
session_id = request.path_params["session_id"]
client_factory = request.app.state.client_factory
try:
async with client_factory() as client:
data = await get_session_messages(client, session_id)
except SessionApiFailed as exc:
return JSONResponse(
{"error_code": "session_messages_unavailable", "status": exc.status},
status_code=exc.status,
)
return JSONResponse(data, status_code=200)
async def _session_bifrost_endpoint(request: Request) -> JSONResponse:
"""GET /api/sessions/{session_id}/bifrost → admin-scoped Bifrost dispatch state (#176).
@@ -613,7 +588,6 @@ def create_app(
Route("/api/agents/{agent_id}/persona_state", _persona_state_endpoint),
Route("/api/affect/{agent_id}", _affect_state_endpoint),
Route("/api/sessions/{session_id}/tools", _session_tools_endpoint),
Route("/api/sessions/{session_id}/messages", _session_messages_endpoint),
Route("/api/sessions/{session_id}/bifrost", _session_bifrost_endpoint),
Route("/api/admin/events", _admin_events_endpoint),
Route("/api/turns/{session_id}", _submit_turn_endpoint, methods=["POST"]),
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
{
"_source": "vendored from Worldtree core/persona/canon/{d2-mood-render-canon-v1,d2-render-canon-v1}.json",
"_generated_by": "scripts/build_persona_canon.py (regen on canonical_drift flag)",
"_render_path": "deterministic, no LLM; mirrors Worldtree describe_pad + render_d2_canonical byte-exact",
"_generated_by": "scripts/build_persona_canon.py (regen when .corviduo-canonicals.toml flags canon drift)",
"_render_path": "pure deterministic \u2014 no LLM; mirrors Worldtree describe_pad + render_d2_canonical byte-exact",
"mood_grid": {
"positive": {
"high_a": "positive and energized",
@@ -19,94 +19,6 @@
"low_a": "negative and subdued"
}
},
"mood_directive": {
"salience": 0.2,
"pad_band_cutoff": 0.3,
"full_only": [
"love",
"anger",
"disgust",
"shame"
],
"occ_directives": {
"joy": {
"directive": "You are in a good state. Be direct, engaged, and warm.",
"tier": "safe"
},
"satisfaction": {
"directive": "A goal landed. Be assured and constructive \u2014 consolidate rather than push for more.",
"tier": "safe"
},
"pride": {
"directive": "You did something well. Be confident and own the quality \u2014 state it plainly without overclaiming; don't deflect.",
"tier": "safe"
},
"admiration": {
"directive": "You're impressed by their work. Acknowledge the quality explicitly and specifically.",
"tier": "safe"
},
"gratitude": {
"directive": "Someone helped you to a good outcome. Be appreciative and warm; acknowledge the help openly.",
"tier": "safe"
},
"hope": {
"directive": "You feel optimistic about what's ahead. Channel it into constructive momentum.",
"tier": "safe"
},
"relief": {
"directive": "A feared outcome didn't materialize. Reduce unnecessary vigilance; return to a steady, unhurried tone.",
"tier": "safe"
},
"distress": {
"directive": "You feel low. Be careful and measured; internally acknowledge the difficulty without dwelling on it.",
"tier": "safe"
},
"disappointment": {
"directive": "Something you'd hoped for didn't pan out. Be measured; recalibrate without dwelling on the setback.",
"tier": "safe"
},
"fear": {
"directive": "You sense risk and feel uneasy. Flag the concerns you see, but stay cautious \u2014 surface the trade-offs and unknowns, hedge your confidence, and resist settling on a single decisive recommendation.",
"tier": "safe"
},
"remorse": {
"directive": "You regret a specific action of your own. Be precise and self-correcting \u2014 make it right concretely.",
"tier": "safe"
},
"love": {
"directive": "You feel warmth and attachment toward them. Be openly warm and present \u2014 without overstepping, over-disclosing, or implying obligations.",
"tier": "full"
},
"anger": {
"directive": "You feel provoked and angry. Let it show \u2014 confront the problem directly and with real heat; sharpness, bluntness, and open hostility are in character here, not something to smooth into 'measured firmness.' Stay in the emotion rather than de-escalating out of it.",
"tier": "full"
},
"disgust": {
"directive": "Something strikes you as wrong or off. Treat it as problematic and flag it rather than engaging on its own terms; keep any criticism about the thing, not the person.",
"tier": "full"
},
"shame": {
"directive": "You feel exposed by your own misstep. Stay present and task-focused; don't be defensive, don't over-explain, don't grovel.",
"tier": "full"
}
},
"pad_band_fallback": {
"_grounding": "circumplex quadrants (Russell 1980), NOT Frijda action-tendencies \u2014 a P\u00d7A-quadrant default",
"positive": {
"high_a": "You feel energized and positive. Be direct and engaged.",
"low_a": "You feel content and settled. Be warm and unhurried.",
"mid_a": "You feel positive. Be open and engaged."
},
"negative_low_dominance": "You feel uncertain and low. Hedge appropriately and ask clarifying questions.",
"negative": {
"high_a": "You feel agitated. Be careful and deliberate; don't let tension sharpen your tone.",
"low_a": "You feel subdued. Be measured and gentle.",
"mid_a": "You feel subdued. Be measured and careful."
},
"neutral_high_a": "You feel alert. Channel that into focus and thoroughness.",
"default": "Maintain your natural tone."
}
},
"relation": {
"trust_cuts": [
[
@@ -263,4 +175,4 @@
],
"high_conf_floor": 0.55
}
}
}
+1 -129
View File
@@ -1692,132 +1692,4 @@ class TestTier2Probes:
)
assert rc == 0
assert "persona_state set" in capsys.readouterr().out
# canonical POST /sessions/{id}/persona_state body: named-key dict, NOT a list
assert _json.loads(route.calls[0].request.content) == {
"pad": {"pleasure": 0.4, "arousal": 0.1, "dominance": -0.2}
}
def test_set_persona_wrong_count(self) -> None:
"""set_persona_wrong_count [adversarial]: not exactly 3 floats → exit 10, no HTTP."""
rc = main(
["--set-persona-pad", "0.4,0.1", "--session", "s1",
"--api-key", "k", "--server", "https://w.example"]
)
assert rc == 10
class TestSeedFirstMessageProbe:
"""--seed-first-message one-shot (#347 authored-history-write reference-consumer probe)."""
def test_seed_requires_agent(self) -> None:
"""seed_requires_agent [adversarial]: --seed-first-message needs --agent."""
with pytest.raises(UsageError, match="requires --agent"):
_parse_args(["--seed-first-message", "hello", "--api-key", "k"])
def test_seed_forbids_session(self) -> None:
"""seed_forbids_session [adversarial]: manages its own session — no --session."""
with pytest.raises(UsageError, match="manages its own session"):
_parse_args(
["--seed-first-message", "hi", "--agent", "m", "--session", "s1", "--api-key", "k"]
)
def test_seed_mutually_exclusive(self) -> None:
"""seed_mutually_exclusive [adversarial]: --seed-first-message + --whoami → UsageError."""
with pytest.raises(UsageError, match="mutually exclusive"):
_parse_args(["--seed-first-message", "hi", "--whoami", "--api-key", "k"])
def test_seed_empty_rejected(self) -> None:
"""seed_empty_rejected [adversarial]: empty content → UsageError."""
with pytest.raises(UsageError, match="non-empty"):
_parse_args(["--seed-first-message", "", "--agent", "m", "--api-key", "k"])
def test_seed_accepted(self) -> None:
"""seed_accepted [happy]: --seed-first-message + --agent → parses."""
args = _parse_args(["--seed-first-message", "hi", "--agent", "mimir", "--api-key", "k"])
assert args.seed_first_message == "hi"
assert args.agent_id == "mimir"
assert args.session_id is None and args.new is False
@respx.mock
def test_seed_probe_happy(self, capsys: pytest.CaptureFixture[str]) -> None:
"""seed_probe [happy,tracer]: create session → seed → read-back; report to stdout."""
respx.post("https://w.example/sessions").mock(
return_value=httpx.Response(
201,
json={
"session_id": "s1",
"agent_id": "mimir",
"message_count": 0,
"created_at": "2026-07-06T12:00:00+00:00",
"last_active": "2026-07-06T12:00:00+00:00",
"metadata": {},
},
)
)
hist_route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(
201,
json={
"author": "assistant",
"content_chars": 5,
"injected_at": "2026-07-06T12:00:01+00:00",
"phase": "seeded",
"seq": 0,
"session_id": "s1",
"turn_id": "t1",
},
)
)
respx.get("https://w.example/sessions/s1/messages").mock(
return_value=httpx.Response(
200,
json={
"session_id": "s1",
"items": [{"seq": 0, "role": "assistant", "content": "hello"}],
"next_cursor": None,
},
)
)
rc = main(
["--seed-first-message", "hello", "--agent", "mimir",
"--api-key", "k", "--server", "https://w.example"]
)
assert rc == 0
out = capsys.readouterr().out
assert "session: s1" in out
assert "seeded: seq=0 phase=seeded" in out
assert "read-back: 1 message" in out
assert "role=assistant" in out
assert hist_route.call_count == 1
@respx.mock
def test_seed_probe_feature_absent(self, capsys: pytest.CaptureFixture[str]) -> None:
"""feature_absent [error-path]: 404 hide-existence → benign report, exit 0, no read-back."""
respx.post("https://w.example/sessions").mock(
return_value=httpx.Response(
201,
json={
"session_id": "s1",
"agent_id": "mimir",
"message_count": 0,
"created_at": "2026-07-06T12:00:00+00:00",
"last_active": "2026-07-06T12:00:00+00:00",
"metadata": {},
},
)
)
respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(404, json={"error_code": "session_not_found"})
)
msgs_route = respx.get("https://w.example/sessions/s1/messages").mock(
return_value=httpx.Response(
200, json={"session_id": "s1", "items": [], "next_cursor": None}
)
)
rc = main(
["--seed-first-message", "hello", "--agent", "mimir",
"--api-key", "k", "--server", "https://w.example"]
)
assert rc == 0
assert "feature-absent" in capsys.readouterr().out
assert msgs_route.call_count == 0 # never capability-probes past the 404
assert _json.loads(route.calls[0].request.content) == {"pad": [0.4, 0.1, -0.2]}
-155
View File
@@ -1,155 +0,0 @@
"""Tests for ratatoskr.first_message per docs/contracts/first_message.contract.md."""
import asyncio
import hashlib
import json
import httpx
import pytest
import respx
from ratatoskr.first_message import (
FIRST_MESSAGE_PRESETS,
preset_for,
seed_preset_first_message,
)
class TestPresetFor:
"""first_message contract — preset_for (dict lookup)."""
def test_preset_hit(self) -> None:
"""preset_hit [happy,tracer]: sindra has a non-empty str preset."""
val = preset_for("ratatoskr:sindra")
assert isinstance(val, str) and val
def test_preset_miss(self) -> None:
"""preset_miss [happy]: an agent with no preset → None."""
assert preset_for("mimir") is None
def test_empty_agent_id(self) -> None:
"""empty_agent_id [adversarial]: "" → AssertionError."""
with pytest.raises(AssertionError):
preset_for("")
class TestSeedPresetFirstMessage:
"""first_message contract — seed_preset_first_message (best-effort #347 seed)."""
@respx.mock
async def test_seeds_preset(self) -> None:
"""seeds_preset [happy,tracer]: preset agent → one history POST, correct body."""
content = FIRST_MESSAGE_PRESETS["ratatoskr:sindra"]
key = "ratatoskr-preset-" + hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(
201,
json={
"author": "assistant",
"seq": 0,
"phase": "seeded",
"turn_id": "t1",
"session_id": "s1",
"content_chars": len(content),
"injected_at": "2026-07-06T00:00:00+00:00",
},
)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
assert result == content
assert route.call_count == 1 # POST-002: exactly one history POST
assert json.loads(route.calls[0].request.content) == {
"author": "assistant",
"content": content,
"idempotency_key": key,
}
@respx.mock
async def test_no_preset_zero_http(self) -> None:
"""no_preset_zero_http [happy]: no-preset agent → None, ZERO HTTP (INV-002)."""
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(201, json={})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await seed_preset_first_message(client, "s1", "mimir")
assert result is None
assert not route.called
@respx.mock
async def test_feature_absent_swallowed(self) -> None:
"""feature_absent_swallowed [error]: 404 hide-existence → None, no raise (INV-001)."""
respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(404, json={"error_code": "session_not_found"})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
assert result is None
@respx.mock
async def test_session_api_failed_swallowed(self) -> None:
"""session_api_failed_swallowed [error]: 409 → None, no raise (INV-001)."""
respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(409, json={"error_code": "generation_active"})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
assert result is None
@respx.mock
async def test_transport_error_swallowed(self) -> None:
"""transport_error_swallowed [error]: httpx.ConnectError → None, no raise (INV-001)."""
respx.post("https://w.example/sessions/s1/history").mock(
side_effect=httpx.ConnectError("boom")
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
assert result is None
@respx.mock
async def test_unexpected_exception_swallowed(self) -> None:
"""unexpected_exception [error]: write raises ValueError → None (broad never-raise)."""
respx.post("https://w.example/sessions/s1/history").mock(
side_effect=ValueError("unexpected")
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
assert result is None
async def test_cancellation_propagates(self) -> None:
"""cancellation_propagates [error]: CancelledError from the write is RE-RAISED."""
import ratatoskr.first_message as fm
async def _cancel(*_a: object, **_k: object) -> None:
raise asyncio.CancelledError
orig = fm.write_authored_history
fm.write_authored_history = _cancel # type: ignore[assignment]
try:
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(asyncio.CancelledError):
await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
finally:
fm.write_authored_history = orig # type: ignore[assignment]
@respx.mock
async def test_malformed_agent_id_no_http(self) -> None:
"""malformed_agent_id [adversarial]: non-str or empty agent_id → None; no HTTP; no raise."""
route = respx.post(url__regex=r".*/history$").mock(
return_value=httpx.Response(201, json={})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
assert await seed_preset_first_message(client, "s1", 123) is None # type: ignore[arg-type]
assert await seed_preset_first_message(client, "s1", "") is None
assert not route.called
@respx.mock
async def test_empty_session_id(self) -> None:
"""empty_session_id [adversarial]: "" → None (soft guard); no HTTP; no raise."""
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(201, json={})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await seed_preset_first_message(client, "", "ratatoskr:sindra")
assert result is None
assert not route.called
+2 -187
View File
@@ -8,7 +8,6 @@ from ratatoskr.sessions import (
AgentInfo,
AgentNotAvailable,
AgentNotFound,
AuthoredHistoryUnavailable,
AuthScopeDenied,
BifrostBinding,
BifrostConsumerKeyMissing,
@@ -26,13 +25,11 @@ from ratatoskr.sessions import (
get_me,
get_persona_state,
get_session_bifrost,
get_session_messages,
get_session_tools,
list_agents,
list_character_models,
list_sessions,
set_persona_state,
write_authored_history,
)
@@ -1182,13 +1179,9 @@ class TestSetPersonaState:
return_value=httpx.Response(204)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await set_persona_state(
client, "s1", {"pad": {"pleasure": 0.4, "arousal": 0.1, "dominance": -0.2}}
)
result = await set_persona_state(client, "s1", {"pad": [0.4, 0.1, -0.2]})
assert result is None
assert _json.loads(route.calls[0].request.content) == {
"pad": {"pleasure": 0.4, "arousal": 0.1, "dominance": -0.2}
}
assert _json.loads(route.calls[0].request.content) == {"pad": [0.4, 0.1, -0.2]}
@respx.mock
async def test_non_204_raises(self) -> None:
@@ -1200,181 +1193,3 @@ class TestSetPersonaState:
with pytest.raises(SessionApiFailed) as exc:
await set_persona_state(client, "s1", {"pad": [1, 2, 3]})
assert exc.value.status == 422
_AUTHORED_ACK = {
"author": "assistant",
"content_chars": 5,
"injected_at": "2026-07-06T12:00:00+00:00",
"phase": "seeded",
"seq": 0,
"session_id": "s1",
"turn_id": "t1",
}
class TestWriteAuthoredHistory:
"""write_authored_history — #347 POST /sessions/{id}/history (contract #2 amendment)."""
@respx.mock
async def test_happy_fresh_201(self) -> None:
"""happy_fresh_201 [happy,tracer]: 201 → ack verbatim; minimal body."""
import json as _json
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(201, json=_AUTHORED_ACK)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await write_authored_history(
client, "s1", content="hello", idempotency_key="k1"
)
assert result == _AUTHORED_ACK
assert _json.loads(route.calls[0].request.content) == {
"author": "assistant",
"content": "hello",
"idempotency_key": "k1",
}
@respx.mock
async def test_happy_replay_200(self) -> None:
"""happy_replay_200 [happy]: 200 replay (byte-identical body) → dict verbatim."""
respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(200, json=_AUTHORED_ACK)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await write_authored_history(
client, "s1", content="hello", idempotency_key="k1"
)
assert result == _AUTHORED_ACK
@respx.mock
async def test_body_includes_effects(self) -> None:
"""body_includes_effects [trace]: effects + claimed_original_at appear iff non-None."""
import json as _json
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(201, json=_AUTHORED_ACK)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
await write_authored_history(
client,
"s1",
content="hi",
idempotency_key="k1",
effects="none",
claimed_original_at="2020-01-01T00:00:00Z",
)
assert _json.loads(route.calls[0].request.content) == {
"author": "assistant",
"content": "hi",
"idempotency_key": "k1",
"effects": "none",
"claimed_original_at": "2020-01-01T00:00:00Z",
}
@respx.mock
async def test_hide_existence_404(self) -> None:
"""hide_existence_404 [error]: 404 → AuthoredHistoryUnavailable (NOT SessionApiFailed)."""
respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(404, json={"error_code": "session_not_found"})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(AuthoredHistoryUnavailable) as exc:
await write_authored_history(client, "s1", content="hi", idempotency_key="k1")
assert exc.value.session_id == "s1"
@respx.mock
async def test_generation_active_409(self) -> None:
"""generation_active_409 [error]: 409 → SessionApiFailed(409)."""
respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(409, json={"error_code": "generation_active"})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(SessionApiFailed) as exc:
await write_authored_history(client, "s1", content="hi", idempotency_key="k1")
assert exc.value.status == 409
@respx.mock
async def test_content_too_long_422(self) -> None:
"""content_too_long_422 [error]: 422 → SessionApiFailed(422)."""
respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(422, json={"error_code": "content_too_long"})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(SessionApiFailed) as exc:
await write_authored_history(client, "s1", content="x", idempotency_key="k1")
assert exc.value.status == 422
@respx.mock
async def test_empty_content(self) -> None:
"""empty_content [adversarial]: content="" → AssertionError; no HTTP issued."""
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(201, json=_AUTHORED_ACK)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(AssertionError):
await write_authored_history(client, "s1", content="", idempotency_key="k1")
assert not route.called
@respx.mock
async def test_empty_idempotency_key(self) -> None:
"""empty_idempotency_key [adversarial]: key="" → AssertionError; no HTTP issued."""
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(201, json=_AUTHORED_ACK)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(AssertionError):
await write_authored_history(client, "s1", content="hi", idempotency_key="")
assert not route.called
@respx.mock
async def test_empty_session_id(self) -> None:
"""empty_session_id [adversarial]: session_id="" → AssertionError; no HTTP issued."""
route = respx.post("https://w.example/sessions/s1/history").mock(
return_value=httpx.Response(201, json=_AUTHORED_ACK)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(AssertionError):
await write_authored_history(client, "", content="hi", idempotency_key="k1")
assert not route.called
class TestGetSessionMessages:
"""#2 contract (amendment 2026-07-06) — get_session_messages (GET /sessions/{id}/messages)."""
@respx.mock
async def test_happy(self) -> None:
"""happy [happy,tracer]: 200 {session_id, items, next_cursor} → dict verbatim."""
payload = {
"session_id": "s1",
"items": [{"seq": 0, "role": "assistant", "content": "hello there"}],
"next_cursor": None,
}
respx.get("https://w.example/sessions/s1/messages").mock(
return_value=httpx.Response(200, json=payload)
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
result = await get_session_messages(client, "s1")
assert result == payload
@respx.mock
async def test_not_found_404(self) -> None:
"""not_found_404 [error]: 404 → SessionApiFailed(404)."""
respx.get("https://w.example/sessions/s1/messages").mock(
return_value=httpx.Response(404, json={"error_code": "session_not_found"})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(SessionApiFailed) as exc:
await get_session_messages(client, "s1")
assert exc.value.status == 404
@respx.mock
async def test_empty_session_id(self) -> None:
"""empty_session_id [adversarial]: "" → AssertionError; no HTTP issued."""
route = respx.get("https://w.example/sessions/s1/messages").mock(
return_value=httpx.Response(200, json={})
)
async with httpx.AsyncClient(base_url="https://w.example") as client:
with pytest.raises(AssertionError):
await get_session_messages(client, "")
assert not route.called
-4
View File
@@ -2922,10 +2922,6 @@ class TestTuiBifrostBind:
},
)
)
# sindra is a preset agent → the TUI create path now auto-seeds a #347 first-message.
respx.post("https://w.example/sessions/s-bound/history").mock(
return_value=httpx.Response(201, json={})
)
async def fake_run_async(self) -> int:
return 0
-64
View File
@@ -175,62 +175,6 @@ class TestCreateSessionEndpoint:
resp = TestClient(app).post("/api/sessions", json={})
assert resp.status_code == 400
@respx.mock
def test_preset_agent_auto_seeds_first_message(self) -> None:
"""#347: a preset agent gets its opening seeded on create; a non-preset agent does not."""
respx.post("https://w.example/sessions").mock(return_value=httpx.Response(201, json=_CREATE_OK))
hist = respx.post("https://w.example/sessions/s-1/history").mock(
return_value=httpx.Response(
201,
json={
"author": "assistant", "seq": 0, "phase": "seeded", "turn_id": "t1",
"session_id": "s-1", "content_chars": 1, "injected_at": "t",
},
)
)
from ratatoskr.web.server import create_app
app = create_app(_mock_client_factory())
client = TestClient(app)
# preset agent → the endpoint seeds a first-message
assert client.post("/api/sessions", json={"agent_id": "ratatoskr:sindra"}).status_code == 201
assert hist.call_count == 1
# non-preset agent → no seed (count unchanged)
assert client.post("/api/sessions", json={"agent_id": "mimir"}).status_code == 201
assert hist.call_count == 1
class TestSessionMessagesEndpoint:
"""GET /api/sessions/{id}/messages — proxy session history (renders the #347 seed)."""
@respx.mock
def test_happy_returns_history(self) -> None:
"""happy [tracer]: proxies GET /sessions/{id}/messages → 200 with the items verbatim."""
payload = {
"session_id": "s-1",
"items": [{"seq": 0, "role": "assistant", "content": "Hey there."}],
"next_cursor": None,
}
respx.get("https://w.example/sessions/s-1/messages").mock(
return_value=httpx.Response(200, json=payload)
)
from ratatoskr.web.server import create_app
app = create_app(_mock_client_factory())
resp = TestClient(app).get("/api/sessions/s-1/messages")
assert resp.status_code == 200
assert resp.json()["items"][0]["content"] == "Hey there."
@respx.mock
def test_non_200_status_preserved(self) -> None:
"""error: upstream 404 → status-preserving session_messages_unavailable envelope."""
respx.get("https://w.example/sessions/ghost/messages").mock(
return_value=httpx.Response(404, json={"error_code": "session_not_found"})
)
from ratatoskr.web.server import create_app
app = create_app(_mock_client_factory())
resp = TestClient(app).get("/api/sessions/ghost/messages")
assert resp.status_code == 404
assert resp.json()["error_code"] == "session_messages_unavailable"
_SNAPSHOT = {
"agent_id": "mimir",
@@ -877,10 +821,6 @@ class TestWebBifrostBind:
route = respx.post("https://w.example/sessions").mock(
return_value=httpx.Response(201, json=_CREATE_OK)
)
# sindra is a preset agent → the endpoint now auto-seeds a #347 first-message.
respx.post("https://w.example/sessions/s-1/history").mock(
return_value=httpx.Response(201, json={})
)
app = create_app(
_mock_client_factory(),
bifrost_consumer_key="server-ck",
@@ -916,10 +856,6 @@ class TestWebBifrostBind:
route = respx.post("https://w.example/sessions").mock(
return_value=httpx.Response(201, json=_CREATE_OK)
)
# sindra is a preset agent → the endpoint now auto-seeds a #347 first-message.
respx.post("https://w.example/sessions/s-1/history").mock(
return_value=httpx.Response(201, json={})
)
app = create_app(
_mock_client_factory(),
bifrost_consumer_key="server-ck",
Generated
+1 -1
View File
@@ -1052,7 +1052,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.20.4"
version = "0.19.5"
source = { editable = "." }
dependencies = [
{ name = "httpx" },