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

feat(KR-P2-CLEANUP ST2): SEA-PANEL flip-to-real + active_provider singleton#57

Merged
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR-P2-CLEANUP-st2-sea-panel-flip
May 21, 2026
Merged

feat(KR-P2-CLEANUP ST2): SEA-PANEL flip-to-real + active_provider singleton#57
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR-P2-CLEANUP-st2-sea-panel-flip

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

KR-P2-CLEANUP ST2 of 5. Flips /api/sea-tickets/kora-assigned from the hardcoded stub to a live read against the gateway-level IsoKronMemoryProvider, with a graceful fallback to the stub shape (+ error field) when no active provider is registered.

Shared infrastructure (also used by ST3 + ST4)

plugins/memory/isokron/active_provider.py (new)

Process-wide singleton accessor for the live IsoKronMemoryProvider. Mirrors the operational_state_holder pattern: one initialized provider per process, accessed by anything that needs to read substrate state without going through an agent session.

  • set_active_provider(provider) — called once at gateway boot
  • get_active_provider() -> Optional[IsoKronMemoryProvider] — read by cross-cutting endpoints
  • clear_active_provider() — test-only reset

Why not lazy load_memory_provider("isokron") per request? The plugin loader returns a fresh uninitialized instance each call (the plugin's register(ctx) constructs anew). Initializing per request would open new asyncpg pools + MCP transports each time. The singleton holds the ALREADY-initialized provider for re-use.

sea_ticket_poller_lifecycle.py — set hook

After provider.initialize() succeeds, set_active_provider(provider) runs. Fail-soft: a registration failure logs but doesn't break the poller startup.

SEA-PANEL read

plugins/memory/isokron/assigned_sea_tickets.py (new, 273 LOC)

Queries public.tickets for sea-tickets assigned to Kora's actor_id, groups client-side into the four panel buckets:

Bucket Selector
in_progress sea_active_claim_token IS NOT NULL
queued No active claim, neither resolved nor failed
recently_resolved sea_status IN ('completed', 'released', 'failed_retryable') (capped at 20)
failed_or_blocked sea_status IN ('failed_terminal', 'blocked_needs_operator')

get_assigned_sea_tickets_via_provider() resolves the actor_id lazily via actor_registry JOIN (mirrors the poller's helper), runs the read on the IsoKron dedicated I/O loop. Returns None on any failure; caller falls back to stub + error.

Endpoint flip — kora_cli/web_server.py

  • Live branch: returns real data WITHOUT stub flag (cockpit auto-stops rendering the stub banner).
  • Uninitialized branch: returns stub-shape with stub: True + error field naming the cause (cockpit renders a distinct banner — "no provider yet" vs cold-stub).

Stub block extracted into _kora_assigned_sea_tickets_stub(error=...) so both fallback paths share the same shape.

Honest scope — deferred fields

The stub returned a couple of cockpit-friendly fields the substrate doesn't carry on tickets:

  • model_tier_used — requires a kora.sea_ticket.resolved chain-event JOIN per row. ST2 returns None; panel renders "unknown". A future "denormalize last model_tier onto tickets" change can backfill.
  • failure_count_by_reason — requires an event_log aggregation. ST2 returns {}.

Both non-blocking for cockpit rendering — the absence is visible but doesn't break the page.

Tests — tests/kora_cli/test_web_server_sea_tickets.py (full rewrite, 258 LOC)

Both branches:

  • Uninitialized branch: stub + stub:True + error field + resolution-enum sanity
  • Live branch (monkeypatched helper): real data passes through; neither stub nor error field appears
  • Live read returning None: falls back to stub with the "substrate read returned None" error
  • claim_fence_token regression guard: never appears in either branch
  • active_provider singleton round-trip

Sub-task chain (this bucket — all independent)

ST PR Status
ST1 #55 open
ST2 this PR open (branches from main; uses active_provider shipped here)
ST3 next branches from main; reuses active_provider once this lands
ST4 next branches from main; reuses active_provider once this lands
ST5 next independent test fix

Test plan

  • CI green on pytest tests/kora_cli/test_web_server_sea_tickets.py
  • Manual smoke after merge: gateway boots, IsoKron provider initializes, admin panel /api/sea-tickets/kora-assigned returns live data + stub banner stops rendering
  • Manual smoke: temporarily disable the IsoKron provider config → endpoint returns stub-shape + error field; panel renders the distinct uninitialized-state banner

🤖 Generated with Claude Code

…gleton

Flips /api/sea-tickets/kora-assigned from the hardcoded stub to a
live read against the gateway-level IsoKronMemoryProvider, with a
graceful fallback to the stub shape (+ ``error`` field) when no
active provider is registered.

Shared infrastructure:
  - New ``plugins/memory/isokron/active_provider.py`` —
    process-wide singleton accessor for the live, initialized
    IsoKronMemoryProvider. Mirrors the operational_state_holder
    pattern. Set once at gateway boot
    (sea_ticket_poller_lifecycle.build_and_start_sea_ticket_poller);
    read by cross-cutting endpoints (this ST + ST3/ST4 will share).
  - sea_ticket_poller_lifecycle.py now calls set_active_provider
    after provider.initialize() succeeds (fail-soft: a registration
    failure logs but doesn't break the poller startup).

SEA-PANEL read:
  - New ``plugins/memory/isokron/assigned_sea_tickets.py`` —
    queries public.tickets for sea-tickets assigned to Kora's
    actor_id, groups in Python into the four panel buckets
    (in_progress / queued / recently_resolved / failed_or_blocked).
    Shape matches the v1 stub exactly so the cockpit panel renders
    without changes.
  - get_assigned_sea_tickets_via_provider() resolves the actor_id
    lazily via actor_registry JOIN (mirrors the poller's helper),
    runs the read on the IsoKron dedicated I/O loop, projects rows
    into the panel-shaped buckets. Returns None on any failure;
    caller falls back to stub + error.

Endpoint (kora_cli/web_server.py):
  - GET /api/sea-tickets/kora-assigned now reads
    get_active_provider() + delegates to the helper. Live branch
    returns real data WITHOUT ``stub`` flag (cockpit auto-stops
    rendering the stub banner). Uninitialized branch returns
    stub-shape with ``stub:True`` + ``error`` naming the cause
    (cockpit renders a distinct banner so operators can tell
    "no provider yet" from cold-stub).
  - Stub block extracted into _kora_assigned_sea_tickets_stub
    helper so both fallback paths share the same shape.

# Honest scope — panel-side richness deferred

The stub returned a few cockpit-friendly fields the substrate
doesn't directly carry on tickets:
  - model_tier_used — requires a kora.sea_ticket.resolved
    chain-event JOIN per row; ST2 returns None and the panel
    renders "unknown". A future "denormalize last model_tier
    onto tickets" can backfill.
  - failure_count_by_reason — requires an event_log aggregation;
    ST2 returns {}.
Both are non-blocking for cockpit rendering — the absence is
visible but doesn't break the page.

Tests (tests/kora_cli/test_web_server_sea_tickets.py): full rewrite
covering both branches:
  - Uninitialized branch: stub-shape + stub:True + error field +
    resolution-enum sanity
  - Live branch: monkeypatch the helper to return canned data;
    assert real data passes through + neither stub nor error
    appears
  - Live read returning None: falls back to stub with the
    "substrate read returned None" error message
  - claim_fence_token never appears in either branch (regression
    guard from §4 non-scope)
  - active_provider singleton set/get/clear round-trip

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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