-
Notifications
You must be signed in to change notification settings - Fork 67
feat(broker): publish fleet workers' provider session id on their Relaycast agent #1853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ use crate::{ | |
| listen_api::{DeliveryRouteError, ListenApiRequest, SetInboundDeliveryModeOk}, | ||
| node_control::{delivery_ack, handler_unavailable_result, DeliveryDecision, ReceiptAckability}, | ||
| node_delivery_probe::DeliverDisposition, | ||
| relaycast::SessionMetadataPublish, | ||
| terminal_control::{ | ||
| request_terminal_reconnect, TerminalControlCommand, TerminalControlEvent, | ||
| TerminalDeliveryDiagnostics, TerminalFromCloud, TerminalMode, TerminalToCloud, | ||
|
|
@@ -2294,6 +2295,98 @@ pub(super) fn spawn_declared_metadata_publish( | |
| }); | ||
| } | ||
|
|
||
| /// The provider session a spawned worker runs, as published on its Relaycast | ||
| /// agent: `(session_id, session_kind)`. | ||
| /// | ||
| /// `session_id` is the id the worker's CLI records its session under — the | ||
| /// Claude Code session UUID or the Codex thread id — resolved by | ||
| /// `WorkerRegistry::spawn` before the process starts (and reported as | ||
| /// `sessionId` by `GET /api/spawned`). `None` when the spawn has no session id, | ||
| /// e.g. a CLI the broker cannot pre-assign one for. | ||
| /// | ||
| /// `session_kind` names the CLI family, using the same values desktop session | ||
| /// agents publish where they overlap: `claude-terminal` for an interactive | ||
| /// (PTY) Claude Code session, `codex` for Codex. Other CLIs report their | ||
| /// normalized CLI name (headless Claude reports `claude`). | ||
| pub(super) fn worker_session_metadata(spec: &AgentSpec) -> Option<(String, Option<String>)> { | ||
| let session_id = spec | ||
| .session_id | ||
| .as_deref() | ||
| .map(str::trim) | ||
| .filter(|id| !id.is_empty())? | ||
| .to_string(); | ||
| let cli = spec | ||
| .cli | ||
| .as_deref() | ||
| // Parse the executable the way worker startup does, so a quoted path | ||
| // with spaces (`"/opt/AI Tools/codex" --flag`) names `codex`. | ||
| .and_then(|cli| crate::cli::command_parse::parse_cli_command(cli).ok()) | ||
| .map(|(command, _)| crate::cli::command_parse::normalize_cli_name(&command).to_lowercase()) | ||
| .or_else(|| { | ||
| spec.provider | ||
| .as_ref() | ||
| .map(|provider| super::headless::headless_provider_cli_name(provider).to_string()) | ||
| }); | ||
| let kind = cli.map(|cli| { | ||
| let family = cli.split(':').next().unwrap_or(&cli).to_string(); | ||
| match family.as_str() { | ||
| "claude" if spec.runtime == AgentRuntime::Pty => "claude-terminal".to_string(), | ||
| _ => family, | ||
| } | ||
| }); | ||
| Some((session_id, kind)) | ||
| } | ||
|
|
||
| /// Publish a freshly spawned (or respawned) worker's provider session id onto | ||
| /// its Relaycast agent, on its own task. | ||
| /// | ||
| /// Called after every successful spawn of a worker that holds a hosted | ||
| /// identity, so a respawn that resumes or starts a different session updates | ||
| /// the published id. The session claim is taken here, synchronously and even | ||
| /// when the spawn has no session id, so any publish still pending from an | ||
| /// earlier worker of the same name is superseded and cannot label this one | ||
| /// with the old session. Detached and best-effort for the same reasons as | ||
| /// [`spawn_declared_metadata_publish`]: a failure is logged, never fatal. | ||
| pub(super) fn spawn_session_metadata_publish( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This introduces externally observable broker behavior that publishes fleet workers' provider-session metadata for dashboard linking, but the commit leaves AGENTS.md reference: AGENTS.md:L31-L34 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in e6f4e0e under |
||
| relaycast_http: &RelaycastHttpClient, | ||
| name: &str, | ||
| spec: &AgentSpec, | ||
| ) { | ||
| let Ok(claim) = relaycast_http.claim_session_metadata(name) else { | ||
| return; | ||
| }; | ||
| let Some((session_id, session_kind)) = worker_session_metadata(spec) else { | ||
| return; | ||
| }; | ||
| let http = relaycast_http.clone(); | ||
| tokio::spawn(async move { | ||
| let agent = claim.agent_name(); | ||
| match http | ||
| .publish_session_metadata(&claim, &session_id, session_kind.as_deref()) | ||
| .await | ||
|
Comment on lines
+2361
to
+2366
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Old session links to replacement worker When a name is reused, Learn moreA successful spawn starts a detached session-metadata task. The task keeps the worker's name but not the agent ID or process generation. If the name is released and registered again before the PATCH completes, the old task can update the replacement's agent. Example: Worker Recommended fix: Fence publication to the registered agent ID and worker generation, or serialize and cancel pending publications per name. A name-only PATCH cannot provide identity fencing by itself. Was this helpful? React with 👍 or 👎 to provide feedback.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e6f4e0e. Each successful spawn now claims the name's session publish synchronously ( |
||
| { | ||
| Ok(SessionMetadataPublish::Published) => tracing::debug!( | ||
| worker = %agent, | ||
| session_id = %session_id, | ||
| "published provider session id for spawned agent" | ||
| ), | ||
| Ok(SessionMetadataPublish::Superseded) => tracing::debug!( | ||
| worker = %agent, | ||
| session_id = %session_id, | ||
| "skipped provider session id publish; a later spawn of this name owns it" | ||
| ), | ||
| Ok(SessionMetadataPublish::NothingToPublish) => {} | ||
| Err(error) => tracing::error!( | ||
| worker = %agent, | ||
| session_id = %session_id, | ||
| error = %error, | ||
| "failed to publish provider session id; the agent is registered and running \ | ||
| but its session cannot be linked to it" | ||
| ), | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /// Bind an agent to this node by sending node-control `agent.register` and | ||
| /// awaiting the engine reply with the minted agent token. This is the single | ||
| /// "register agent via node" step both the `/api/spawn` path and the node | ||
|
|
@@ -4652,6 +4745,61 @@ mod tests { | |
| assert_eq!(delivery_book.active_agent_id("worker-a"), None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn worker_session_metadata_reports_provider_session_and_kind() { | ||
| let mut spec = test_agent_spec(Some(" thread-123 "), None); | ||
| assert_eq!( | ||
| worker_session_metadata(&spec), | ||
| Some(("thread-123".to_string(), Some("codex".to_string()))) | ||
| ); | ||
|
|
||
| spec.cli = Some("/usr/local/bin/claude --model opus".to_string()); | ||
| assert_eq!( | ||
| worker_session_metadata(&spec), | ||
| Some(( | ||
| "thread-123".to_string(), | ||
| Some("claude-terminal".to_string()) | ||
| )) | ||
| ); | ||
|
|
||
| // A quoted executable path with spaces names the binary, not the | ||
| // first whitespace-separated fragment of its directory. | ||
| spec.cli = Some("\"/opt/AI Tools/codex\" --full-auto".to_string()); | ||
| assert_eq!( | ||
| worker_session_metadata(&spec), | ||
| Some(("thread-123".to_string(), Some("codex".to_string()))) | ||
| ); | ||
|
|
||
| spec.cli = Some("claude:opus".to_string()); | ||
| spec.runtime = AgentRuntime::Headless; | ||
| assert_eq!( | ||
| worker_session_metadata(&spec), | ||
| Some(("thread-123".to_string(), Some("claude".to_string()))) | ||
| ); | ||
|
|
||
| spec.cli = None; | ||
| spec.provider = Some(ProtocolHeadlessProvider::Opencode); | ||
| assert_eq!( | ||
| worker_session_metadata(&spec), | ||
| Some(("thread-123".to_string(), Some("opencode".to_string()))) | ||
| ); | ||
|
|
||
| spec.provider = None; | ||
| assert_eq!( | ||
| worker_session_metadata(&spec), | ||
| Some(("thread-123".to_string(), None)) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn worker_session_metadata_is_none_without_a_session_id() { | ||
| assert_eq!(worker_session_metadata(&test_agent_spec(None, None)), None); | ||
| assert_eq!( | ||
| worker_session_metadata(&test_agent_spec(Some(" "), None)), | ||
| None | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn refresh_fleet_inventory_session_ref_publishes_immediate_sync() { | ||
| let (tx, mut rx) = mpsc::channel(4); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.