feat: TUIに他Podからの通知を表示する
This commit is contained in:
parent
69a6f63023
commit
cae18a4339
|
|
@ -448,6 +448,9 @@ impl PodController {
|
||||||
}
|
}
|
||||||
|
|
||||||
Method::Notify { message } => {
|
Method::Notify { message } => {
|
||||||
|
let _ = event_tx.send(Event::Notify {
|
||||||
|
message: message.clone(),
|
||||||
|
});
|
||||||
pod.push_notify(message);
|
pod.push_notify(message);
|
||||||
if shared_state.get_status() != PodStatus::Idle {
|
if shared_state.get_status() != PodStatus::Idle {
|
||||||
// RUNNING / Paused: the buffer push is the
|
// RUNNING / Paused: the buffer push is the
|
||||||
|
|
@ -609,6 +612,10 @@ impl PodController {
|
||||||
Method::GetHistory | Method::ListCompletions { .. } => {}
|
Method::GetHistory | Method::ListCompletions { .. } => {}
|
||||||
|
|
||||||
Method::PodEvent(event) => {
|
Method::PodEvent(event) => {
|
||||||
|
// Echo the received event to all subscribers so
|
||||||
|
// every client sees the input that drove any
|
||||||
|
// following auto-kicked turn.
|
||||||
|
let _ = event_tx.send(Event::PodEvent(event.clone()));
|
||||||
// (1) system side effects — idempotent and
|
// (1) system side effects — idempotent and
|
||||||
// tolerant of out-of-order delivery (e.g.
|
// tolerant of out-of-order delivery (e.g.
|
||||||
// `TurnEnded` arriving after `ShutDown`).
|
// `TurnEnded` arriving after `ShutDown`).
|
||||||
|
|
@ -809,12 +816,16 @@ where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(Method::Notify { message }) => {
|
Some(Method::Notify { message }) => {
|
||||||
|
let _ = event_tx.send(Event::Notify {
|
||||||
|
message: message.clone(),
|
||||||
|
});
|
||||||
// Route into the buffer; the in-flight turn will
|
// Route into the buffer; the in-flight turn will
|
||||||
// drain it at its next pre_llm_request.
|
// drain it at its next pre_llm_request.
|
||||||
notify_buffer.push(message);
|
notify_buffer.push(message);
|
||||||
}
|
}
|
||||||
Some(Method::GetHistory | Method::ListCompletions { .. }) => {}
|
Some(Method::GetHistory | Method::ListCompletions { .. }) => {}
|
||||||
Some(Method::PodEvent(event)) => {
|
Some(Method::PodEvent(event)) => {
|
||||||
|
let _ = event_tx.send(Event::PodEvent(event.clone()));
|
||||||
// mpsc is consume-once, so we cannot defer this
|
// mpsc is consume-once, so we cannot defer this
|
||||||
// to the next main-loop iteration — drop here
|
// to the next main-loop iteration — drop here
|
||||||
// would lose the event entirely (children fire
|
// would lose the event entirely (children fire
|
||||||
|
|
|
||||||
|
|
@ -532,12 +532,16 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Wait for the auto-started turn to complete.
|
// Wait for the auto-started turn to complete.
|
||||||
|
let mut saw_notify_echo = false;
|
||||||
let mut saw_turn_end = false;
|
let mut saw_turn_end = false;
|
||||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
event = rx.recv() => {
|
event = rx.recv() => {
|
||||||
match event {
|
match event {
|
||||||
|
Ok(Event::Notify { ref message }) if message == "turn finished" => {
|
||||||
|
saw_notify_echo = true;
|
||||||
|
}
|
||||||
Ok(Event::TurnEnd { .. }) => { saw_turn_end = true; break; }
|
Ok(Event::TurnEnd { .. }) => { saw_turn_end = true; break; }
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -546,6 +550,10 @@ async fn notify_while_idle_auto_starts_turn_and_injects_system_message() {
|
||||||
_ = tokio::time::sleep_until(deadline) => break,
|
_ = tokio::time::sleep_until(deadline) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
saw_notify_echo,
|
||||||
|
"Method::Notify on idle Pod should be echoed as Event::Notify"
|
||||||
|
);
|
||||||
assert!(saw_turn_end, "auto-triggered turn should complete");
|
assert!(saw_turn_end, "auto-triggered turn should complete");
|
||||||
// Status flips back to Idle on the controller thread after RunEnd.
|
// Status flips back to Idle on the controller thread after RunEnd.
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
|
@ -585,12 +593,18 @@ async fn pod_event_turn_ended_while_idle_auto_starts_turn_and_injects_system_mes
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mut saw_pod_event_echo = false;
|
||||||
let mut saw_turn_end = false;
|
let mut saw_turn_end = false;
|
||||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
event = rx.recv() => {
|
event = rx.recv() => {
|
||||||
match event {
|
match event {
|
||||||
|
Ok(Event::PodEvent(protocol::PodEvent::TurnEnded { ref pod_name }))
|
||||||
|
if pod_name == "child" =>
|
||||||
|
{
|
||||||
|
saw_pod_event_echo = true;
|
||||||
|
}
|
||||||
Ok(Event::TurnEnd { .. }) => { saw_turn_end = true; break; }
|
Ok(Event::TurnEnd { .. }) => { saw_turn_end = true; break; }
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -599,6 +613,10 @@ async fn pod_event_turn_ended_while_idle_auto_starts_turn_and_injects_system_mes
|
||||||
_ = tokio::time::sleep_until(deadline) => break,
|
_ = tokio::time::sleep_until(deadline) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
saw_pod_event_echo,
|
||||||
|
"Method::PodEvent on idle Pod should be echoed as Event::PodEvent"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
saw_turn_end,
|
saw_turn_end,
|
||||||
"PodEvent::TurnEnded on idle Pod should auto-start a turn"
|
"PodEvent::TurnEnded on idle Pod should auto-start a turn"
|
||||||
|
|
@ -644,6 +662,8 @@ async fn notify_while_running_does_not_emit_already_running_error() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Drain events until the run ends; AlreadyRunning must never appear.
|
// Drain events until the run ends; AlreadyRunning must never appear.
|
||||||
|
// The in-flight branch must still echo the Notify as a log element.
|
||||||
|
let mut saw_notify_echo = false;
|
||||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
@ -652,6 +672,9 @@ async fn notify_while_running_does_not_emit_already_running_error() {
|
||||||
Ok(Event::Error { code, .. }) if code == pod::ErrorCode::AlreadyRunning => {
|
Ok(Event::Error { code, .. }) if code == pod::ErrorCode::AlreadyRunning => {
|
||||||
panic!("Notify while running must not produce AlreadyRunning");
|
panic!("Notify while running must not produce AlreadyRunning");
|
||||||
}
|
}
|
||||||
|
Ok(Event::Notify { ref message }) if message == "ping" => {
|
||||||
|
saw_notify_echo = true;
|
||||||
|
}
|
||||||
Ok(Event::TurnEnd { .. }) => break,
|
Ok(Event::TurnEnd { .. }) => break,
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -660,6 +683,10 @@ async fn notify_while_running_does_not_emit_already_running_error() {
|
||||||
_ = tokio::time::sleep_until(deadline) => break,
|
_ = tokio::time::sleep_until(deadline) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
saw_notify_echo,
|
||||||
|
"in-flight Notify must still be echoed as Event::Notify"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -751,6 +778,7 @@ async fn socket_pod_event_turn_ended_while_idle_auto_starts_turn() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mut saw_pod_event_echo = false;
|
||||||
let mut saw_turn_start = false;
|
let mut saw_turn_start = false;
|
||||||
let mut saw_turn_end = false;
|
let mut saw_turn_end = false;
|
||||||
|
|
||||||
|
|
@ -759,6 +787,11 @@ async fn socket_pod_event_turn_ended_while_idle_auto_starts_turn() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
event = reader.next::<Event>() => {
|
event = reader.next::<Event>() => {
|
||||||
match event {
|
match event {
|
||||||
|
Ok(Some(Event::PodEvent(protocol::PodEvent::TurnEnded { pod_name })))
|
||||||
|
if pod_name == "child" =>
|
||||||
|
{
|
||||||
|
saw_pod_event_echo = true;
|
||||||
|
}
|
||||||
Ok(Some(Event::TurnStart { .. })) => saw_turn_start = true,
|
Ok(Some(Event::TurnStart { .. })) => saw_turn_start = true,
|
||||||
Ok(Some(Event::TurnEnd { .. })) => {
|
Ok(Some(Event::TurnEnd { .. })) => {
|
||||||
saw_turn_end = true;
|
saw_turn_end = true;
|
||||||
|
|
@ -772,6 +805,10 @@ async fn socket_pod_event_turn_ended_while_idle_auto_starts_turn() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
saw_pod_event_echo,
|
||||||
|
"PodEvent::TurnEnded via socket should be echoed as Event::PodEvent"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
saw_turn_start,
|
saw_turn_start,
|
||||||
"PodEvent::TurnEnded via socket should auto-start a turn"
|
"PodEvent::TurnEnded via socket should auto-start a turn"
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,20 @@ pub enum Event {
|
||||||
UserMessage {
|
UserMessage {
|
||||||
segments: Vec<Segment>,
|
segments: Vec<Segment>,
|
||||||
},
|
},
|
||||||
|
/// Echo of `Method::Notify` received by this Pod. Broadcast on
|
||||||
|
/// receipt so subscribers can render the external input as a log
|
||||||
|
/// element. The same `message` is independently pushed into the
|
||||||
|
/// notification buffer for LLM injection (with prompt-pack
|
||||||
|
/// wrapping); this echo carries the raw payload and does not
|
||||||
|
/// imply any turn-boundary semantics.
|
||||||
|
Notify {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
/// Echo of `Method::PodEvent` received by this Pod. Same rationale
|
||||||
|
/// as `Notify`: subscribers render the event as a log element,
|
||||||
|
/// while a rendered summary is independently injected into the LLM
|
||||||
|
/// context via the notification buffer.
|
||||||
|
PodEvent(PodEvent),
|
||||||
TurnStart {
|
TurnStart {
|
||||||
turn: usize,
|
turn: usize,
|
||||||
},
|
},
|
||||||
|
|
@ -930,6 +944,43 @@ mod tests {
|
||||||
assert_eq!(parsed["data"]["code"], "already_running");
|
assert_eq!(parsed["data"]["code"], "already_running");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_notify_roundtrip() {
|
||||||
|
let event = Event::Notify {
|
||||||
|
message: "child-pod finished".into(),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
|
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(parsed["event"], "notify");
|
||||||
|
assert_eq!(parsed["data"]["message"], "child-pod finished");
|
||||||
|
|
||||||
|
let decoded: Event = serde_json::from_str(&json).unwrap();
|
||||||
|
match decoded {
|
||||||
|
Event::Notify { message } => assert_eq!(message, "child-pod finished"),
|
||||||
|
other => panic!("expected Notify, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_pod_event_roundtrip() {
|
||||||
|
let event = Event::PodEvent(PodEvent::TurnEnded {
|
||||||
|
pod_name: "child".into(),
|
||||||
|
});
|
||||||
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
|
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(parsed["event"], "pod_event");
|
||||||
|
assert_eq!(parsed["data"]["kind"], "turn_ended");
|
||||||
|
assert_eq!(parsed["data"]["pod_name"], "child");
|
||||||
|
|
||||||
|
let decoded: Event = serde_json::from_str(&json).unwrap();
|
||||||
|
match decoded {
|
||||||
|
Event::PodEvent(PodEvent::TurnEnded { pod_name }) => {
|
||||||
|
assert_eq!(pod_name, "child");
|
||||||
|
}
|
||||||
|
other => panic!("expected PodEvent::TurnEnded, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_user_message_roundtrip() {
|
fn event_user_message_roundtrip() {
|
||||||
let event = Event::UserMessage {
|
let event = Event::UserMessage {
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,14 @@ impl App {
|
||||||
self.blocks.push(Block::UserMessage { segments });
|
self.blocks.push(Block::UserMessage { segments });
|
||||||
self.assistant_streaming = false;
|
self.assistant_streaming = false;
|
||||||
}
|
}
|
||||||
|
Event::Notify { message } => {
|
||||||
|
self.blocks.push(Block::Notify { message });
|
||||||
|
self.assistant_streaming = false;
|
||||||
|
}
|
||||||
|
Event::PodEvent(event) => {
|
||||||
|
self.blocks.push(Block::PodEvent { event });
|
||||||
|
self.assistant_streaming = false;
|
||||||
|
}
|
||||||
Event::TurnStart { .. } => {
|
Event::TurnStart { .. } => {
|
||||||
self.running = true;
|
self.running = true;
|
||||||
self.paused = false;
|
self.paused = false;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use protocol::{AlertLevel, AlertSource, Greeting, Segment};
|
use protocol::{AlertLevel, AlertSource, Greeting, PodEvent, Segment};
|
||||||
|
|
||||||
pub enum Block {
|
pub enum Block {
|
||||||
Greeting(Greeting),
|
Greeting(Greeting),
|
||||||
|
|
@ -19,6 +19,17 @@ pub enum Block {
|
||||||
UserMessage {
|
UserMessage {
|
||||||
segments: Vec<Segment>,
|
segments: Vec<Segment>,
|
||||||
},
|
},
|
||||||
|
/// Echo of `Method::Notify` received by this Pod, surfaced as a log
|
||||||
|
/// element so subscribers see the external input that drove any
|
||||||
|
/// following auto-kicked turn.
|
||||||
|
Notify {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
/// Echo of `Method::PodEvent` received by this Pod. Same role as
|
||||||
|
/// `Notify` — an input log element, not a turn-control signal.
|
||||||
|
PodEvent {
|
||||||
|
event: PodEvent,
|
||||||
|
},
|
||||||
AssistantText {
|
AssistantText {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use ratatui::widgets::{
|
||||||
};
|
};
|
||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
use protocol::{AlertLevel, CompletionEntry, Greeting, Segment};
|
use protocol::{AlertLevel, CompletionEntry, Greeting, PodEvent, Segment};
|
||||||
|
|
||||||
use crate::app::{App, CompletionState, alert_source_label, fmt_tokens};
|
use crate::app::{App, CompletionState, alert_source_label, fmt_tokens};
|
||||||
use crate::block::{Block, CompactEvent, ThinkingBlock, ThinkingState};
|
use crate::block::{Block, CompactEvent, ThinkingBlock, ThinkingState};
|
||||||
|
|
@ -361,6 +361,20 @@ fn render_block_into(lines: &mut Vec<Line<'static>>, block: &Block, width: u16,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Block::UserMessage { segments } => render_user_message(lines, segments, width, mode),
|
Block::UserMessage { segments } => render_user_message(lines, segments, width, mode),
|
||||||
|
Block::Notify { message } => {
|
||||||
|
let text = format!("[notify] {message}");
|
||||||
|
match mode {
|
||||||
|
Mode::Overview => push_overview_line(lines, &text, width, MessageKind::Notify, ""),
|
||||||
|
_ => push_padded_lines(lines, &text, MessageKind::Notify),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Block::PodEvent { event } => {
|
||||||
|
let text = format_pod_event(event);
|
||||||
|
match mode {
|
||||||
|
Mode::Overview => push_overview_line(lines, &text, width, MessageKind::Notify, ""),
|
||||||
|
_ => push_padded_lines(lines, &text, MessageKind::Notify),
|
||||||
|
}
|
||||||
|
}
|
||||||
Block::AssistantText { text } => match mode {
|
Block::AssistantText { text } => match mode {
|
||||||
Mode::Overview => push_overview_line(lines, text, width, MessageKind::Assistant, ""),
|
Mode::Overview => push_overview_line(lines, text, width, MessageKind::Assistant, ""),
|
||||||
_ => push_padded_lines(lines, text, MessageKind::Assistant),
|
_ => push_padded_lines(lines, text, MessageKind::Assistant),
|
||||||
|
|
@ -913,6 +927,10 @@ fn greeting_lines(g: &Greeting) -> Vec<Line<'static>> {
|
||||||
pub enum MessageKind {
|
pub enum MessageKind {
|
||||||
TurnHeader,
|
TurnHeader,
|
||||||
User,
|
User,
|
||||||
|
/// External-input echoes (`Method::Notify` / `Method::PodEvent`).
|
||||||
|
/// Visually distinct from User / Assistant / Notice so it's clear
|
||||||
|
/// the line came from another Pod or operator, not the local user.
|
||||||
|
Notify,
|
||||||
Assistant,
|
Assistant,
|
||||||
Thinking,
|
Thinking,
|
||||||
TurnStats,
|
TurnStats,
|
||||||
|
|
@ -924,6 +942,7 @@ pub fn kind_style(kind: MessageKind) -> Style {
|
||||||
match kind {
|
match kind {
|
||||||
MessageKind::TurnHeader => Style::default().fg(Color::DarkGray),
|
MessageKind::TurnHeader => Style::default().fg(Color::DarkGray),
|
||||||
MessageKind::User => Style::default().fg(Color::Green),
|
MessageKind::User => Style::default().fg(Color::Green),
|
||||||
|
MessageKind::Notify => Style::default().fg(Color::Yellow),
|
||||||
MessageKind::Assistant => Style::default().fg(Color::White),
|
MessageKind::Assistant => Style::default().fg(Color::White),
|
||||||
MessageKind::Thinking => Style::default()
|
MessageKind::Thinking => Style::default()
|
||||||
.fg(Color::Magenta)
|
.fg(Color::Magenta)
|
||||||
|
|
@ -939,3 +958,26 @@ pub fn kind_style(kind: MessageKind) -> Style {
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One-line summary of a `PodEvent` for display in the activity log.
|
||||||
|
/// Independent from the LLM-injection wrapper (`crate::ipc::event::render_event`
|
||||||
|
/// in the pod crate) — that path applies prompt-pack wrapping, while
|
||||||
|
/// this is the human-facing rendering of the raw structured event.
|
||||||
|
fn format_pod_event(event: &PodEvent) -> String {
|
||||||
|
match event {
|
||||||
|
PodEvent::TurnEnded { pod_name } => {
|
||||||
|
format!("[pod_event] {pod_name} → turn_ended")
|
||||||
|
}
|
||||||
|
PodEvent::Errored { pod_name, message } => {
|
||||||
|
format!("[pod_event] {pod_name} → errored: {message}")
|
||||||
|
}
|
||||||
|
PodEvent::ShutDown { pod_name } => {
|
||||||
|
format!("[pod_event] {pod_name} → shut_down")
|
||||||
|
}
|
||||||
|
PodEvent::ScopeSubDelegated {
|
||||||
|
parent_pod, sub_pod, ..
|
||||||
|
} => {
|
||||||
|
format!("[pod_event] {parent_pod} → scope_sub_delegated: {sub_pod}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,29 @@
|
||||||
# TUI で auto-kick 由来のターンが表示されない
|
# TUI で Pod への外部入力 (Notify / PodEvent) が描画されない
|
||||||
|
|
||||||
## 背景
|
## 背景
|
||||||
|
|
||||||
Pod が `Method::PodEvent::TurnEnded` などを socket 経由で受信すると、controller は notification を notify buffer に積み、Idle なら `pod.run_for_notification()` で新しいターンを起動する(`crates/pod/src/controller.rs:611-687`)。このターンの assistant 出力 (`Event::TurnStart` / `TextDelta` / `TurnEnd` 等) は通常通り broadcast Event として全クライアント(TUI 含む)に配信されるはず。
|
Pod が `Method::Notify` / `Method::PodEvent` を socket 経由で受信すると、controller は内容を notify buffer に積み、Idle なら `pod.run_for_notification()` で新しいターンを起動する(`crates/pod/src/controller.rs`)。auto-kick されたターンの assistant 出力 (`Event::TurnStart` / `TextDelta` / `TurnEnd` 等) は通常通り broadcast Event として全クライアント(TUI 含む)に配信される。
|
||||||
|
|
||||||
## 問題
|
## 問題
|
||||||
|
|
||||||
socat で稼働中の codex-oauth pod の socket に `Method::PodEvent::TurnEnded` を 1 行流したところ、socat 側の subscribe には turn が完全に流れてきた(thinking_delta / text_done / turn_end 取得済み)が、同じ pod を起動している TUI 画面には新ターンが描画されなかった。
|
socat で稼働中の codex-oauth pod の socket に `Method::PodEvent::TurnEnded` を 1 行流したところ、socat 側の subscribe には turn が完全に流れてきた(thinking_delta / text_done / turn_end 取得済み)が、同じ pod を起動している TUI 画面には新ターンが描画されなかった。
|
||||||
|
|
||||||
`Method::Run` 経由の通常ターンは TUI に表示されるので、broadcast 配信そのものは生きている。auto-kick 由来のターン(user_message を伴わない turn)に固有の表示パスで落ちている可能性が高い。
|
`Method::Run` 経由の通常ターンは TUI に表示されるので、broadcast 配信そのものは生きている。原因は **Pod が受信した外部入力 (`Method::Notify` / `Method::PodEvent`) が broadcast event として echo されておらず**、TUI からは「何も入力がない状態で突然ターンが始まる」ように見えていることにある。auto-kick ターンが描画されない件はこの下流症状の一つ。
|
||||||
|
|
||||||
## 要件
|
## 要件
|
||||||
|
|
||||||
- auto-kick で起動したターン(user 入力を伴わないターン)も、user 由来ターンと同様に TUI 履歴に表示される。
|
- Pod が socket で受信した外部入力のうち、活動ログとして残すべきもの (`Method::Notify` / `Method::PodEvent`) を broadcast event として全 subscriber に echo する。
|
||||||
- turn header 等の見た目で「通知由来である」ことを示す表記を入れるかは別議論。
|
- TUI はその event を user message / assistant text と並列のログ要素として描画する。
|
||||||
|
- auto-kick 由来ターン (`TurnStart` 以降) は既存経路で従来通り表示される。Notify / PodEvent 受信が表示されるようになれば、ターン境界の出所はログ上で自然に区別できる。
|
||||||
|
|
||||||
|
## 範囲外 / 非目標
|
||||||
|
|
||||||
|
- LLM 注入テキスト (`notify_wrapper` 適用後の wrapped string) を UI に見せるかは別判断。本チケットでは **raw メッセージをそのまま echo** する形で着地する。UI 側で wrapper を適用したくなったら、別途 catalog を引く形で対応する。
|
||||||
|
- `starts_turn` 等の「auto-kick フラグ」を新 event に持たせない。ターン境界制御は `TurnStart` の責務であり、入力 echo event はあくまで入力ログ要素のみを表す。
|
||||||
|
- protocol に追加する Event variant は **入力 echo の責務だけ**を持ち、UI 通知 (toast / OS 通知) を兼ねない。
|
||||||
|
|
||||||
## 完了条件
|
## 完了条件
|
||||||
|
|
||||||
- 親 pod が PodEvent を受信して auto-kick した際、TUI 上で thinking / assistant text / turn_end が user 由来ターンと同様に表示される。
|
- socket に `Method::Notify { message }` を流すと、全 subscriber(TUI 含む)のログにその通知本文が user / assistant と並列の要素として表示される。
|
||||||
|
- socket に `Method::PodEvent::TurnEnded` 等を流すと、その受信を示すログ要素 + 後続 auto-kick ターンの thinking / assistant text / turn_end が、user 由来ターンと同様に TUI に表示される。
|
||||||
|
- 追加した broadcast event は `Method::Notify` / `Method::PodEvent` の payload と一対一対応し、`starts_turn` のような派生フラグを持たない。
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user