fix: restore orchestrator companion notifications

This commit is contained in:
Keisuke Hirata 2026-06-18 23:42:07 +09:00
parent dcbfb6314e
commit cd86cc533c
No known key found for this signature in database
14 changed files with 431 additions and 40 deletions

View File

@ -1,8 +1,8 @@
--- ---
title: 'Plugin: execute Plugin Tool with minimal WASM runtime' title: 'Plugin: execute Plugin Tool with minimal WASM runtime'
state: 'done' state: 'closed'
created_at: '2026-06-15T14:48:59Z' created_at: '2026-06-15T14:48:59Z'
updated_at: '2026-06-18T12:39:30Z' updated_at: '2026-06-18T13:55:12Z'
assignee: null assignee: null
readiness: 'implementation_ready' readiness: 'implementation_ready'
risk_flags: ['plugin', 'wasm', 'tool-runtime', 'sandbox', 'capability-boundary', 'cancellation'] risk_flags: ['plugin', 'wasm', 'tool-runtime', 'sandbox', 'capability-boundary', 'cancellation']

View File

@ -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 は開始されていません。

View File

@ -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. 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.
---
<!-- event: state_changed author: hare at: 2026-06-18T13:55:12Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-18T13:55:12Z status: closed -->
## 完了
Ticket `00001KV5W3PHW` (`Plugin: execute Plugin Tool with minimal WASM runtime`) はすでに `state: done` に到達していたため、workspace Panel から close しました。
この Close action によって、実装作業、state 変更、Orchestrator/Companion launch、worker invocation は開始されていません。
--- ---

View File

@ -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。

View File

@ -0,0 +1,23 @@
<!-- event: create author: ticket-intake at: 2026-06-18T14:09:59Z -->
## 作成
LocalTicketBackend によって作成されました。
---
<!-- event: intake_summary author: ticket-intake at: 2026-06-18T14:10:12Z -->
## 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 に記録済み。
---
<!-- event: state_changed author: ticket-intake at: 2026-06-18T14:10:12Z from: planning to: ready reason: user_approved_intake_ready field: state -->
## State changed
Ticket intake が完了しました。実装起動は Orchestrator routing / queue flow に委ねます。
---

View File

@ -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"
}
]
}

View File

@ -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へ通知する。

View File

@ -0,0 +1,7 @@
<!-- event: create author: "yoi ticket" at: 2026-06-18T14:33:09Z -->
## 作成
LocalTicketBackend によって作成されました。
---

View File

@ -10,6 +10,7 @@ use session_store::Store;
use ticket::LocalTicketBackend; use ticket::LocalTicketBackend;
use ticket::config::TicketConfig; use ticket::config::TicketConfig;
use tokio::sync::{broadcast, mpsc, oneshot}; 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::discovery::{PodDiscovery, list_pods_tool, restore_pod_tool, send_to_peer_pod_tool};
use crate::feature::FeatureRegistryBuilder; use crate::feature::FeatureRegistryBuilder;
@ -546,6 +547,30 @@ fn install_ticket_event_companion_notify_hook<C, St>(
pod.cwd().to_path_buf(), pod.cwd().to_path_buf(),
spawned_registry, 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( pod.add_post_tool_call_hook(TicketEventCompanionNotifyHook::new(
LocalTicketBackend::new(backend_root), LocalTicketBackend::new(backend_root),
discovery, discovery,

View File

@ -7,6 +7,7 @@
//! state that exists but is outside that visibility set. //! state that exists but is outside that visibility set.
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Stdio; use std::process::Stdio;
@ -177,6 +178,16 @@ where
&self, &self,
peer_name: &str, peer_name: &str,
) -> Result<PeerRegistrationResult, PodDiscoveryError> { ) -> Result<PeerRegistrationResult, PodDiscoveryError> {
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<Option<PeerRegistrationResult>, PodDiscoveryError> {
validate_pod_name(peer_name)?; validate_pod_name(peer_name)?;
if peer_name == self.self_pod_name { if peer_name == self.self_pod_name {
return Err(PodDiscoveryError::SelfPeer { return Err(PodDiscoveryError::SelfPeer {
@ -191,9 +202,7 @@ where
})?; })?;
let prior_self_peers = self_metadata.peers.clone(); let prior_self_peers = self_metadata.peers.clone();
if self.store.read_by_name(peer_name)?.is_none() { if self.store.read_by_name(peer_name)?.is_none() {
return Err(PodDiscoveryError::MissingPod { return Ok(None);
pod_name: peer_name.to_string(),
});
} }
self.store.add_peer(&self.self_pod_name, peer_name)?; self.store.add_peer(&self.self_pod_name, peer_name)?;
@ -202,10 +211,10 @@ where
return Err(PodDiscoveryError::PodStore(error)); return Err(PodDiscoveryError::PodStore(error));
} }
Ok(PeerRegistrationResult { Ok(Some(PeerRegistrationResult {
source: self.self_pod_name.clone(), source: self.self_pod_name.clone(),
peer: peer_name.to_string(), peer: peer_name.to_string(),
}) }))
} }
async fn visibility(&self) -> Result<VisibilitySet, PodDiscoveryError> { async fn visibility(&self) -> Result<VisibilitySet, PodDiscoveryError> {
@ -354,16 +363,41 @@ where
} }
} }
pub async fn send_weak_notify_to_live_peer(&self, peer_name: &str, message: String) -> bool { pub async fn send_weak_notify_to_live_peer(
let Ok(detail) = self.inspect(peer_name).await else { &self,
return false; 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 { if detail.visibility != VisibilityReason::Peer {
return false; 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 { async fn live_for_name(&self, pod_name: &str, socket_override: Option<&Path>) -> LiveInfo {
@ -585,6 +619,50 @@ pub struct PeerRegistrationResult {
pub peer: String, pub peer: String,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WeakNotifyDelivery {
Delivered,
SkippedMissing,
SkippedNotVisible,
SkippedNotPeer { visibility: VisibilityReason },
SkippedNotLive { reason: Option<String> },
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)] #[derive(Debug, thiserror::Error)]
pub enum PodDiscoveryError { pub enum PodDiscoveryError {
#[error("pod state missing for `{pod_name}`")] #[error("pod state missing for `{pod_name}`")]
@ -1524,18 +1602,20 @@ mod tests {
} }
}); });
assert!( assert_eq!(
discovery discovery
.send_weak_notify_to_live_peer("target", "weak event".into()) .send_weak_notify_to_live_peer("target", "weak event".into())
.await .await,
WeakNotifyDelivery::Delivered
); );
assert_eq!(rx.recv().await.unwrap(), "weak event"); assert_eq!(rx.recv().await.unwrap(), "weak event");
target.await.unwrap(); target.await.unwrap();
assert!( assert_eq!(
!discovery discovery
.send_weak_notify_to_live_peer("missing", "no-op".into()) .send_weak_notify_to_live_peer("missing", "no-op".into())
.await .await,
WeakNotifyDelivery::SkippedMissing
); );
} }
@ -1567,10 +1647,13 @@ mod tests {
SpawnedPodRegistry::new(runtime_dir), SpawnedPodRegistry::new(runtime_dir),
); );
assert!( assert_eq!(
!discovery discovery
.send_weak_notify_to_live_peer("target", "must not send".into()) .send_weak_notify_to_live_peer("target", "must not send".into())
.await .await,
WeakNotifyDelivery::SkippedNotPeer {
visibility: VisibilityReason::SpawnedChild
}
); );
} }

