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

feat(kora): KR-DAEMON-PHASE-2.5-AND-PHASE-3 — 3 promotion listeners + 3 singleton-holders#200

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-DAEMON-PHASE-2.5-AND-PHASE-3-MEGABUCKET
May 24, 2026
Merged

feat(kora): KR-DAEMON-PHASE-2.5-AND-PHASE-3 — 3 promotion listeners + 3 singleton-holders#200
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-DAEMON-PHASE-2.5-AND-PHASE-3-MEGABUCKET

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Two phases of daemon migration in one bucket. After this lands: 12/12 periodic-task listeners + 3/3 singleton-holders on the gateway path. Only HTTP service-mounts (web/mcp/webhooks — stay Kora-local per audit §4.2) + heartbeat scheduler dissolution remain in the remaining Phase 4-6 work.

Per-listener migration table

Phase 2.5 — 3 promotion listeners (pure-periodic-task, no Listener class)

Listener Hermes name Periodic task Callback Kora-side entry?
promote_probe_fix_envelopes promote_probe_fix_envelopes promote_probe_fix_envelopes_cycle _periodic_task No (pure-periodic; never had one)
promote_router_tuning promote_router_tuning promote_router_tuning_cycle _periodic_task No (pure-periodic)
promote_tool_trimming promote_tool_trimming promote_tool_trimming_cycle _periodic_task No (pure-periodic)

Same shape as promote_phrasebook + promote_snapshot_expand migrated in #199: no-op startup/shutdown wrappers + BackgroundDaemonEntry carrying only the periodic task.

Phase 3 — 3 singleton-holders (Listener-class shape; Path B thin-shim)

