Repository navigation
feat(http-transport): replace tiny_http with axum/SSE Streamable HTTP (#227) - #264
Conversation
…#227) Replace the single-threaded tiny_http POST-only HTTP transport with an axum-based server that supports SSE server-initiated push notifications, implementing MCP's Streamable HTTP pattern. Changes: - Remove `tiny_http` dep; add `axum = "0.8"` and `tokio-stream = { features = ["sync"] }` under the `http-transport` feature flag - Rewrite `serve_http()` using axum with a current-thread tokio runtime - Three routes: GET /health (liveness), POST / (JSON-RPC; runs handle_request via spawn_blocking), GET /sse (SSE stream for server-initiated notifications) - POST / detects `_list_changed` sentinel in responses, strips it, and pushes `notifications/tools/list_changed` to all SSE subscribers via broadcast channel - `agent_id_from_headers` updated to take `axum::http::HeaderMap` (std HeaderMap) instead of `&[tiny_http::Header]` — case-insensitive via lowercase key lookup - Default (stdio) transport: unchanged, 44/44 tests PASS - With `--features http-transport`: clean compile, no warnings in the binary Closes #227 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…project_hook MCP tool (#259/#227) - Add `event_bus.rs`: `ProjectEventBus` with `register/fire/fire_if`; `ProjectEvent` convenience constructors (`tool_called`, `task_created`, `task_done`, `lifecycle_transition`); 5 unit tests pass - Wire `Arc<ProjectEventBus>` onto `TurboLedgerService` (`project_event_bus` field, initialized in `from_manifest_str`, `register_project_hook` and `register_project_hook_tool` public methods) - Add `WorkflowArgs::RegisterProjectHook` variant to `contract.rs`; dispatch wired in `mcp_adapter.rs` (`handle_workflow_tool`) - Fire `tool_called` project event from HSM transition/resume dispatch via `fire_tool_event`; fire `lifecycle_transition` event on successful transition - Add axum/SSE `http_transport.rs` (`build_http_app`, `agent_id_from_headers`); 7 integration tests; `POST /` strips `_list_changed` sentinel and pushes SSE notification - Remove `tiny_http`; add `axum = "0.8"` + `tokio-stream` under `http-transport` feature - Server binary `serve_http()` delegates to `ledgerr_mcp::http_transport::build_http_app` Evidence: 56/56 ledgerr-mcp lib tests PASS (--features http-transport), 1/1 msft-agent-gov PASS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… general dispatch, real from-state in lifecycle_transition, task_created/task_done events
- Server binary: fire `tool_called` project event for every successful `tools/call`
dispatch (after the result match, before JSON-RPC envelope is built)
- `dispatch_hsm "transition"`: capture current state via `hsm_status_tool` before
the transition so `lifecycle_transition` event carries real `from` (not "?");
fire `task_done` event when new state is `summarize`
- `dispatch_hsm "resume"`: fire `task_created` on successful checkpoint resume
(agent picking up a workflow task); `resume_from` used as task_id
- Add 3 more `event_bus` unit tests: `task_created_event_has_correct_attrs`,
`task_done_event_has_correct_attrs`, `tool_called_event_has_correct_attrs`
- Verified `budget_reconcile_returns_well_formed_report_shape` passes (3/3) in
this worktree — prior timeout was compile-time, not test-logic
Evidence: 59/59 ledgerr-mcp lib tests PASS (--features http-transport);
3/3 budget_reconcile integration tests PASS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| &crate::event_bus::ProjectEvent::lifecycle_transition(&from_state, &to), | ||
| ); | ||
| // Reaching the Summarize state means the workflow task is complete. | ||
| if response.state.to_ascii_lowercase() == "summarize" { |
| &crate::event_bus::ProjectEvent::lifecycle_transition(&from_state, &to), | ||
| ); | ||
| // Reaching the Summarize state means the workflow task is complete. | ||
| if response.state.to_ascii_lowercase() == "summarize" { |
…echart unification plan WorkflowArgs gains four task-management actions, each firing a typed ProjectEvent: - task_create → task_created event - task_list → tool_called event - task_done → task_done event - task_update → task_updated event (new event kind) TurboLedgerService gains task_store: Mutex<BTreeMap<String, TaskEntry>> as the in-memory backing store. Every task lifecycle operation fires through ProjectEventBus so registered hooks (b00t, agents, CI gates) observe every event. Also adds: - ProjectEvent::task_updated constructor + 2 new unit tests in event_bus.rs - docs/statechart-unification-plan.md: full integration plan for lazily::StateChart ↔ b00t HsmMachine ↔ kr0ki ProcessChart ↔ ufo-types including b00t hook contract table, migration phases, and open gaps Regenerated: docs/agent-mcp-runbook.md, docs/mcp-capability-contract.md, scripts/mcp_cli_demo.sh, ui/docs/public/viz-manifest.json 61/61 ledgerr-mcp lib tests PASS (--features http-transport) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire `fire_tool_event` at the top of `tools/call` handler in `ledgerr-mcp-server.rs` so every incoming tool call fires a `ProjectEvent::tool_called` hook, not just task-management operations. Add `hook_fires_for_every_tool_family` unit test proving all 9 tool families trigger the hook — replaces the implicit assumption with an explicit contract. PM/task events (task_created, task_done, task_updated, tool_called) now form a complete hook surface: registered hooks observe 100% of activity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ooks design doc Phase 4 statechart integration: - `hsm_transition_tool` now fires `ProjectEvent::lifecycle_transition(from, to)` on `project_event_bus` after every successful HSM advance — every registered hook (statechart, cost-checkpoint, LFMF, etc.) observes 100% of lifecycle state changes, not just task-management operations. Test: `hsm_04_transition_fires_lifecycle_event_on_project_event_bus` — registers a hook, advances `ingest.pending → normalize.ready`, asserts one `lifecycle_transition` event fires with correct `from`/`to` attrs. 4/4 hsm_contract tests pass. Docs: `docs/pm-task-hooks-design.md` — complete reference for the PM/task hook architecture: event taxonomy, dispatch path diagram, hook registration (in-process and MCP), statechart integration points, and test coverage table. Closes the "no design doc" gap flagged by the stop hook. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…se 4c+) Add HsmChart to TurboLedgerService so PM lifecycle_transition events fire from lazily statechart entry actions rather than manual inline dispatch: - lifecycle_pm_chart_def(): flat chart with states matching HsmMachine token space; "lifecycle_transition" entry action on each non-initial state - HsmChart: wraps ThreadSafeStateChart + ThreadSafeContext, advance_count for fast-forward on restored sessions - TurboLedgerService.hsm_chart: Mutex<Option<HsmChart>>, initialised in new() - hsm_transition_tool: calls chart.try_advance() before firing ProjectEvent; chart authority audited via _chart_driven bool, best-effort fallback - dispatch_hsm: removes duplicate lifecycle_transition fire (was double-firing via both lib.rs and mcp_adapter.rs) - 4 HsmChart unit tests + hsm_05 integration test (5/5 PASS) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sk_status MCP tool) - pm_hooks.rs: PmHookEvent enum, TaskStatusStore (thread-safe BTreeMap projection), PmHookHandler trait, TaskStatusPmHookHandler, LoggingPmHookHandler, PmHookDispatcher (registers on ProjectEventBus, converts lifecycle_transition events to typed PmHookEvents) - TurboLedgerService::new(): builds TaskStatusStore, registers PmHookDispatcher (logging + task-status handlers) on project_event_bus at construction time - TaskStatus variant added to WorkflowArgs; task_status_tool() method on service - task_status MCP action routed in mcp_adapter.rs handle_workflow_tool - docs/mcp-capability-contract.md: regenerated with task_status in workflow row - 9 pm_hooks unit tests: from_transition mapping, TaskStatusStore apply/snapshot/reset, dispatcher fires on lifecycle_transition, dispatcher ignores non-lifecycle events - All 20 ledgerr-mcp test suites pass (0 failures) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| /// Returns `None` for unrecognised tokens (forward-compatible). | ||
| pub fn from_transition(from: &str, to: &str) -> Option<Self> { | ||
| let _ = from; // reserved for future guard conditions | ||
| let mut parts = to.splitn(2, '.'); |
| /// Returns `None` for unrecognised tokens (forward-compatible). | ||
| pub fn from_transition(from: &str, to: &str) -> Option<Self> { | ||
| let _ = from; // reserved for future guard conditions | ||
| let mut parts = to.splitn(2, '.'); |
| pub fn from_transition(from: &str, to: &str) -> Option<Self> { | ||
| let _ = from; // reserved for future guard conditions | ||
| let mut parts = to.splitn(2, '.'); | ||
| let stage = parts.next()?; |
| pub fn from_transition(from: &str, to: &str) -> Option<Self> { | ||
| let _ = from; // reserved for future guard conditions | ||
| let mut parts = to.splitn(2, '.'); | ||
| let stage = parts.next()?; |
| let _ = from; // reserved for future guard conditions | ||
| let mut parts = to.splitn(2, '.'); | ||
| let stage = parts.next()?; | ||
| let sub = parts.next()?; |
| let _ = from; // reserved for future guard conditions | ||
| let mut parts = to.splitn(2, '.'); | ||
| let stage = parts.next()?; | ||
| let sub = parts.next()?; |
…ly runtime PmHookEvent now covers all ProjectEvent variants: StageReady/Blocked, PipelineComplete/Reset, TaskCreated/Done/Updated, ToolCalled. PmHookDispatcher.register_on_bus routes every event through from_project_event — hooks fire on each event, not just lifecycle transitions. TaskStatusStore gains apply_task() for task events keyed as "task:<id>"; TaskStatusPmHookHandler dispatches correctly by variant. ooda_chart.rs: live lazily ThreadSafeStateChart executor for the OODA proposal lifecycle. States mirror ufo-types OodaPhase; transitions mirror OodaEvent::target. SCXML export (ufo-types) stays unchanged as interchange-only; this is the in-process runtime for PM hook dispatch and SSE events. 13 pm_hooks tests pass; 5 ooda_chart tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Extended in latest commit (5809d52):
|
Adversarial Review: feat(http-transport): replace tiny_http with axum/SSE Streamable HTTP (#227)Provider: sm3lly/neo-coder | Generated: 2026-10-13T13:27Z 🟢 AdvocateApproach AssessmentThe PR replaces a single-threaded
The separation of concerns is correct: the binary wires up the transport, the library exports Strong PointsClean transport abstraction:
Thoughtful design decisions:
Comprehensive test coverage:
Proper dependency management:
Evidence
Recommended to Merge If...
Verdict: This is a well-architected, well-tested upgrade that modernizes the HTTP transport while preserving backward compatibility. The PM hook infrastructure is a clean addition that will enable future observability features. Approve. 🔴 ChallengerConcerns
Two
Missing CoverageNo benchmark or performance comparison
No error handling for broadcast channel full
No test for concurrent SSE subscribers
No documentation update
No migration/rollback plan
No monitoring/metrics
Alternative ApproachesConsider
Use
Defer
Blocking IssuesThe
No test for the
|
Summary
Replaces the single-threaded
tiny_httpHTTP transport with an axum-based server supporting SSE server-initiated push notifications — implementing the Streamable HTTP pattern MCP specifies (issue #227).Also adds the
ProjectEventBusgeneral PM event hook system (issue #259 PM-hooks gap) and wires theregister_project_hookMCP tool.Routes
GET/healthPOST/application/jsonGET/ssenotifications/tools/list_changedeventsKey design decisions
handle_requeststays synchronous —POST /runs it viatokio::task::spawn_blockingtokio::sync::broadcastchannel (cap 64) distributes SSE events;GET /ssesubscribes viaBroadcastStreamPOST /detects_list_changedsentinel, strips it, pushesnotifications/tools/list_changedSSEbuild_http_appis library-exported so binary and integration tests share identical router codeProjectEventBus (PM hooks)
event_bus.rs:ProjectEventBus { register / fire / fire_if },ProjectEventwith constructors fortool_called,task_created,task_done,lifecycle_transitionArc<ProjectEventBus>onTurboLedgerService; initialized infrom_manifest_strWorkflowArgs::RegisterProjectHookvariant; dispatch inhandle_workflow_tooltool_called+lifecycle_transitionproject eventsTests
http_transportintegration tests (health, tools/list, 204 notification, SSE content-type, list_changed broadcast+strip, 400, 404)event_busunit tests (empty, fires all, fire_if filters, attr lookup, order)--features http-transport)🤖 Generated with Claude Code