View File

@ -5,9 +5,9 @@ use minijinja::Value as TemplateValue;
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use ticket::{LocalTicketBackend, TicketBackend, TicketIdOrSlug}; 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::hook::{Hook, HookPostToolAction, PostToolCall, ToolResultSummary};
use crate::prompt::catalog::{PodPrompt, PromptCatalog}; use crate::prompt::catalog::{PodPrompt, PromptCatalog};
use pod_store::PodMetadataStore; use pod_store::PodMetadataStore;
@ -48,17 +48,58 @@ impl<St: PodMetadataStore + Clone + Send + Sync + 'static> Hook<PostToolCall>
let Some(notice) = build_ticket_event_notice(&self.backend, summary) else { let Some(notice) = build_ticket_event_notice(&self.backend, summary) else {
return HookPostToolAction::Continue; 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 .discovery
.send_weak_notify_to_live_peer(&self.companion_pod_name, notice.message) .send_weak_notify_to_live_peer(&self.companion_pod_name, notice.message)
.await; .await;
if delivered { match delivery {
debug!( WeakNotifyDelivery::Delivered => {
ticket = %notice.ticket_id, debug!(
event_kind = %notice.event_kind, ticket = %notice.ticket_id,
companion = %self.companion_pod_name, event_kind = %notice.event_kind,
"delivered weak Ticket event notification to Companion peer" 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 HookPostToolAction::Continue
} }
@ -327,7 +368,7 @@ mod tests {
} }
#[tokio::test(flavor = "current_thread")] #[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 root = tempdir().expect("tempdir");
let runtime_base = root.path().join("runtime"); let runtime_base = root.path().join("runtime");
let store_dir = root.path().join("store"); let store_dir = root.path().join("store");
@ -339,9 +380,7 @@ mod tests {
active: None, active: None,
spawned_children: Vec::new(), spawned_children: Vec::new(),
reclaimed_children: Vec::new(), reclaimed_children: Vec::new(),
peers: vec![pod_store::PodPeer { peers: Vec::new(),
pod_name: "companion".into(),
}],
resolved_manifest_snapshot: None, resolved_manifest_snapshot: None,
}) })
.unwrap(); .unwrap();
@ -351,9 +390,7 @@ mod tests {
active: None, active: None,
spawned_children: Vec::new(), spawned_children: Vec::new(),
reclaimed_children: Vec::new(), reclaimed_children: Vec::new(),
peers: vec![pod_store::PodPeer { peers: Vec::new(),
pod_name: "orchestrator".into(),
}],
resolved_manifest_snapshot: None, resolved_manifest_snapshot: None,
}) })
.unwrap(); .unwrap();
@ -363,6 +400,7 @@ mod tests {
.await .await
.unwrap(), .unwrap(),
); );
let store_for_assert = store.clone();
let hook = TicketEventCompanionNotifyHook::new( let hook = TicketEventCompanionNotifyHook::new(
backend, backend,
PodDiscovery::new( PodDiscovery::new(
@ -448,6 +486,15 @@ mod tests {
let message = rx.recv().await.unwrap(); let message = rx.recv().await.unwrap();
assert!(message.contains("event: state/queued->inprogress")); assert!(message.contains("event: state/queued->inprogress"));
assert!(message.contains("title: Companion event hook")); 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(); companion.await.unwrap();
} }
} }

View File

@ -12,6 +12,12 @@ pkgs.mkShell {
openssl openssl
]; ];
shellHook = '' 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 "dev-shell-loaded"
echo "YOI_POD_RUNTIME_COMMAND=$YOI_POD_RUNTIME_COMMAND"
''; '';
} }