From cd86cc533c3a6927bb7149bc5ba90d80e07d862b Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 18 Jun 2026 23:42:07 +0900 Subject: [PATCH] fix: restore orchestrator companion notifications --- .yoi/tickets/00001KV5W3PHW/item.md | 4 +- .yoi/tickets/00001KV5W3PHW/resolution.md | 3 + .yoi/tickets/00001KV5W3PHW/thread.md | 20 +++ .yoi/tickets/00001KVDH2E06/artifacts/.gitkeep | 0 .yoi/tickets/00001KVDH2E06/item.md | 94 +++++++++++++ .yoi/tickets/00001KVDH2E06/thread.md | 23 ++++ .yoi/tickets/00001KVDJCVWZ/artifacts/.gitkeep | 0 .../00001KVDJCVWZ/artifacts/relations.json | 13 ++ .yoi/tickets/00001KVDJCVWZ/item.md | 70 ++++++++++ .yoi/tickets/00001KVDJCVWZ/thread.md | 7 + crates/pod/src/controller.rs | 25 ++++ crates/pod/src/discovery.rs | 125 +++++++++++++++--- crates/pod/src/ticket_event_notify.rs | 81 +++++++++--- devshell.nix | 6 + 14 files changed, 431 insertions(+), 40 deletions(-) create mode 100644 .yoi/tickets/00001KV5W3PHW/resolution.md create mode 100644 .yoi/tickets/00001KVDH2E06/artifacts/.gitkeep create mode 100644 .yoi/tickets/00001KVDH2E06/item.md create mode 100644 .yoi/tickets/00001KVDH2E06/thread.md create mode 100644 .yoi/tickets/00001KVDJCVWZ/artifacts/.gitkeep create mode 100644 .yoi/tickets/00001KVDJCVWZ/artifacts/relations.json create mode 100644 .yoi/tickets/00001KVDJCVWZ/item.md create mode 100644 .yoi/tickets/00001KVDJCVWZ/thread.md diff --git a/.yoi/tickets/00001KV5W3PHW/item.md b/.yoi/tickets/00001KV5W3PHW/item.md index 5a9ef879..8251c301 100644 --- a/.yoi/tickets/00001KV5W3PHW/item.md +++ b/.yoi/tickets/00001KV5W3PHW/item.md @@ -1,8 +1,8 @@ --- title: 'Plugin: execute Plugin Tool with minimal WASM runtime' -state: 'done' +state: 'closed' created_at: '2026-06-15T14:48:59Z' -updated_at: '2026-06-18T12:39:30Z' +updated_at: '2026-06-18T13:55:12Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'wasm', 'tool-runtime', 'sandbox', 'capability-boundary', 'cancellation'] diff --git a/.yoi/tickets/00001KV5W3PHW/resolution.md b/.yoi/tickets/00001KV5W3PHW/resolution.md new file mode 100644 index 00000000..f791a74d --- /dev/null +++ b/.yoi/tickets/00001KV5W3PHW/resolution.md @@ -0,0 +1,3 @@ +Ticket `00001KV5W3PHW` (`Plugin: execute Plugin Tool with minimal WASM runtime`) はすでに `state: done` に到達していたため、workspace Panel から close しました。 + +この Close action によって、実装作業、state 変更、Orchestrator/Companion launch、worker invocation は開始されていません。 diff --git a/.yoi/tickets/00001KV5W3PHW/thread.md b/.yoi/tickets/00001KV5W3PHW/thread.md index e2dedec2..00d92d26 100644 --- a/.yoi/tickets/00001KV5W3PHW/thread.md +++ b/.yoi/tickets/00001KV5W3PHW/thread.md @@ -309,4 +309,24 @@ Cleanup planned: Reviewer approved, implementation branch merged into the orchestration branch, and focused plus packaging validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch. +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +Ticket `00001KV5W3PHW` (`Plugin: execute Plugin Tool with minimal WASM runtime`) はすでに `state: done` に到達していたため、workspace Panel から close しました。 + +この Close action によって、実装作業、state 変更、Orchestrator/Companion launch、worker invocation は開始されていません。 + + --- diff --git a/.yoi/tickets/00001KVDH2E06/artifacts/.gitkeep b/.yoi/tickets/00001KVDH2E06/artifacts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.yoi/tickets/00001KVDH2E06/item.md b/.yoi/tickets/00001KVDH2E06/item.md new file mode 100644 index 00000000..624b39eb --- /dev/null +++ b/.yoi/tickets/00001KVDH2E06/item.md @@ -0,0 +1,94 @@ +--- +title: 'Panel 表示を現在 workspace の Pod に限定する' +state: 'ready' +created_at: '2026-06-18T14:09:59Z' +updated_at: '2026-06-18T14:10:12Z' +assignee: null +readiness: 'ready' +risk_flags: ['panel', 'pod-metadata', 'workspace-boundary', 'runtime-observation'] +--- + +## Background + +`yoi panel` は workspace の状況確認・Ticket queue・Orchestrator 操作の入口になっているため、別 workspace の Pod が一覧に混ざると、現在 workspace の状態として誤認しやすい。 + +Yoi では runtime workspace root、Pod identity、Profile、cwd、Ticket backend checkout が別概念として整理されている。したがって Panel の表示対象も、Pod 名や cwd の heuristic ではなく、Pod metadata に記録された runtime workspace identity を基準に workspace-scoped にする必要がある。 + +Request snapshot: + +- `yoi panel` の Pod 表示で、現在の workspace に属さない Pod が表示されないようにしたい。 +- Panel handoff context: + - workspace: `yoi` + - workspace_orchestrator_pod: `yoi-orchestrator` + +## Requirements + +- `yoi panel` の通常 Pod 表示は、現在の runtime workspace root に属する Pod のみに限定する。 +- 別 workspace の Pod は Panel の通常一覧・通常 action target に表示しない。 +- workspace Orchestrator Pod、Companion Pod、Ticket role Pod など、現在 workspace に属する role Pod は表示対象に残す。 +- dedicated orchestration worktree や implementation worktree を cwd にしている Pod でも、runtime workspace が現在 workspace なら表示対象に残す。 +- Pod 名 prefix や cwd だけで workspace 所属を推測しない。可能な限り persisted Pod metadata / resolved runtime workspace root を authority とする。 +- metadata が壊れている、または workspace 判定不能な Pod は、通常一覧に混ぜず、必要なら bounded diagnostic として扱う。 +- no-Ticket workspace / Pod-centric fallback でも、現在 workspace の Pod discovery と attach/open は維持する。 +- Ticket rows / queue actions / Panel Orchestrator lifecycle は維持する。 + +## Acceptance criteria + +- `yoi panel` を workspace `A` で開いたとき、workspace `B` の Pod が通常 Pod list に表示されない。 +- 現在 workspace の `workspace_orchestrator_pod`、Companion、Ticket role Pod は引き続き表示・操作できる。 +- cwd が `.worktree/...` 配下でも、runtime workspace が現在 workspace なら隠されない。 +- workspace 判定不能な legacy/corrupt Pod metadata が、現在 workspace の通常 row として誤表示されない。 +- Panel の attach/open は、表示されている現在 workspace Pod に対して従来通り機能する。 +- Focused unit tests または E2E/fixture tests で、複数 workspace の Pod metadata が存在する場合に Panel が現在 workspace の Pod だけを表示することを確認する。 + +## Binding decisions / invariants + +- Panel 表示スコープの authority は、runtime workspace root / Pod metadata に基づく。Pod name prefix や process cwd のみでは判定しない。 +- `role_workspace_root` / `original_workspace_root` / `implementation_worktree_root` / `merge_target_workspace_root` は混同しない。 +- dedicated orchestration worktree を使う Orchestrator は、cwd が workspace 外に見えても、runtime workspace が元 workspace なら表示対象に残す。 +- ワークスペース外 Pod を通常一覧に出してから UI 上で注意表示するのではなく、通常一覧から除外する。 +- 別 workspace の Pod に対する attach/open/action path を Panel から提供しない。 +- 既存 Pod metadata の破壊的 migration はこの Ticket の範囲外。必要なら escalation する。 +- ユーザー承認により、metadata が古く workspace 判定不能な Pod は通常 Panel 表示から隠し、必要なら diagnostic にだけ出す。 + +## Implementation latitude + +- workspace root の canonicalization / path comparison の具体実装は Coder が調査して選んでよい。 +- 判定不能 Pod の diagnostic 表示方法は、既存 Panel diagnostic pattern に合わせてよい。 +- 既存 ViewModel / Pod listing abstraction のどこで filter するかは実装調査に任せてよい。 +- E2E が重すぎる場合、まず unit/fixture test で複数 workspace Pod metadata を作る focused coverage でもよい。ただし user-visible Panel 挙動の確認手段は残す。 + +## Readiness + +- readiness: implementation_ready +- risk_flags: [panel, pod-metadata, workspace-boundary, runtime-observation] +- open_questions: none + +## Escalation conditions + +- 既存 metadata に runtime workspace root が十分に保存されておらず、正しい workspace 判定に schema/storage 変更が必要な場合。 +- legacy Pod を隠すことで既存の復元/attach 導線が実用上失われる場合。 +- workspace root canonicalization で symlink / moved checkout / worktree の扱いに人間判断が必要な場合。 +- Panel の no-Ticket fallback が「全 Pod dashboard」であるべきか「current workspace Pod dashboard」であるべきか、既存設計と衝突する場合。 +- 別 workspace Pod を診断用に見せる必要が出た場合。その場合も通常 action row とは分離する。 + +## Validation + +- Focused tests for Panel ViewModel / Pod list filtering: + - current workspace Pod is visible; + - other workspace Pod is hidden; + - workspace Orchestrator / role Pod for current workspace is visible; + - cwd/worktree difference alone does not hide current workspace Pod; + - unknown/corrupt workspace metadata is not treated as current workspace. +- Existing Panel/TUI tests continue to pass. +- If practical: Panel E2E fixture with multiple workspace Pod metadata records. +- `cargo fmt --check` +- `git diff --check` +- relevant `cargo test` / `cargo check` + +## Related work + +- `00001KTFQ109S`: Workspace panel Companion interface。 +- `00001KTFQ109V`: Remove workspace panel direct Pod send。 +- `00001KV0YK5S0`: E2E harness を完全な tmp runtime/data/workspace 隔離と cleanup に対応させる。 +- Runtime workspace / Pod identity decisions in existing memory and related closed runtime-workspace Tickets。 diff --git a/.yoi/tickets/00001KVDH2E06/thread.md b/.yoi/tickets/00001KVDH2E06/thread.md new file mode 100644 index 00000000..027e5bea --- /dev/null +++ b/.yoi/tickets/00001KVDH2E06/thread.md @@ -0,0 +1,23 @@ + + +## 作成 + +LocalTicketBackend によって作成されました。 + +--- + + + +## Intake summary + +ユーザー承認済み。`yoi panel` の通常 Pod 表示を現在の runtime workspace に属する Pod だけに限定する concrete work item。workspace 外 Pod は通常一覧/action target から除外し、workspace 判定不能な legacy/corrupt metadata は通常表示せず bounded diagnostic のみ許容する。受け入れ条件・binding invariants・validation は Ticket body に記録済み。 + +--- + + + +## State changed + +Ticket intake が完了しました。実装起動は Orchestrator routing / queue flow に委ねます。 + +--- diff --git a/.yoi/tickets/00001KVDJCVWZ/artifacts/.gitkeep b/.yoi/tickets/00001KVDJCVWZ/artifacts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.yoi/tickets/00001KVDJCVWZ/artifacts/relations.json b/.yoi/tickets/00001KVDJCVWZ/artifacts/relations.json new file mode 100644 index 00000000..aa0ff15f --- /dev/null +++ b/.yoi/tickets/00001KVDJCVWZ/artifacts/relations.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "relations": [ + { + "ticket_id": "00001KVDJCVWZ", + "kind": "related", + "target": "00001KTTW04W2", + "note": "Fixes live delivery gap for auto_run:false Orchestrator Ticket event Companion notifications.", + "author": "yoi ticket", + "at": "2026-06-18T14:33:50Z" + } + ] +} diff --git a/.yoi/tickets/00001KVDJCVWZ/item.md b/.yoi/tickets/00001KVDJCVWZ/item.md new file mode 100644 index 00000000..429b7dd3 --- /dev/null +++ b/.yoi/tickets/00001KVDJCVWZ/item.md @@ -0,0 +1,70 @@ +--- +title: 'Orchestrator Ticket event Companion notify の peer registration / diagnostics を修正する' +state: 'done' +created_at: '2026-06-18T14:33:09Z' +updated_at: '2026-06-18T14:33:50Z' +assignee: null +readiness: 'implementation_ready' +risk_flags: ['orchestrator', 'companion', 'peer-notify', 'ticket-event', 'auto-run-false', 'diagnostics'] +--- + +## Background + +`00001KTTW04W2` で Orchestrator role の明示 Ticket lifecycle event を workspace Companion に `Notify { auto_run: false }` で送る hook は実装済みだった。しかし live 状態では Orchestrator metadata 側に workspace Companion peer が無いと、`send_weak_notify_to_live_peer` が silent no-op になり、Companion に通知が届かない。 + +現在の運用では workspace Companion `yoi` と `yoi-orchestrator` が同じ workspace の role Pod として存在していても、peer metadata が片方向または欠落することがある。この場合、通知 hook は実行されても delivery 前提を満たせず、ユーザーには何も見えない。 + +この Ticket では peer 境界を緩めず、workspace Companion / Orchestrator の reciprocal peer relationship を通知前に保証し、delivery skip reason を bounded diagnostic として残す。 + +## Requirements + +- Orchestrator Ticket event Companion notify は `auto_run: false` を維持する。 +- Companion missing / stopped / unreachable では spawn / restore しない。 +- Peer visibility check は維持する。 + - arbitrary Pod name へ notify できるようにはしない。 +- Orchestrator startup / hook install 時に、既存 workspace Companion metadata があれば reciprocal peer を ensure する。 +- Ticket event hook 実行時にも、既存 workspace Companion metadata があれば reciprocal peer を ensure する。 + - 既存 running state で片方向 peer / missing peer があっても回復できるようにする。 +- `send_weak_notify_to_live_peer` は bool ではなく delivery reason を返す。 + - delivered + - missing metadata + - not visible + - visible but not peer + - not live / unreachable + - send failed +- Delivery skip / failure reason は bounded tracing diagnostic として確認できる。 +- Missing Companion は debug/no-op に留める。 +- Ticket event hook は passive Ticket read/list/show では発火しない既存条件を維持する。 + +## Implementation summary + +- `PodDiscovery::ensure_existing_peer` を追加し、peer metadata が存在する場合だけ reciprocal peer registration を行う。 +- `register_peer` は `ensure_existing_peer` を使う形に整理し、missing peer は従来どおり `MissingPod` error を返す。 +- `WeakNotifyDelivery` を追加し、weak notify delivery result を reason 付きで返すようにした。 +- Orchestrator Ticket event hook install 時に existing Companion peer を ensure する。 +- Ticket event hook call 時にも existing Companion peer を ensure してから weak notify する。 +- Delivery skipped / send failed を `warn!`、missing Companion / ensured peer を `debug!` で記録する。 +- Tests を追加・更新し、peer が事前登録されていない既存 Companion metadata でも hook が reciprocal peer を作り、`Notify { auto_run: false }` を届けることを確認した。 + +## Acceptance criteria + +- Existing workspace Companion metadata がある場合、Orchestrator Ticket event notify 前に reciprocal peer が ensure される。 +- Orchestrator Ticket event hook は peer 未登録の既存 Companion に `Notify { auto_run: false }` を届けられる。 +- `send_weak_notify_to_live_peer` は delivered / skipped reason を区別して返す。 +- Spawned-child visibility など peer ではない Pod には weak notify しない。 +- Missing Companion では spawn / restore せず no-op diagnostic に留める。 +- Passive Ticket tool call では通知しない既存挙動を維持する。 + +## Validation + +- `cargo test -p pod discovery::tests::register_peer_persists_reciprocal_metadata --no-default-features` +- `cargo test -p pod weak_notify --no-default-features` +- `cargo test -p pod ticket_event_notify --no-default-features` +- `cargo check -p pod -p tui --all-targets` +- `cargo fmt --check` +- `git diff --check` +- `nix build .#yoi --no-link` + +## Related work + +- `00001KTTW04W2` — Orchestrator進捗をAutoKickなしでCompanionへ通知する。 diff --git a/.yoi/tickets/00001KVDJCVWZ/thread.md b/.yoi/tickets/00001KVDJCVWZ/thread.md new file mode 100644 index 00000000..d719ae04 --- /dev/null +++ b/.yoi/tickets/00001KVDJCVWZ/thread.md @@ -0,0 +1,7 @@ + + +## 作成 + +LocalTicketBackend によって作成されました。 + +--- diff --git a/crates/pod/src/controller.rs b/crates/pod/src/controller.rs index cc8b5b06..2a36a9e9 100644 --- a/crates/pod/src/controller.rs +++ b/crates/pod/src/controller.rs @@ -10,6 +10,7 @@ use session_store::Store; use ticket::LocalTicketBackend; use ticket::config::TicketConfig; use tokio::sync::{broadcast, mpsc, oneshot}; +use tracing::{debug, warn}; use crate::discovery::{PodDiscovery, list_pods_tool, restore_pod_tool, send_to_peer_pod_tool}; use crate::feature::FeatureRegistryBuilder; @@ -546,6 +547,30 @@ fn install_ticket_event_companion_notify_hook( pod.cwd().to_path_buf(), spawned_registry, ); + match discovery.ensure_existing_peer(&companion_pod_name) { + Ok(Some(_)) => { + debug!( + companion = %companion_pod_name, + orchestrator = %pod.manifest().pod.name, + "ensured Companion peer relationship for Orchestrator Ticket event notifications" + ); + } + Ok(None) => { + debug!( + companion = %companion_pod_name, + orchestrator = %pod.manifest().pod.name, + "Companion metadata is missing; Ticket event notifications will skip until Companion exists" + ); + } + Err(error) => { + warn!( + companion = %companion_pod_name, + orchestrator = %pod.manifest().pod.name, + error = %error, + "failed to ensure Companion peer relationship for Orchestrator Ticket event notifications" + ); + } + } pod.add_post_tool_call_hook(TicketEventCompanionNotifyHook::new( LocalTicketBackend::new(backend_root), discovery, diff --git a/crates/pod/src/discovery.rs b/crates/pod/src/discovery.rs index 9ca50323..82fa07bf 100644 --- a/crates/pod/src/discovery.rs +++ b/crates/pod/src/discovery.rs @@ -7,6 +7,7 @@ //! state that exists but is outside that visibility set. use std::collections::BTreeMap; +use std::fmt; use std::io; use std::path::{Path, PathBuf}; use std::process::Stdio; @@ -177,6 +178,16 @@ where &self, peer_name: &str, ) -> Result { + self.ensure_existing_peer(peer_name)? + .ok_or_else(|| PodDiscoveryError::MissingPod { + pod_name: peer_name.to_string(), + }) + } + + pub fn ensure_existing_peer( + &self, + peer_name: &str, + ) -> Result, PodDiscoveryError> { validate_pod_name(peer_name)?; if peer_name == self.self_pod_name { return Err(PodDiscoveryError::SelfPeer { @@ -191,9 +202,7 @@ where })?; let prior_self_peers = self_metadata.peers.clone(); if self.store.read_by_name(peer_name)?.is_none() { - return Err(PodDiscoveryError::MissingPod { - pod_name: peer_name.to_string(), - }); + return Ok(None); } self.store.add_peer(&self.self_pod_name, peer_name)?; @@ -202,10 +211,10 @@ where return Err(PodDiscoveryError::PodStore(error)); } - Ok(PeerRegistrationResult { + Ok(Some(PeerRegistrationResult { source: self.self_pod_name.clone(), peer: peer_name.to_string(), - }) + })) } async fn visibility(&self) -> Result { @@ -354,16 +363,41 @@ where } } - pub async fn send_weak_notify_to_live_peer(&self, peer_name: &str, message: String) -> bool { - let Ok(detail) = self.inspect(peer_name).await else { - return false; + pub async fn send_weak_notify_to_live_peer( + &self, + peer_name: &str, + message: String, + ) -> WeakNotifyDelivery { + let detail = match self.inspect(peer_name).await { + Ok(detail) => detail, + Err(PodDiscoveryError::StateMissing { .. } | PodDiscoveryError::MissingPod { .. }) => { + return WeakNotifyDelivery::SkippedMissing; + } + Err(PodDiscoveryError::NotVisible { .. }) => { + return WeakNotifyDelivery::SkippedNotVisible; + } + Err(error) => { + return WeakNotifyDelivery::SendFailed { + error: error.to_string(), + }; + } }; - if detail.visibility != VisibilityReason::Peer || !detail.live.reachable { - return false; + if detail.visibility != VisibilityReason::Peer { + return WeakNotifyDelivery::SkippedNotPeer { + visibility: detail.visibility, + }; + } + if !detail.live.reachable { + return WeakNotifyDelivery::SkippedNotLive { + reason: detail.live.error, + }; + } + match send_notify(&detail.live.socket_path, message, false).await { + Ok(()) => WeakNotifyDelivery::Delivered, + Err(error) => WeakNotifyDelivery::SendFailed { + error: error.to_string(), + }, } - send_notify(&detail.live.socket_path, message, false) - .await - .is_ok() } async fn live_for_name(&self, pod_name: &str, socket_override: Option<&Path>) -> LiveInfo { @@ -585,6 +619,50 @@ pub struct PeerRegistrationResult { pub peer: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WeakNotifyDelivery { + Delivered, + SkippedMissing, + SkippedNotVisible, + SkippedNotPeer { visibility: VisibilityReason }, + SkippedNotLive { reason: Option }, + SendFailed { error: String }, +} + +impl WeakNotifyDelivery { + pub fn delivered(&self) -> bool { + matches!(self, WeakNotifyDelivery::Delivered) + } +} + +impl fmt::Display for WeakNotifyDelivery { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WeakNotifyDelivery::Delivered => write!(f, "delivered"), + WeakNotifyDelivery::SkippedMissing => { + write!(f, "skipped: target pod metadata is missing") + } + WeakNotifyDelivery::SkippedNotVisible => { + write!(f, "skipped: target pod is not visible") + } + WeakNotifyDelivery::SkippedNotPeer { visibility } => { + write!( + f, + "skipped: target pod is visible as {visibility:?}, not peer" + ) + } + WeakNotifyDelivery::SkippedNotLive { reason } => { + if let Some(reason) = reason { + write!(f, "skipped: target peer is not live/reachable ({reason})") + } else { + write!(f, "skipped: target peer is not live/reachable") + } + } + WeakNotifyDelivery::SendFailed { error } => write!(f, "send failed: {error}"), + } + } +} + #[derive(Debug, thiserror::Error)] pub enum PodDiscoveryError { #[error("pod state missing for `{pod_name}`")] @@ -1524,18 +1602,20 @@ mod tests { } }); - assert!( + assert_eq!( discovery .send_weak_notify_to_live_peer("target", "weak event".into()) - .await + .await, + WeakNotifyDelivery::Delivered ); assert_eq!(rx.recv().await.unwrap(), "weak event"); target.await.unwrap(); - assert!( - !discovery + assert_eq!( + discovery .send_weak_notify_to_live_peer("missing", "no-op".into()) - .await + .await, + WeakNotifyDelivery::SkippedMissing ); } @@ -1567,10 +1647,13 @@ mod tests { SpawnedPodRegistry::new(runtime_dir), ); - assert!( - !discovery + assert_eq!( + discovery .send_weak_notify_to_live_peer("target", "must not send".into()) - .await + .await, + WeakNotifyDelivery::SkippedNotPeer { + visibility: VisibilityReason::SpawnedChild + } ); } diff --git a/crates/pod/src/ticket_event_notify.rs b/crates/pod/src/ticket_event_notify.rs index aa9d1e96..124ac5fa 100644 --- a/crates/pod/src/ticket_event_notify.rs +++ b/crates/pod/src/ticket_event_notify.rs @@ -5,9 +5,9 @@ use minijinja::Value as TemplateValue; use serde_json::Value; use std::collections::BTreeMap; use ticket::{LocalTicketBackend, TicketBackend, TicketIdOrSlug}; -use tracing::debug; +use tracing::{debug, warn}; -use crate::discovery::PodDiscovery; +use crate::discovery::{PodDiscovery, WeakNotifyDelivery}; use crate::hook::{Hook, HookPostToolAction, PostToolCall, ToolResultSummary}; use crate::prompt::catalog::{PodPrompt, PromptCatalog}; use pod_store::PodMetadataStore; @@ -48,17 +48,58 @@ impl Hook let Some(notice) = build_ticket_event_notice(&self.backend, summary) else { return HookPostToolAction::Continue; }; - let delivered = self + match self + .discovery + .ensure_existing_peer(&self.companion_pod_name) + { + Ok(Some(_)) => { + debug!( + ticket = %notice.ticket_id, + event_kind = %notice.event_kind, + companion = %self.companion_pod_name, + "ensured Companion peer relationship before Ticket event notification" + ); + } + Ok(None) => { + debug!( + ticket = %notice.ticket_id, + event_kind = %notice.event_kind, + companion = %self.companion_pod_name, + "skipping Companion peer registration because Companion metadata is missing" + ); + } + Err(error) => { + warn!( + ticket = %notice.ticket_id, + event_kind = %notice.event_kind, + companion = %self.companion_pod_name, + error = %error, + "failed to ensure Companion peer relationship before Ticket event notification" + ); + } + } + let delivery = self .discovery .send_weak_notify_to_live_peer(&self.companion_pod_name, notice.message) .await; - if delivered { - debug!( - ticket = %notice.ticket_id, - event_kind = %notice.event_kind, - companion = %self.companion_pod_name, - "delivered weak Ticket event notification to Companion peer" - ); + match delivery { + WeakNotifyDelivery::Delivered => { + debug!( + ticket = %notice.ticket_id, + event_kind = %notice.event_kind, + companion = %self.companion_pod_name, + "delivered weak Ticket event notification to Companion peer" + ); + } + skipped => { + warn!( + ticket = %notice.ticket_id, + event_kind = %notice.event_kind, + companion = %self.companion_pod_name, + delivery = %skipped, + "skipped weak Ticket event notification to Companion peer" + ); + } } HookPostToolAction::Continue } @@ -327,7 +368,7 @@ mod tests { } #[tokio::test(flavor = "current_thread")] - async fn ticket_event_hook_delivers_weak_companion_notification() { + async fn ticket_event_hook_ensures_peer_and_delivers_weak_companion_notification() { let root = tempdir().expect("tempdir"); let runtime_base = root.path().join("runtime"); let store_dir = root.path().join("store"); @@ -339,9 +380,7 @@ mod tests { active: None, spawned_children: Vec::new(), reclaimed_children: Vec::new(), - peers: vec![pod_store::PodPeer { - pod_name: "companion".into(), - }], + peers: Vec::new(), resolved_manifest_snapshot: None, }) .unwrap(); @@ -351,9 +390,7 @@ mod tests { active: None, spawned_children: Vec::new(), reclaimed_children: Vec::new(), - peers: vec![pod_store::PodPeer { - pod_name: "orchestrator".into(), - }], + peers: Vec::new(), resolved_manifest_snapshot: None, }) .unwrap(); @@ -363,6 +400,7 @@ mod tests { .await .unwrap(), ); + let store_for_assert = store.clone(); let hook = TicketEventCompanionNotifyHook::new( backend, PodDiscovery::new( @@ -448,6 +486,15 @@ mod tests { let message = rx.recv().await.unwrap(); assert!(message.contains("event: state/queued->inprogress")); assert!(message.contains("title: Companion event hook")); + let orchestrator = store_for_assert + .read_by_name("orchestrator") + .unwrap() + .unwrap(); + assert_eq!(orchestrator.peers.len(), 1); + assert_eq!(orchestrator.peers[0].pod_name, "companion"); + let companion_metadata = store_for_assert.read_by_name("companion").unwrap().unwrap(); + assert_eq!(companion_metadata.peers.len(), 1); + assert_eq!(companion_metadata.peers[0].pod_name, "orchestrator"); companion.await.unwrap(); } } diff --git a/devshell.nix b/devshell.nix index c659f8a6..ae06238c 100644 --- a/devshell.nix +++ b/devshell.nix @@ -12,6 +12,12 @@ pkgs.mkShell { openssl ]; shellHook = '' + if repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + export YOI_POD_RUNTIME_COMMAND="$repo_root/target/debug/yoi" + else + export YOI_POD_RUNTIME_COMMAND="$PWD/target/debug/yoi" + fi echo "dev-shell-loaded" + echo "YOI_POD_RUNTIME_COMMAND=$YOI_POD_RUNTIME_COMMAND" ''; }