Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(KR-P2-I-integration ST5): flip /api/operational-state stub → real + add history ring#35

Merged
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR-P2-I-integration-st5-flip-endpoint
May 21, 2026
Merged

feat(KR-P2-I-integration ST5): flip /api/operational-state stub → real + add history ring#35
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR-P2-I-integration-st5-flip-endpoint

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

KR-P2-I-integration ST5 of 5. Flips /api/operational-state from the hardcoded stub to a live read of the OperationalStateHolder, and adds the in-memory transition-history ring that the admin panel renders. Stacked on top of #34 (ST3) — base is feat/kora-KR-P2-I-integration-st3-provider-wire. Retarget to main once ST3 merges.

Two changes in this PR

1. OperationalStateHolder — transition history ring buffer

  • New TransitionRecord frozen dataclass: (timestamp, from_state, to_state, trigger). to_dict() matches the endpoint's transition_history row shape.
  • _history: deque[TransitionRecord] with maxlen=10. Appended under the holder lock at the end of transition_to (after the held-state swap) so ordering matches the swap.
  • New holder.history(limit=10) returns a snapshot list of dicts.
  • Bounded at 10 deliberately — durable history lives in the chain-event log (every transition writes kora.operational_state.transitioned via ST2's emit listener). The ring is for the admin panel's immediate-history view and dies on process restart, matching Hermes-fork session-state semantics.

2. /api/operational-state — endpoint flip

  • When get_holder() is initialized: response carries the live state. transition_history from holder.history(limit=10); valid_next_states derived from transitions_from(state.primary_state); the stub flag is dropped. CC#2's admin panel auto-stops rendering its stub banner when the field is absent.
  • When get_holder() returns None (boot incomplete, or the IsoKron provider failed to construct): response uses the stub-shape with stub: True PLUS an error field set to "OperationalStateHolder not yet initialized". The panel can render this distinctly from the cold-stub banner so operators can distinguish "no wire-in yet" from "real runtime is up."

Tests — tests/test_operational_state_endpoint.py (266 LOC)

8 cases:

Ring buffer mechanics

  • transition_to appends the (from, to, trigger) record under the lock
  • Ring is bounded at 10 — 12 transitions ⇒ oldest 2 dropped
  • history(limit=N) clamps the snapshot size; asking for more than we have returns all
  • TransitionRecord.to_dict() shape matches the endpoint dict
  • Timestamps are ISO-8601 UTC (%Y-%m-%dT%H:%M:%SZ) — round-trippable

Endpoint flip

  • Uninitialized holder branch: stub: True + error field + empty history/next-states
  • Initialized branch: no stub key, no error key, live state surfaces correctly
  • degradation_reasons rendered alphabetically (cockpit diff stability)
  • All 6 keys from the v1 stub still present on the live path — CC#2's panel wiring stays compatible

Honest scope

  • No CC#2 OPS-PANEL changes. Per spec §6 ship checklist: "CC#2's OPS-PANEL still renders correctly after stub-flip (manual smoke check on flyctl proxy 9119:9119 or equivalent local run)." I haven't touched the panel; if any banner stops rendering correctly after this lands, that's a CC#2-side fix.
  • In-memory ring resets on restart. The chain-event log is the durable source of truth. The ring is intentionally throwaway.

Sub-task chain (this bucket)

ST PR Status
ST1 #32 open
ST2 #33 open (stacked on ST1)
ST3 #34 open (stacked on ST2)
ST4 STOP-gated — depends on KR-P2-E ST1 (CC#1) merging first; not yet on main and no open PR
ST5 this PR open (stacked on ST3)

Test plan

  • CI green on pytest tests/test_operational_state_endpoint.py
  • Manual smoke (CC#2 panel compatibility): after merge, run flyctl proxy 9119:9119 against a live Kora and confirm the admin panel renders the operational-state card with no stub banner. Trigger a synthetic transition (e.g. via a future test command) and watch the history ring populate
  • Manual smoke (uninitialized branch): boot with MEMORY_PROVIDER unset so the IsoKron provider never constructs; confirm the panel renders the new OperationalStateHolder not yet initialized banner instead of the cold-stub banner

🤖 Generated with Claude Code

@rafe-walker rafe-walker changed the base branch from feat/kora-KR-P2-I-integration-st3-provider-wire to main May 21, 2026 20:57
@rafe-walker rafe-walker force-pushed the feat/kora-KR-P2-I-integration-st3-provider-wire branch from 7d571d8 to 244f373 Compare May 21, 2026 20:57
…l + add history ring

OperationalStateHolder (ST1 module) now retains the last 10
transitions in an in-memory deque ring buffer:

  - TransitionRecord dataclass carries (timestamp, from_state,
    to_state, trigger).
  - Appended under the holder lock at the end of transition_to
    (after the held-state swap) so ordering matches the swap.
  - holder.history(limit=N) returns a list of dicts ready for the
    /api/operational-state JSON payload.
  - Bounded at 10 — durable history lives in the chain-event log
    (every transition writes kora.operational_state.transitioned
    via ST2's emit listener); this ring is for the admin panel's
    immediate-history view and dies on process restart.

kora_cli/web_server.py — endpoint flipped:

  - Now reads agent.operational_state_holder.get_holder().
  - When holder is initialized: returns the live state with
    transition_history sourced from holder.history(limit=10),
    valid_next_states from transitions_from(state.primary_state),
    and the ``stub`` flag DROPPED — CC#2's admin panel auto-stops
    rendering its stub banner.
  - When holder is None (boot incomplete or IsoKron provider
    failed): returns the stub-shape with stub:True PLUS an
    ``error`` field naming the cause, so the panel can distinguish
    "no wire-in yet" from "real runtime is up".

Tests (tests/test_operational_state_endpoint.py): 8 cases covering
the ring mechanics (append, bound at 10, limit clamp, ISO-8601 UTC
timestamp, TransitionRecord shape), the uninitialized-holder
endpoint branch (stub + error), the live endpoint branch (no stub
key, payload keys match pre-flip shape, transition_history and
valid_next_states populated from the holder + table), and the
sorted-degradation_reasons cockpit-diff stability invariant.

Stacked on ST3 (#34); base is feat/kora-KR-P2-I-integration-st3-provider-wire.
Retarget to main once ST3 merges. ST4 (SeaTicketPoller wire-in)
is independently blocked on KR-P2-E ST1 having landed — STOP gate
filed in chat, not in this PR's scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker force-pushed the feat/kora-KR-P2-I-integration-st5-flip-endpoint branch from 6822ecd to f35ae85 Compare May 21, 2026 20:58
@rafe-walker rafe-walker merged commit f5df2a6 into main May 21, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-P2-I-integration-st5-flip-endpoint branch May 21, 2026 20:59
rafe-walker added a commit that referenced this pull request May 21, 2026
… to PR #35 two-branch shape (#64)

Full rewrite around PR #35's stub-true-when-uninit / live-when-initialized response shape. New autouse _reset_holder_for_tests fixture (the missing piece that caused the original 4 intermittent failures — holder singleton survived across tests). Tests-only, no production edits. Closes task #191 + the 4 pre-existing failures.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant