複数クライアント間でのRunメソッドの同期漏れ

This commit is contained in:
Keisuke Hirata 2026-04-21 23:59:49 +09:00
parent 18533b3580
commit a39bce779c
3 changed files with 48 additions and 5 deletions

View File

@ -279,6 +279,13 @@ impl PodController {
}); });
continue; continue;
} }
// Broadcast the accepted user message so every
// subscriber (including the submitter) can
// render the turn header + user line from a
// single source of truth.
let _ = event_tx.send(Event::UserMessage {
text: input.clone(),
});
let was_paused = status_before == PodStatus::Paused; let was_paused = status_before == PodStatus::Paused;
shared_state.set_status(PodStatus::Running); shared_state.set_status(PodStatus::Running);
let _ = runtime_dir.write_status(&shared_state).await; let _ = runtime_dir.write_status(&shared_state).await;

View File

@ -83,6 +83,18 @@ pub enum PodEvent {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", content = "data", rename_all = "snake_case")] #[serde(tag = "event", content = "data", rename_all = "snake_case")]
pub enum Event { pub enum Event {
/// A user input message was accepted by the Pod and is about to
/// start a new turn. Broadcast to every subscribed client so
/// additional TUI / GUI instances show the same pending user line
/// that the submitter already sees — without this event, non-
/// submitting clients would see tool calls and assistant text
/// appear without any preceding user message.
///
/// Fires exactly once per accepted `Method::Run`, before
/// `TurnStart`. Rejected runs (e.g. `AlreadyRunning`) do not emit.
UserMessage {
text: String,
},
TurnStart { TurnStart {
turn: usize, turn: usize,
}, },
@ -596,4 +608,21 @@ mod tests {
assert_eq!(parsed["event"], "error"); assert_eq!(parsed["event"], "error");
assert_eq!(parsed["data"]["code"], "already_running"); assert_eq!(parsed["data"]["code"], "already_running");
} }
#[test]
fn event_user_message_roundtrip() {
let event = Event::UserMessage {
text: "hello 世界".into(),
};
let json = serde_json::to_string(&event).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["event"], "user_message");
assert_eq!(parsed["data"]["text"], "hello 世界");
let decoded: Event = serde_json::from_str(&json).unwrap();
match decoded {
Event::UserMessage { text } => assert_eq!(text, "hello 世界"),
other => panic!("expected UserMessage, got {other:?}"),
}
}
} }

View File

@ -72,11 +72,10 @@ impl App {
} }
return None; return None;
} }
self.turn_index += 1; // TurnHeader / UserMessage blocks are pushed in response to
self.blocks.push(Block::TurnHeader { // `Event::UserMessage` (single source of truth, shared by every
turn: self.turn_index, // client subscribed to the Pod). Locally we only clear the
}); // input buffer and forward the method.
self.blocks.push(Block::UserMessage { text: text.clone() });
self.input.clear(); self.input.clear();
Some(Method::Run { input: text }) Some(Method::Run { input: text })
} }
@ -91,6 +90,14 @@ impl App {
pub fn handle_pod_event(&mut self, event: Event) { pub fn handle_pod_event(&mut self, event: Event) {
match event { match event {
Event::UserMessage { text } => {
self.turn_index += 1;
self.blocks.push(Block::TurnHeader {
turn: self.turn_index,
});
self.blocks.push(Block::UserMessage { text });
self.assistant_streaming = false;
}
Event::TurnStart { .. } => { Event::TurnStart { .. } => {
self.running = true; self.running = true;
self.paused = false; self.paused = false;