Listener Hermes name Periodic task Singleton accessor Notes
slack_client slack_client None (event-driven) current_slack_client() Fail-soft on missing auth env; daemon boots regardless
purelymail_client purelymail_client None (event-driven) current_purelymail_client() Outbound SMTP (NOT IMAP poller — that's email_inbound_imap per #199 clarification); fail-soft on missing auth env
reasoning_engine reasoning_engine None (event-driven) current_reasoning_engine() FATAL on startup failure (see implementation choice below)

Each gets a process-wide _listener_singleton so both registries point at the same instance's bound methods. startup() accepts optional coordinator kwarg for the Hermes consumer signature.

FATAL-semantic implementation choice (operator visibility)

The bucket spec called out reasoning_engine's FATAL contract as critical: engine startup failure MUST abort daemon boot (existing pre-Phase-3 semantic). The STOP-ASK condition was "if preserving this requires a new Hermes extension".

Chosen path: documentation-driven contract. The listener's module docstring now contains an explicit "KR-DAEMON-LISTENERS-VIA-GATEWAY Phase 3 — FATAL semantic contract" section that requires future gateway-side consumers to propagate any startup exception. The Hermes-side BackgroundDaemonRegistry surface stays unchanged (no new startup_failure_is_fatal: bool field).

Rationale:

  • Today's lifecycle is still driven by Kora's DaemonCoordinator (Path B thin-shim) — Kora behavior unchanged; FATAL contract already honored.
  • The future gateway-side consumer doesn't exist yet; adding a flag pre-emptively for a hypothetical consumer is YAGNI.
  • If/when that consumer lands and needs structured FATAL hinting, the additive flag can land alongside the consumer code that actually reads it. Avoids drift between an extension surface and a consumer that doesn't exist.
  • Minimizes Hermes-side surface change in this bucket (zero new extensions; pure consumer-side change).

Operator: push back if you prefer the additive-flag approach. I can land that in a follow-up bucket (~30 min — adds the field to BackgroundDaemonEntry, sets it True on the reasoning_engine entry, no consumer change needed today; future consumer reads it). I went with the conservative path because the spec's STOP-ASK condition was conditional ("if … requires a new Hermes extension") and the answer is no — it's preservable via consumer discipline + documentation. But happy to flip if you want structural enforcement.

Tests

  • 12 new tests in tests/kora_cli/test_listeners/test_phase_2_5_and_3_migrations.py:
    • Phase 2.5 / Phase 3 inventory pins (both registries)
    • Periodic-task callback identity for the 3 Phase 2.5 listeners
    • Singleton-holders carry no periodic_task (event-driven contract pin)
    • Singleton invariant: both registries point at the same listener instance
    • Startup signature accepts optional coordinator kwarg
    • reasoning_engine FATAL contract: documented in docstring + behavioral pin that startup-raise propagation is preserved (unsetting both credential envs → ReasoningEngineError)
  • 586/586 focused regression set green (all listener tests + all kora_hermes_plugin tests + cost_ladder + cost_telemetry + identity)

Test plan

  • Local: 75 phase-migration tests + 586 focused regression set all green
  • Smoke-imported all listeners; all 15 expected entries (1 Phase 1 + 8 Phase 2 + 3 Phase 2.5 + 3 Phase 3) present in BackgroundDaemonRegistry
  • Pre-merge: review the FATAL-semantic implementation choice (above) and signal preference
  • Post-merge: 48h monitoring window for any listener-lifecycle surprise on the gateway path
  • Rollback plan: revert this PR; Kora-side LISTENER_REGISTRY entries stay intact (Path B keeps the back-compat path live), so no production behavior changes

Remaining daemon migration work (post-merge state)

Per kora-docs audit §5:

  • Phase 4 (3 HTTP service-mounts: web / mcp / webhooks) — stays Kora-local per audit recommendation; no migration needed
  • Phase 5 (heartbeat scheduler dissolution) — collapse the scheduler into the gateway main loop's per-entry PeriodicTaskSpec consumer
  • Phase 6 (DaemonCoordinator final shim removal) — once gateway consumer is mature, dissolve the Kora-side LISTENER_REGISTRY entirely

🤖 Generated with Claude Code

… 3 singleton-holders

After this: 12/12 periodic-task listeners + 3/3 singleton-holders on the gateway path. Only HTTP service-mounts (web/mcp/webhooks — stay Kora-local per audit §4.2) + heartbeat scheduler dissolution remain.

Phase 2.5 (3 pure-periodic-task listeners — same shape as promote_phrasebook + promote_snapshot_expand from #199):
  - promote_probe_fix_envelopes
  - promote_router_tuning
  - promote_tool_trimming

Each: no-op startup/shutdown wrappers + BackgroundDaemonEntry with periodic_task carrying the existing callback + interval. No Kora-side LISTENER_REGISTRY entry (never had one). Defensive duplicate-registration guard for importlib.reload / xdist workers.

Phase 3 (3 singleton-holders — Path B thin-shim same as Phase 2 Listener-class listeners):
  - slack_client (event-driven; no periodic_task)
  - purelymail_client (event-driven; no periodic_task; outbound SMTP — distinct from email_inbound_imap IMAP poller per #199 clarification)
  - reasoning_engine (event-driven; no periodic_task; FATAL on startup failure)

Each gets the same dual-registry pattern from Phase 1/2: process-wide _listener_singleton, startup() accepts optional coordinator kwarg, BackgroundDaemonEntry with shared bound methods, defensive duplicate-registration guard.

reasoning_engine FATAL semantic — implementation choice
=======================================================

The bucket spec called out the FATAL contract as critical: engine startup failure MUST abort daemon boot (existing semantic). The STOP-ASK condition was "if preserving this requires a new Hermes extension".

Chosen path: documentation-driven contract. The listener's module docstring now contains an explicit "KR-DAEMON-LISTENERS-VIA-GATEWAY Phase 3 — FATAL semantic contract" section that requires future gateway-side consumers to propagate any startup exception (vs swallowing it silently). The Hermes-side BackgroundDaemonRegistry surface stays unchanged.

Rationale:
  - Today's lifecycle is still driven by Kora's DaemonCoordinator (Path B thin-shim) which already honors the FATAL contract — Kora behavior unchanged.
  - The future gateway consumer doesn't exist yet; adding a startup_failure_is_fatal flag pre-emptively is YAGNI.
  - If/when the gateway consumer lands and needs structured FATAL hinting, the additive flag can land in that bucket alongside the consumer code that uses it.
  - This keeps the Hermes surface area minimal and avoids drift between extension surfaces and their consumers.

Documented this choice in the listener docstring + flagged in PM hand-off for operator awareness. Operator can dispatch a follow-up bucket if they prefer the additive-flag approach instead.

Tests
=====

12 new tests in tests/kora_cli/test_listeners/test_phase_2_5_and_3_migrations.py:
  - Phase 2.5 / Phase 3 inventory pins (both registries)
  - Periodic-task callback identity for the 3 Phase 2.5 listeners
  - Singleton-holders carry no periodic_task (event-driven contract)
  - Singleton invariant: both registries point at the same listener instance's bound methods
  - Startup signature accepts optional coordinator kwarg
  - reasoning_engine FATAL contract: documented in docstring + startup-raise propagation behavior preserved

586/586 focused regression set green (all listener tests + all kora_hermes_plugin tests + cost_ladder + cost_telemetry + identity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit 5c85d75 into feature/phase2-upgrades May 24, 2026
2 of 4 checks passed
@rafe-walker rafe-walker deleted the feat/kora-KR-DAEMON-PHASE-2.5-AND-PHASE-3-MEGABUCKET branch May 24, 2026 08:57
rafe-walker added a commit that referenced this pull request May 24, 2026
…n pip POC + structural FATAL flag (#204)

Two deliverables closing #203's §6.4 gap (no pip-install path validated) + #200's deferred follow-up (FATAL contract was documentation-only). Per Joshua's amended feedback-local-first-upstream-after: structure only, no PyPI publish, no upstream PR.

Deliverable A — Pip-packaging foundation

plugins/marvin/ restructured to relocatable src/ layout:

  plugins/marvin/
  ├── pyproject.toml          # NEW — setuptools build + hermes_agent.plugins entry point
  ├── README.md               # NEW — operator-facing install doc
  ├── plugin.yaml             # KEPT — Hermes bundled-plugin discovery
  ├── __init__.py             # REPLACED — sys.path-adjusting compat shim
  └── src/marvin/
      ├── __init__.py         # canonical module (relocated)
      └── data/
          ├── MARVIN.md       # relocated
          └── marvin_system_prompt.md

pyproject.toml declares "[project.entry-points.hermes_agent.plugins] marvin = marvin:register" — the exact entry-point group Hermes's _scan_entry_points already reads. Package_data includes the markdown files so the wheel ships relocatable identity assets.

The in-tree compat shim at plugins/marvin/__init__.py prepends plugins/marvin/src to sys.path + re-exports register/marvin_identity_provider from the canonical module. Existing 11 multi-tenant tests from #203 continue to pass with only one updated assertion (data files moved to src/marvin/data/).

Live dry-run install validated this session:
  $ uv build --wheel → marvin_runtime-0.1.0a1-py3-none-any.whl
  $ pip install <wheel> into fresh /tmp/marvin-dry-install venv
  $ importlib.metadata.entry_points() discovers ('marvin', 'marvin:register')
  $ marvin.__file__ = /tmp/.../site-packages/marvin/__init__.py (NOT in-tree)
  $ provider call → IdentitySpec(agent_name='Marvin', soul_chars=1306, system_chars=1486)
  $ register(stub_ctx) → wires identity provider via stub PluginContext ✓

CI-runnable regression guard: tests/plugins/test_marvin_pip_install_dry_run.py — 6 tests covering pyproject structure pins, wheel content pins, entry-point declaration pin, METADATA Requires-Python pin. ~0.5s per run via uv build (or python -m build if uv unavailable).

Companion kora-docs deliverables (separate PR):
  - kora_docs/14_research/kora_pip_packaging_2026-05-24/AUDIT.md — concrete shopping list for the future 4-package Kora restructure (kora-runtime + kora-cli + kora-cockpit + kora-promote-loops). 7-9 CC-days estimate across 4 sequential phases. 4 open questions for operator (Hermes pip-installability, IsoKron client packaging, schema migrations, versioning cadence).
  - kora_docs/14_research/plugin_identity_option_c_2026-05-24/MARVIN_DEMO_TRANSCRIPT.md §7 addendum — gap §6.4 closed; full wheel build + dry-run install transcript captured.

Deliverable B — Structural FATAL flag

Replaces #200's documentation-driven FATAL contract with structural enforcement:

agent/background_daemon_registry.py:
  - New field BackgroundDaemonEntry.fatal_on_startup_failure: bool = False
  - Field docstring documents the semantics + the looked-up-by-name path for the Path B thin-shim shape

kora_cli/plugins.py:
  - PluginContext.register_background_daemon accepts the new fatal_on_startup_failure kwarg + threads it into the BackgroundDaemonEntry construction

kora_cli/daemon.py:
  - New method DaemonCoordinator._is_startup_failure_fatal(listener_name):
    - Looks up the listener's BackgroundDaemonEntry by name
    - Returns entry.fatal_on_startup_failure if found
    - Returns True if not found (preserves pre-flag behavior for Kora-only HTTP service mounts: web/mcp/webhooks)
    - Returns True on lookup failure (defensive — never silently degrade on infrastructure error)
  - run() loop checks the flag at the listener-startup-raise site:
    - True → abort daemon boot (existing behavior)
    - False → log + continue starting subsequent listeners (NEW: lenient default for non-critical daemons whose own try/except didn't catch an unexpected exception)

kora_cli/listeners/reasoning_engine_listener.py:
  - _hermes_entry now passes fatal_on_startup_failure=True explicitly
  - Module docstring updated to reflect structural-not-documentation contract

Tests: 18 in tests/kora_cli/test_daemon_fatal_on_startup_failure.py covering:
  - Dataclass field defaults + frozen invariant
  - PluginContext kwarg passthrough
  - Lookup behavior (fatal entry / non-fatal entry / no entry / unknown name)
  - End-to-end coordinator behavior (fatal raises abort; non-fatal raises log + continue past)
  - Production pin: reasoning_engine has fatal=True; 7 phase-1/2/3 listeners (snapshot/heartbeat_probes/slack_client/purelymail_client/alert_notifier/cost_telemetry/mcp_consumption) default to fatal=False

Per the [[feedback-local-first-upstream-after]] amendment: this Hermes extension lives in the fork only this dispatch. When operator approves the next upstream-PR batch, this becomes upstream candidate #4 (joining the 3 already-deferred branches from #196).

Tests: 473/473 focused regression set green (Marvin tests + pip dry-run tests + FATAL flag tests + listener tests + identity tests + plugin tests).

Co-authored-by: CC#3 Kora Runtime <kora-pm@stormhavenenterprises.com>
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