fix: restore orchestrator companion notifications
This commit is contained in:
parent
dcbfb6314e
commit
cd86cc533c
|
|
@ -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']
|
||||||
|
|
|
||||||
3
.yoi/tickets/00001KV5W3PHW/resolution.md
Normal file
3
.yoi/tickets/00001KV5W3PHW/resolution.md
Normal 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 は開始されていません。
|
||||||
|
|
@ -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 は開始されていません。
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
0
.yoi/tickets/00001KVDH2E06/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVDH2E06/artifacts/.gitkeep
Normal file
94
.yoi/tickets/00001KVDH2E06/item.md
Normal file
94
.yoi/tickets/00001KVDH2E06/item.md
Normal 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。
|
||||||
23
.yoi/tickets/00001KVDH2E06/thread.md
Normal file
23
.yoi/tickets/00001KVDH2E06/thread.md
Normal 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 に委ねます。
|
||||||
|
|
||||||
|
---
|
||||||
0
.yoi/tickets/00001KVDJCVWZ/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVDJCVWZ/artifacts/.gitkeep
Normal file
13
.yoi/tickets/00001KVDJCVWZ/artifacts/relations.json
Normal file
13
.yoi/tickets/00001KVDJCVWZ/artifacts/relations.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
70
.yoi/tickets/00001KVDJCVWZ/item.md
Normal file
70
.yoi/tickets/00001KVDJCVWZ/item.md
Normal 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へ通知する。
|
||||||
7
.yoi/tickets/00001KVDJCVWZ/thread.md
Normal file
7
.yoi/tickets/00001KVDJCVWZ/thread.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<!-- event: create author: "yoi ticket" at: 2026-06-18T14:33:09Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user