5 TDD tasks: ConsideringPhrases (pure) + authored considering.json;
ProxyConfig.request_timeout_seconds (default 35s, reject <=0); HttpTransport
applies it (closes the infinite-hang gap); ConsideringIndicator node shim
(Timer rotates a Label); wire both harnesses around the await.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pivot M2 anti-freeze from streaming to a lighter considering-state: a rotating
in-character 'the Master considers…' indicator (authored content, §13 spirit)
on the proven non-streaming loop, plus the HttpTransport request-timeout fix so
a hung call yields to the fallback instead of spinning forever. Meets §14's
anti-freeze goal by reframing the wait; near-zero new failure surface. Streaming
design PARKED (banner + roadmap ○), revisit if playtest shows the wait hurts.
Conscious §14 deviation recorded per §18.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real token streaming to hide the ~2s call (§14). Proxy emits its own minimal
NDJSON (delta/done/error); /dm/narrate becomes a 200 stream (422 stays sync,
model errors become an in-stream frame). Client reworks to an HTTPClient
poll-loop StreamTransport behind a testable seam; hold-back tag filter;
keep-partial mid-stream degrade; folds in the HttpTransport timeout fix.
Narrator first; NPC fast-follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NPC harness built a canon log with an empty player.luck_descriptor, which the
schema rejects (minLength 1) → /npc/speak 422. Build the player through
LogPlayer with a non-empty §7 fortune line. Human-confirmed working live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The throwaway harness hand-built a minimal canon log and left luck_descriptor
as the default "", which fails the canon-log schema (minLength 1) → /npc/speak
422s and the harness silently degrades to fallback. Build the player through
LogPlayer with a non-empty §7 fortune line. Verified: request now 200s and Fenn
answers in-voice.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bounded NPC conversation (/npc/speak), M2 core experiment. Server role is a
thin drop-in on the M1 pipeline (persona+knowledge server-side); client adds a
pure MoveValidator + MoveApplier over the proven DmService loop — validates
moves against live state, drops invalid, keeps prose (§6). Live-proven vs
qwen3.5 (Fenn in-voice, grounded in knowledge, no Luck leak). Live smoke caught
+ fixed two TagExtractor tag-format drifts. 18 tasks, all reviewed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M2 core experiment. Server owns persona+knowledge (spoilers/IP, no DB);
client owns live state and computes available_moves. Free text straight
to the NPC prompt (no Adjudicator). Sibling NpcService + pure MoveValidator;
moves validated against state, invalid dropped, prose always kept (§6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M2 client HTTP loop: the client posts its canon log to /dm/narrate, shows
the prose, harvests [FACT] via TagExtractor, and degrades to an authored
fallback (§13) on any failure. First end-to-end vertical slice, live-proven
against qwen3.5 through Docker. 67/67 client tests, whole-branch review
MERGE AS-IS.
Package api/prompts into the Docker image and give the Dockerized proxy a
route to a host-run Ollama. M1's narrator pipeline was proven via venv-on-
host; these were the two Docker-path gaps the client HTTP loop surfaced.
After the prompts fix, /dm/narrate reached the model call and failed with
ConnectError: Connection refused — the container's localhost is not the
host, so the default OLLAMA_BASE_URL=http://localhost:11434 hit nothing.
Ollama runs on the host (M1's venv smoke reached it because uvicorn ran on
the host too; the container never had a route).
Add extra_hosts host.docker.internal:host-gateway so the container can
reach the host, and document using http://host.docker.internal:11434 in
.env.example. Verified a container reaches host Ollama's /api/tags (200).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The narrator pipeline (M1) reads role prompts from /app/prompts, but the
Dockerfile copied only api/app and docs/schemas — never api/prompts — so
/dm/narrate 500'd with FileNotFoundError on narrator.md in any Docker run.
The M1 live smoke passed only because it ran uvicorn from the local venv,
where ../prompts resolves on the host; the container path was never
exercised until the client HTTP loop drove a real request through compose.
Dockerfile now COPYs api/prompts; compose mounts it :ro so prompt edits
reload live like the app code (prompts are source code, §16). Verified by
building the image and confirming /app/prompts/narrator.md is present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whole-branch review Minor #1: JSON.parse_string pushes an engine-level
error on a non-JSON body (e.g. a gateway HTML 502), which the project's
gutconfig promotes to a false failure — the same reason FallbackLibrary
already avoids it. Consistency fix; a non-JSON body still degrades.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First M2 vertical slice: CanonLog -> HTTP -> /dm/narrate -> prose ->
screen -> fact-harvest, with a §13 degraded-DM fallback on any failure.
Pure DmService core behind an injectable DmTransport seam (headless-
testable); throwaway harness scene; one authored fallback line under
client/content (renderer owns the file's home).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regroup the flat status list into milestones with a 'what this proves' goal, and
tag every item with its §2 side (state/text/both), dependencies, and milestone
goal. Sequencing calls locked: M3 leads with the playable dungeon
(Adjudicator→Combat→Luck) before the comedic roles (Improviser/Banter); streaming
deferred until the M2 dialogue experiment proves aliveness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan status → COMPLETE, all 8 tasks/44 steps checked. Roadmap: Narrator pipeline
✅, Client HTTP loop promoted to ▶ next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- call_log._default_write: swallow any exception from the default file/
stdout sink and report to stderr, so a log-write failure (disk full,
bad permissions, misconfigured CALL_LOG_PATH) never turns a successful
narration into a 500 (charter §13). Injected write= sinks (used by
tests) are left to surface their own errors.
- ollama_client.chat: catch ValueError alongside httpx.HTTPError in the
retry loop so a 200 response with a non-JSON body (JSONDecodeError is
a ValueError subclass) counts as a failed attempt and falls through to
ModelError after the one retry, instead of escaping chat() uncaught
(charter §12 — one retry, then ModelError, nothing else escapes).
- main.py: update stale docstrings — /dm/narrate is now fully wired
(routing + model call + logging); the other four roles remain stubs.
Regression tests added for both fixes (TDD: watched RED, then GREEN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>