feat(otel/state-patch): route SPAN_QUEST_UPDATE; document port-dead siblings#49
Merged
slabgorb merged 1 commit intofeat/otel-dashboard-restorationfrom Apr 25, 2026
Conversation
…iblings
Phase 2 emission rollout — narrow state-patch bundle to the only constant
with a live Python production caller.
What landed
-----------
- helper: ``quest_update_span`` in ``sidequest/telemetry/spans.py``
(JSON-encodes the updates dict so OTEL primitive-attr restriction
doesn't drop it).
- route: ``SPAN_ROUTES[SPAN_QUEST_UPDATE]`` → ``state_transition`` /
``component="quest_log"`` with the same payload shape the prior
direct ``publish_event`` carried — dashboard sees no change.
- migration: ``sidequest/server/narration_apply.py`` quest_log block
now opens ``quest_update_span`` and the inline ``_watcher_publish``
call is deleted (spec §6.6 dedupe rule).
- tests:
* ``tests/server/test_watcher_events.py`` — translator routing row
(fake span → typed event).
* ``tests/integration/test_state_patch_wiring.py`` — drives
``_apply_narration_result_to_snapshot`` end-to-end through a real
``WatcherSpanProcessor`` and asserts exactly one ``state_transition``
with ``component=quest_log`` reaches the hub. Uses a monkeypatched
``tracer()`` because OTEL forbids replacing an already-installed
global provider mid-suite.
What stayed flat-only
---------------------
``SPAN_APPLY_WORLD_PATCH``, ``SPAN_BUILD_PROTOCOL_DELTA``, and
``SPAN_COMPUTE_DELTA`` remain in ``FLAT_ONLY_SPANS`` with port-artifact
comments. Audit found no production caller for any of the three:
``apply_world_patch`` lives in ``game/session.py`` but the Python port
inlined those mutations directly in ``narration_apply.py``;
``build_protocol_delta`` doesn't exist as a function (only mentioned in
a docstring); ``compute_delta`` is exported from ``game/delta.py`` but
called only from tests. Wiring spans into dead functions would violate
CLAUDE.md "Verify Wiring, Not Just Existence" — flagged for follow-up:
either delete the constants or wire the helpers through the dispatch
loop (not in scope for this bundle).
Validation gates
----------------
- ``tests/telemetry/test_routing_completeness.py`` — green (constant
moved out of FLAT_ONLY_SPANS; new route registered).
- ``tests/server/test_watcher_events.py`` — green.
- ``tests/integration/test_state_patch_wiring.py`` — green.
- Full ``tests/server/ tests/telemetry/ tests/integration/`` suite —
500 passed, 2 skipped (no regressions).
- ``ruff check`` on touched files — clean.
ADR-090 §"Phase 2 emission rollouts"; handoff
``docs/superpowers/plans/2026-04-25-otel-phase-2-emission-rollouts-HANDOFF.md``
bundle #1 (state-patch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 25, 2026
slabgorb
added a commit
to slabgorb/sidequest
that referenced
this pull request
Apr 25, 2026
* docs(spec): shared Room.snapshot for ADR-037 (Python port) Design for the proper architectural fix to per-session GameSnapshot divergence in multiplayer. Replaces the band-aid _merge_peer_state_into_snapshot helper with a single canonical snapshot held on SessionRoom and shared by every WS session bound to the slug. Constraint that simplifies scope: no saved MP games exist on disk (multiplayer has never worked end-to-end). No migration path needed. Band-aid + its 5 merge tests are deleted in the same change. Out of scope: ADR-028 LLM rewrites, per-recipient narration region filtering, PlayerState overlay struct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): shared Room.snapshot implementation plan (ADR-037 Python port) Step-by-step plan implementing the spec at docs/superpowers/specs/2026-04-25-shared-room-snapshot-design.md. 9 tasks, 8 commits, ends at 739 passed / 2 skipped on the server + agents sweep. Sequential single-developer plan suitable for inline execution as the Bicycle Repair Man dev agent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(spec): OTEL dashboard restoration design (faithful ADR-031 port) Forensic audit found four root causes of the OTEL dashboard regression since the Rust→Python port: broken `just otel` recipe, ~80% dead SPAN_* constants, missing Layer-3 narrative validator, and impoverished translator. Design specifies a four-phase faithful port that restores full parity with the Rust contract. Approved interactively via /superpowers:brainstorming. Self-review pass fixed an emit-double on json_extraction_result (translator owns; not the validator) and reclassified SPAN_CONTENT_RESOLVE to FLAT_ONLY due to volume. Next: writing-plans for implementation plan. * fix(justfile): point otel recipe at playtest_dashboard.py - Update `just otel` to call `playtest_dashboard.py` via `uv run` instead of the deleted `playtest.py --dashboard-only` - Add `__main__` entry point with argparse to `playtest_dashboard.py` (it was a library-only module with no CLI entry point) - Add `websockets>=12.0` and `rich>=13.0` to orchestrator `pyproject.toml` (required by playtest_dashboard.py, previously missing from manifest) * ci: smoke-test the just otel recipe to catch script renames Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(adr): ADR-090 OTEL Dashboard Restoration after Python Port Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(adr): amend ADR-031 with Python-port section, regen indexes Add observability tag, replace stale Rust-only opening blockquote, and append Python-port note pointing at the canonical telemetry implementation files in sidequest-server. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(plan): OTEL dashboard restoration implementation plan Pairs with the design spec at docs/superpowers/specs/2026-04-25-otel-dashboard-restoration-design.md. This is the 25-task plan (Phase 0–4) executed by the OTEL feat branches across orchestrator, sidequest-server, and sidequest-ui. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(otel): Phase-2 port-audit follow-up after bundles #1 + #2 Discovered-state correction for the original Phase-2 emission-rollouts handoff: the Python port (ADR-082) replaced most Rust info_span!/ start_as_current_span calls with logger.info(...) strings or direct _watcher_publish calls. The SPAN_* constants in spans.py are mostly byte-identical-to-Rust name-string prefixes, not actual span openings. Records: - Which families are genuinely live in Python (the ones that ship now). - Which ~7 bundles in the original handoff are port-dead and require engine porting before routing. - The two real Phase-2 workstreams: migrate live _watcher_publish sites (Workstream A — what bundles #49 and #50 did), vs. port the missing engines (Workstream B — out of scope for telemetry restoration). - Suggested next bundle picks from Workstream A. - Open question on whether to bridge port gaps via log-string parsing (recommended: no — Illusionism violation per ADR-031). Closes the loop after PRs slabgorb/sidequest-server#49 and #50. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
slabgorb
added a commit
that referenced
this pull request
Apr 25, 2026
Phase 2 emission rollout — inventory bundle. Migrates the narrator-extracted ``items_gained`` / ``items_lost`` block of ``server/narration_apply.py`` from a direct ``_watcher_publish`` call to a routed span helper. Stacked on #48 (``feat/otel-dashboard-restoration`` → ``develop``); will auto-retarget once #48 lands. What landed ----------- - New ``SPAN_INVENTORY_NARRATOR_EXTRACTED`` constant + route. The prior ``SPAN_INVENTORY_EXTRACTION`` mirrors the unported Rust ``inventory_extractor`` agent and stays in ``FLAT_ONLY_SPANS`` — the integrated narrator pipeline replaces that standalone agent in the Python port, so its emissions need their own constant. - ``inventory_narrator_extracted_span()`` helper. ``gained`` and ``lost`` are JSON-encoded into span attributes because OTEL silently drops list/dict attribute values; the route ``extract`` returns the JSON strings to the dashboard which already parses this shape. - ``narration_apply.py`` opens the helper after computing the post-mutation ``added_names`` / ``removed_names`` (items_lost is case-insensitive and only records successful matches), so the route payload is byte-identical to what ``_watcher_publish`` sent. ``_watcher_publish`` call deleted (spec §6.6 dedupe). Audit pivot ----------- Original handoff recommended bundle #2 (encounter-lifecycle extras in ``apply_encounter_updates``). Audit found that function has zero production callers — supplanted by the unified ``_apply_narration_result_to_snapshot`` (session_handler.py:2898). Per project policy "do not wire spans into dead functions" (CLAUDE.md "Verify Wiring, Not Just Existence" + the user's explicit constraint), pivoted to bundle #3 (inventory) which has a live caller and validator-correlation value. What stayed flat-only --------------------- ``SPAN_INVENTORY_EXTRACTION`` remains in ``FLAT_ONLY_SPANS`` — port-artifact for the unported Rust standalone extractor agent. Same pattern as ``SPAN_APPLY_WORLD_PATCH`` siblings (#49) and ``SPAN_NPC_REGISTRATION`` siblings (#50). Comment added near the constant. Validation gates ---------------- - ``tests/telemetry/test_routing_completeness.py`` — green (constant added to ``SPAN_ROUTES``; not duplicated in ``FLAT_ONLY_SPANS``). - ``tests/server/test_watcher_events.py`` — green (new translator-routing test for the inventory route, asserts JSON-encoded gained/lost survive the OTEL primitive-types restriction). - ``tests/integration/test_inventory_wiring.py`` — green (3 tests: items_gained, items_lost case-insensitive matching, no double-emission). - Full ``tests/server/ tests/telemetry/ tests/integration/`` suite — 508 passed, 2 skipped (no regressions; +4 vs prior bundle). - ``ruff check`` on touched files — clean. References ---------- - ADR-090 §"Phase 2 emission rollouts" - Audit: ``docs/superpowers/plans/2026-04-25-otel-phase-2-port-audit-followup.md`` (Workstream A, recommended pick #3) - Builds on #49 (state-patch) and #50 (NPC) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
slabgorb
added a commit
that referenced
this pull request
Apr 25, 2026
…validator pipeline (#48) * feat(telemetry): gate ConsoleSpanExporter behind SIDEQUEST_OTEL_CONSOLE Normal server runs no longer spam stdout with span dumps. Console export is only enabled when SIDEQUEST_OTEL_CONSOLE=1 is set explicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(telemetry): add SpanRoute, SPAN_ROUTES, FLAT_ONLY_SPANS scaffolding * feat(telemetry): seed SPAN_ROUTES and FLAT_ONLY_SPANS for live spans Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(watcher): translator emits typed events via SPAN_ROUTES on close Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(telemetry): lint every SPAN_* constant has a routing decision Add tests/telemetry/test_routing_completeness.py — static lint that fails if any SPAN_* constant in spans.py is missing both a SPAN_ROUTES entry and membership in FLAT_ONLY_SPANS. Bulk-add 72 currently-dead (Rust-port) span constants to FLAT_ONLY_SPANS as a Phase 2 deferred baseline. Each Phase 2 family rollout will move entries out of this set and into SPAN_ROUTES with proper extract lambdas. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(telemetry): add turn_span() root context manager * feat(server): open turn_span at dispatch entry to anchor traces Wraps _execute_narration_turn body in turn_span() so every span emitted during a dispatch turn (orchestrator, local_dm, encounter, etc.) fires as a child of the root 'turn' span. Without this, traces are orphaned and the OTEL Timing tab cannot group by turn. Adds tests/server/test_turn_span_wiring.py with two tests: - test_dispatch_opens_turn_span: asserts a 'turn' span is produced and no other span is a true orphan (parent=None without a turn root) - test_turn_span_carries_required_attributes: asserts turn_id, player_id, and agent_name are set on the span per ADR-031 §Layer 2 contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(telemetry): TurnRecord and PatchSummary dataclasses * feat(telemetry): Validator skeleton with bounded queue and lifecycle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(validator): entity_check warns on narration of unknown entities * feat(validator): inventory_check warns on narration/patch mismatches * feat(validator): patch_legality_check flags HP overflow and dead-actor patches Adds patch_legality_check to validator.py (hp > hp_max for characters/NPCs, dead NPC referenced as actor in combat patches) and registers it in Validator.__init__. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(validator): trope_alignment_check warns on beats fired without keywords * feat(validator): subsystem exercise summary + coverage_gap from sliding window Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(validator): emit turn_complete from each TurnRecord Validator._validate now publishes turn_complete (with turn_id, player_id, agent_name, and all key counters) before iterating checks, per spec §6.7. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(validator): periodic heartbeat surfaces queue depth and check timing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(validator): crash-containment and backpressure coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(server): wire Validator lifecycle into FastAPI startup/shutdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(server): assemble TurnRecord at dispatch end and submit to validator Wires the turn hot path to the Layer-3 narrative validator (Task 21). At the end of _execute_narration_turn, a TurnRecord is assembled from the NarrationTurnResult and submitted to self._validator. The validator reference is injected via WebSocketSessionHandler.__init__(validator=...) and passed from app.state.validator at connection time. Protected by try/except so validator failures never crash the dispatch path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(server): consolidate turn_complete emission in validator (ADR-089 §6.7) Removes the dispatch-side turn_complete watcher emit in _execute_narration_turn now that the TurnRecord is forwarded to the validator (Task 21). Per ADR-089 §6.7 the validator owns the turn_complete event. The validator's turn_complete payload is enriched to be the union of fields previously emitted from both sites — turn_id/turn_number, player_id, player_input, agent_name, timing & metering, patches (full + legacy short-form), beats_fired, delta_empty — so existing dashboard consumers keep working. Severity flips to "warning" when is_degraded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(otel/state-patch): route SPAN_QUEST_UPDATE; document port-dead siblings (#49) Phase 2 emission rollout — narrow state-patch bundle to the only constant with a live Python production caller. What landed ----------- - helper: ``quest_update_span`` in ``sidequest/telemetry/spans.py`` (JSON-encodes the updates dict so OTEL primitive-attr restriction doesn't drop it). - route: ``SPAN_ROUTES[SPAN_QUEST_UPDATE]`` → ``state_transition`` / ``component="quest_log"`` with the same payload shape the prior direct ``publish_event`` carried — dashboard sees no change. - migration: ``sidequest/server/narration_apply.py`` quest_log block now opens ``quest_update_span`` and the inline ``_watcher_publish`` call is deleted (spec §6.6 dedupe rule). - tests: * ``tests/server/test_watcher_events.py`` — translator routing row (fake span → typed event). * ``tests/integration/test_state_patch_wiring.py`` — drives ``_apply_narration_result_to_snapshot`` end-to-end through a real ``WatcherSpanProcessor`` and asserts exactly one ``state_transition`` with ``component=quest_log`` reaches the hub. Uses a monkeypatched ``tracer()`` because OTEL forbids replacing an already-installed global provider mid-suite. What stayed flat-only --------------------- ``SPAN_APPLY_WORLD_PATCH``, ``SPAN_BUILD_PROTOCOL_DELTA``, and ``SPAN_COMPUTE_DELTA`` remain in ``FLAT_ONLY_SPANS`` with port-artifact comments. Audit found no production caller for any of the three: ``apply_world_patch`` lives in ``game/session.py`` but the Python port inlined those mutations directly in ``narration_apply.py``; ``build_protocol_delta`` doesn't exist as a function (only mentioned in a docstring); ``compute_delta`` is exported from ``game/delta.py`` but called only from tests. Wiring spans into dead functions would violate CLAUDE.md "Verify Wiring, Not Just Existence" — flagged for follow-up: either delete the constants or wire the helpers through the dispatch loop (not in scope for this bundle). Validation gates ---------------- - ``tests/telemetry/test_routing_completeness.py`` — green (constant moved out of FLAT_ONLY_SPANS; new route registered). - ``tests/server/test_watcher_events.py`` — green. - ``tests/integration/test_state_patch_wiring.py`` — green. - Full ``tests/server/ tests/telemetry/ tests/integration/`` suite — 500 passed, 2 skipped (no regressions). - ``ruff check`` on touched files — clean. ADR-090 §"Phase 2 emission rollouts"; handoff ``docs/superpowers/plans/2026-04-25-otel-phase-2-emission-rollouts-HANDOFF.md`` bundle #1 (state-patch). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(otel/npc): route SPAN_NPC_AUTO_REGISTERED + SPAN_NPC_REINVENTED (#50) Phase 2 emission rollout — NPC bundle. Both auto_registered and reinvented identity-drift paths now emit through SPAN_ROUTES instead of direct ``publish_event`` calls. What landed ----------- - helpers: ``npc_auto_registered_span``, ``npc_reinvented_span`` in ``sidequest/telemetry/spans.py``. Use ``npc_name`` (not ``name``) for the entity attribute to avoid colliding with the OTEL span ``name`` reserved attribute. - routes: ``SPAN_NPC_AUTO_REGISTERED`` and ``SPAN_NPC_REINVENTED`` → ``state_transition`` / ``component=npc_registry`` carrying ``op=auto_registered`` and ``op=reinvented`` respectively. Same payload shape the dashboard already consumes. - watcher: ``WatcherSpanProcessor`` gains a span-attribute escape hatch for severity. When the helper sets ``severity=warning`` as an attribute the typed event surfaces ``severity=warning`` instead of the OTEL-status-derived default. Required because OTEL Status only carries OK/ERROR — there's no native warning level. - migration: ``narration_apply.py`` opens ``npc_auto_registered_span`` and ``session_helpers._detect_npc_identity_drift`` opens ``npc_reinvented_span``. Both inline ``_watcher_publish`` calls are deleted (spec §6.6 dedupe). Now-unused ``publish_event`` import removed from ``session_helpers.py``. - tests: * Translator routing rows for both routes in ``tests/server/test_watcher_events.py``, including a dedicated test that ``severity=warning`` propagates from span attribute to typed event. * ``test_state_transition_fires_on_npc_auto_register`` updated to install the local TracerProvider+WatcherSpanProcessor, so it still exercises the production code path after migration. * New wiring integration test ``tests/integration/test_npc_wiring.py`` drives both ``_apply_narration_result_to_snapshot`` (auto) and ``_detect_npc_identity_drift`` (reinvented) end-to-end through a real ``WatcherSpanProcessor`` and asserts exactly one ``state_transition`` per path with the right component/op/severity. What stayed flat-only --------------------- ``SPAN_NPC_REGISTRATION`` and ``SPAN_NPC_MERGE_PATCH`` remain in ``FLAT_ONLY_SPANS``. Audit found no production caller for either — the Python port did not port the Rust ``register_npc`` / ``merge_npc_patch`` helpers; ``narration_apply.py`` mutates ``npc_registry`` directly. Same port-artifact pattern documented for state-patch in the prior PR. Flagged for follow-up. Validation gates ---------------- - ``tests/telemetry/test_routing_completeness.py`` — green. - ``tests/server/test_watcher_events.py`` — green (4 NPC-related tests including warning-severity propagation). - ``tests/integration/test_npc_wiring.py`` — green (auto + reinvented). - ``tests/server/test_npc_identity_drift.py`` — green (no regression). - Full ``tests/server/ tests/telemetry/ tests/integration/`` suite — 504 passed, 2 skipped (no regressions; +4 vs prior bundle). - ``ruff check`` on touched files — clean. ADR-090 §"Phase 2 emission rollouts"; handoff bundle #2 (NPC). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(otel/inventory): route SPAN_INVENTORY_NARRATOR_EXTRACTED (#52) Phase 2 emission rollout — inventory bundle. Migrates the narrator-extracted ``items_gained`` / ``items_lost`` block of ``server/narration_apply.py`` from a direct ``_watcher_publish`` call to a routed span helper. Stacked on #48 (``feat/otel-dashboard-restoration`` → ``develop``); will auto-retarget once #48 lands. What landed ----------- - New ``SPAN_INVENTORY_NARRATOR_EXTRACTED`` constant + route. The prior ``SPAN_INVENTORY_EXTRACTION`` mirrors the unported Rust ``inventory_extractor`` agent and stays in ``FLAT_ONLY_SPANS`` — the integrated narrator pipeline replaces that standalone agent in the Python port, so its emissions need their own constant. - ``inventory_narrator_extracted_span()`` helper. ``gained`` and ``lost`` are JSON-encoded into span attributes because OTEL silently drops list/dict attribute values; the route ``extract`` returns the JSON strings to the dashboard which already parses this shape. - ``narration_apply.py`` opens the helper after computing the post-mutation ``added_names`` / ``removed_names`` (items_lost is case-insensitive and only records successful matches), so the route payload is byte-identical to what ``_watcher_publish`` sent. ``_watcher_publish`` call deleted (spec §6.6 dedupe). Audit pivot ----------- Original handoff recommended bundle #2 (encounter-lifecycle extras in ``apply_encounter_updates``). Audit found that function has zero production callers — supplanted by the unified ``_apply_narration_result_to_snapshot`` (session_handler.py:2898). Per project policy "do not wire spans into dead functions" (CLAUDE.md "Verify Wiring, Not Just Existence" + the user's explicit constraint), pivoted to bundle #3 (inventory) which has a live caller and validator-correlation value. What stayed flat-only --------------------- ``SPAN_INVENTORY_EXTRACTION`` remains in ``FLAT_ONLY_SPANS`` — port-artifact for the unported Rust standalone extractor agent. Same pattern as ``SPAN_APPLY_WORLD_PATCH`` siblings (#49) and ``SPAN_NPC_REGISTRATION`` siblings (#50). Comment added near the constant. Validation gates ---------------- - ``tests/telemetry/test_routing_completeness.py`` — green (constant added to ``SPAN_ROUTES``; not duplicated in ``FLAT_ONLY_SPANS``). - ``tests/server/test_watcher_events.py`` — green (new translator-routing test for the inventory route, asserts JSON-encoded gained/lost survive the OTEL primitive-types restriction). - ``tests/integration/test_inventory_wiring.py`` — green (3 tests: items_gained, items_lost case-insensitive matching, no double-emission). - Full ``tests/server/ tests/telemetry/ tests/integration/`` suite — 508 passed, 2 skipped (no regressions; +4 vs prior bundle). - ``ruff check`` on touched files — clean. References ---------- - ADR-090 §"Phase 2 emission rollouts" - Audit: ``docs/superpowers/plans/2026-04-25-otel-phase-2-port-audit-followup.md`` (Workstream A, recommended pick #3) - Builds on #49 (state-patch) and #50 (NPC) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
5 tasks
slabgorb
added a commit
that referenced
this pull request
Apr 25, 2026
Phase 2 emission rollout — audio bundle (Workstream A pick #1 from the port-audit follow-up). Migrates all 5 audio ``_watcher_publish`` sites in ``server/session_handler.py`` to routed span helpers. High-volume during play; lights up the GM panel's ``audio`` Subsystems-tab component immediately. Branches off ``develop`` directly (the prior restoration branch landed in #48). What landed ----------- - 4 new constants + routes: * ``SPAN_AUDIO_BACKEND_ENABLED`` — backend init success (genre, mood_count, sfx_count). * ``SPAN_AUDIO_BACKEND_DISABLED`` — backend init bail (``pack_dir_missing`` / ``empty_config``). * ``SPAN_AUDIO_SKIPPED`` — per-turn skip (``no_audio_config`` / ``no_narration`` / ``empty_cues`` / ``error``). The original ``_audio_skip`` helper accepted a flexible ``extra`` dict; the span attribute is JSON-encoded (``extra_json``) because OTEL silently drops dict attribute values, and the route extract returns the JSON string for dashboard parity. * ``SPAN_AUDIO_DISPATCHED`` — per-turn cue dispatch (turn_number, mood, music_track, sfx_count). - 4 helpers: ``audio_backend_enabled_span``, ``audio_backend_disabled_span``, ``audio_skipped_span``, ``audio_dispatched_span``. ``mood`` / ``music_track`` from ``AudioCuePayload`` can be ``None``; the dispatched helper coerces to ``""`` so OTEL doesn't drop the attribute. - ``session_handler.py`` migrations: ``_build_audio_backend`` opens enabled/disabled spans (3 sites collapsed); ``_audio_skip`` and ``_audio_dispatched`` open their respective spans. All 5 ``_watcher_publish`` calls deleted (spec §6.6 dedupe). What stayed flat-only --------------------- ``SPAN_MUSIC_EVALUATE`` / ``SPAN_MUSIC_CLASSIFY_MOOD`` remain in ``FLAT_ONLY_SPANS`` — they mirror the unported Rust ``music_director`` agent (the integrated cue pipeline replaces it in the port). Comment added near the constant. Same port-artifact pattern as ``SPAN_INVENTORY_EXTRACTION`` (#52), ``SPAN_NPC_REGISTRATION`` (#50), and ``SPAN_APPLY_WORLD_PATCH`` siblings (#49). Tests ----- - 4 translator-routing tests in ``tests/server/test_watcher_events.py`` (one per audio route) using a shared fake-span factory + capturing hub harness. - 5 wiring integration tests in ``tests/integration/test_audio_wiring.py`` driving ``WebSocketSessionHandler._audio_skip`` and ``_audio_dispatched`` end-to-end through a real ``WatcherSpanProcessor``. The handler methods don't read ``self``; called as unbound methods with a ``MagicMock`` to side-step ``__init__``. Validation gates ---------------- - ``tests/telemetry/test_routing_completeness.py`` — green (4 new constants present in ``SPAN_ROUTES``; not duplicated in ``FLAT_ONLY_SPANS``). - ``tests/server/test_watcher_events.py`` — green (4 new audio routes). - ``tests/integration/test_audio_wiring.py`` — green (5 tests). - Full ``tests/server/ tests/telemetry/ tests/integration/`` suite — 524 passed, 2 skipped (no regressions; +9 vs prior bundle). - ``ruff check`` — clean on all touched files for the diff range. Two pre-existing SIM102/SIM105 errors at session_handler.py lines 594 (committed 2026-04-23) and 1031 (committed 2026-04-20) are unrelated to this bundle and predate it; flagged as out of scope for follow-up. References ---------- - ADR-090 §"Phase 2 emission rollouts" - Audit: ``docs/superpowers/plans/2026-04-25-otel-phase-2-port-audit-followup.md`` (Workstream A, recommended pick #1) - Builds on #48 (restoration), #49 (state-patch), #50 (NPC), #52 (inventory) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 emission rollout — narrow state-patch bundle to the only constant with a live Python production caller.
Stacked on #48 (
feat/otel-dashboard-restoration→develop). Will retarget todevelopautomatically once #48 merges.What landed
quest_update_span()helper insidequest/telemetry/spans.py(JSON-encodes the updates dict so OTEL's primitive-attribute restriction doesn't drop it).SPAN_ROUTES[SPAN_QUEST_UPDATE]→state_transition/component="quest_log"carrying the same payload shape the prior directpublish_eventdid — dashboard sees no shape change.sidequest/server/narration_apply.pyquest_log block now opensquest_update_span; the inline_watcher_publishcall is deleted (spec §6.6 dedupe rule).tests/server/test_watcher_events.py.tests/integration/test_state_patch_wiring.py— drives_apply_narration_result_to_snapshotend-to-end through a realWatcherSpanProcessorand asserts exactly onestate_transitionwithcomponent=quest_logreaches the hub. Uses a monkeypatchedtracer()because OTEL forbids replacing an already-installed global provider mid-suite.What stayed flat-only
SPAN_APPLY_WORLD_PATCH,SPAN_BUILD_PROTOCOL_DELTA, andSPAN_COMPUTE_DELTAremain inFLAT_ONLY_SPANSwith port-artifact comments. Audit found no production caller for any of the three:apply_world_patchlives ingame/session.pybut the Python port inlined those mutations directly innarration_apply.py.build_protocol_deltadoesn't exist as a function (only mentioned in a docstring).compute_deltais exported fromgame/delta.pybut called only from tests.Wiring spans into dead functions would violate
CLAUDE.md"Verify Wiring, Not Just Existence" — flagged for follow-up: either delete the constants or wire the helpers through the dispatch loop. Not in scope for this bundle.Validation gates
tests/telemetry/test_routing_completeness.py— green (constant moved out ofFLAT_ONLY_SPANS; new route registered).tests/server/test_watcher_events.py— green.tests/integration/test_state_patch_wiring.py— green.tests/server/ tests/telemetry/ tests/integration/suite — 500 passed, 2 skipped, no regressions.ruff checkon touched files — clean.References
docs/superpowers/plans/2026-04-25-otel-phase-2-emission-rollouts-HANDOFF.mdbundle Story 41-0: Scaffold sidequest-server #1 (state-patch)Test plan
_apply_narration_result_to_snapshotcall)ruff checkclean🤖 Generated with Claude Code