diff --git a/.yoi/tickets/00001KY1ZKCPC/item.md b/.yoi/tickets/00001KY1ZKCPC/item.md index b42bf5ab..5f92f1cb 100644 --- a/.yoi/tickets/00001KY1ZKCPC/item.md +++ b/.yoi/tickets/00001KY1ZKCPC/item.md @@ -2,7 +2,7 @@ title: 'Backend runtime経由の操作をprotocol transportへ統一しTUI同等にする' state: 'planning' created_at: '2026-07-21T09:20:07Z' -updated_at: '2026-07-21T11:42:36Z' +updated_at: '2026-07-22T02:11:40Z' assignee: null readiness: 'draft' --- diff --git a/.yoi/tickets/00001KY1ZKCPC/thread.md b/.yoi/tickets/00001KY1ZKCPC/thread.md index fdae1157..8193fa08 100644 --- a/.yoi/tickets/00001KY1ZKCPC/thread.md +++ b/.yoi/tickets/00001KY1ZKCPC/thread.md @@ -166,3 +166,35 @@ Verification: - `git diff --check` --- + + + +## Implementation report + +Bug fix for missing local echo in TUI/Web: + +Observed symptom: +- Sending from TUI/Web over unified `/protocol/ws` reached the runtime and was accepted as `protocol_method`, but the user's sent content did not appear in the Console. + +Root cause: +- The old HTTP input path emitted observation events for accepted input via `input_protocol_event(...)` (`Event::UserMessage` for user input / `Event::SystemItem` for system input). +- The new raw `Method::Run` / `Method::Notify` protocol path dispatched the method to the execution backend but did not record the corresponding observation echo event. +- Therefore the unified protocol stream had assistant/runtime events, but no echo event representing the user-sent content. + +Fix: +- Added protocol-method observation echo in `worker-runtime` after accepted `Runtime::send_protocol_method(...)` dispatch. +- `Method::Run` now records `Event::UserMessage { segments }`. +- `Method::Notify` now records a system input `Event::SystemItem`. +- Kept compact/list_rewind_targets/register_peer command echoes aligned with the old command-input observation shape. +- Added a runtime protocol WS regression test: send `Method::Run` over `/protocol/ws` and assert the same socket receives `Event::UserMessage` with the sent text. + +Verification: +- `nix develop -c cargo fmt -- --check` +- `nix develop -c cargo check -p worker-runtime -p yoi-workspace-server -p client -p tui` +- `nix develop -c cargo test -p worker-runtime --features ws-server --lib protocol_ws_echoes_accepted_run_as_user_message` +- `nix develop -c cargo test -p worker-runtime --features ws-server --lib protocol_ws` +- `nix develop -c cargo test -p yoi-workspace-server protocol_ws --lib` +- `nix develop -c cargo test -p client backend_runtime --lib` +- `git diff --check` + +--- diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index 3ea1f1b0..6bbb72d3 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -1167,6 +1167,17 @@ mod ws_tests { WorkerExecutionRunState::Idle, ) } + + fn dispatch_method( + &self, + _handle: &WorkerExecutionHandle, + _method: protocol::Method, + ) -> WorkerExecutionResult { + WorkerExecutionResult::accepted( + WorkerExecutionOperation::ProtocolMethod, + WorkerExecutionRunState::Idle, + ) + } } fn ws_test_bundle(profile: ProfileSelector) -> ConfigBundle { @@ -1328,6 +1339,34 @@ mod ws_tests { )); } + #[tokio::test] + async fn protocol_ws_echoes_accepted_run_as_user_message() { + let (_runtime, _worker_ref, url) = spawn_runtime_server().await; + let (mut stream, _) = connect_async(&url).await.unwrap(); + let _ = next_frame(&mut stream).await; + + stream + .send(Message::Text( + serde_json::to_string(&protocol::Method::Run { + input: vec![protocol::Segment::text("hello from protocol")], + }) + .unwrap() + .into(), + )) + .await + .unwrap(); + + match next_frame(&mut stream).await { + protocol::Event::UserMessage { segments } => { + assert_eq!( + protocol::Segment::flatten_to_text(&segments), + "hello from protocol" + ); + } + event => panic!("expected user message echo, got {event:?}"), + } + } + #[tokio::test] async fn protocol_ws_reports_malformed_cursor_and_method_frame() { let (_runtime, _worker_ref, url) = spawn_runtime_server().await; diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 5c9bc38b..cf813f2d 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -576,6 +576,8 @@ impl Runtime { return Ok(vec![Event::Completions { kind, entries }]); } + let observation_payload = protocol_method_observation_event(&method); + let (backend, handle) = { let mut state = self.lock()?; state.ensure_running()?; @@ -622,6 +624,9 @@ impl Runtime { } self.record_execution_result(worker_ref, dispatch_result)?; + if let Some(payload) = observation_payload { + self.record_protocol_method_observation(worker_ref, payload)?; + } Ok(Vec::new()) } @@ -958,12 +963,7 @@ impl Runtime { worker_ref: &WorkerRef, input: WorkerInput, ) -> Result<(), RuntimeError> { - let mut state = self.lock()?; - state.ensure_worker_ref(worker_ref)?; - let event = - state.push_worker_observation_event(worker_ref.clone(), input_protocol_event(&input)); - state.persist_worker_observation_event(&event)?; - Ok(()) + self.record_protocol_method_observation(worker_ref, input_protocol_event(&input)) } #[cfg(not(feature = "ws-server"))] @@ -975,6 +975,28 @@ impl Runtime { Ok(()) } + #[cfg(feature = "ws-server")] + fn record_protocol_method_observation( + &self, + worker_ref: &WorkerRef, + payload: Event, + ) -> Result<(), RuntimeError> { + let mut state = self.lock()?; + state.ensure_worker_ref(worker_ref)?; + let event = state.push_worker_observation_event(worker_ref.clone(), payload); + state.persist_worker_observation_event(&event)?; + Ok(()) + } + + #[cfg(not(feature = "ws-server"))] + fn record_protocol_method_observation( + &self, + _worker_ref: &WorkerRef, + _payload: Event, + ) -> Result<(), RuntimeError> { + Ok(()) + } + fn transition_worker( &self, worker_ref: &WorkerRef, @@ -1836,6 +1858,48 @@ fn validate_worker_input(input: &WorkerInput) -> Result<(), RuntimeError> { Ok(()) } +#[cfg(feature = "ws-server")] +fn protocol_method_observation_event(method: &Method) -> Option { + match method { + Method::Run { input } => Some(Event::UserMessage { + segments: input.clone(), + }), + Method::Notify { message, .. } => Some(Event::SystemItem { + item: serde_json::json!({ + "kind": "embedded_worker_system_input", + "content": message, + }), + }), + Method::RegisterPeer { name } => Some(Event::SystemItem { + item: serde_json::json!({ + "kind": "embedded_worker_command_input", + "command": "register_peer", + "content": name, + }), + }), + Method::Compact => Some(Event::SystemItem { + item: serde_json::json!({ + "kind": "embedded_worker_command_input", + "command": "compact", + "content": "", + }), + }), + Method::ListRewindTargets => Some(Event::SystemItem { + item: serde_json::json!({ + "kind": "embedded_worker_command_input", + "command": "list_rewind_targets", + "content": "", + }), + }), + _ => None, + } +} + +#[cfg(not(feature = "ws-server"))] +fn protocol_method_observation_event(_method: &Method) -> Option { + None +} + #[cfg(feature = "ws-server")] fn input_protocol_event(input: &WorkerInput) -> protocol::Event { match input.kind {