@team-agent/installer 0.5.113 → 0.5.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +28 -15
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +55 -20
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/mod.rs +152 -8
  10. package/crates/team-agent/src/cli/models.rs +68 -18
  11. package/crates/team-agent/src/cli/spec.rs +7 -1
  12. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  13. package/crates/team-agent/src/cli/tests/base.rs +88 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
  15. package/crates/team-agent/src/compiler.rs +66 -18
  16. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  17. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  18. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  19. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  20. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  21. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  22. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  23. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  24. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  25. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  26. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  27. package/crates/team-agent/src/leader/mod.rs +1 -0
  28. package/crates/team-agent/src/leader/prompt.rs +906 -0
  29. package/crates/team-agent/src/leader/start.rs +260 -16
  30. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  31. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  32. package/crates/team-agent/src/leader/types.rs +6 -0
  33. package/crates/team-agent/src/lib.rs +4 -0
  34. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
  35. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  36. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  38. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  39. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  40. package/crates/team-agent/src/main.rs +6 -0
  41. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  42. package/crates/team-agent/src/messaging/send.rs +10 -0
  43. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  44. package/crates/team-agent/src/model/spec.rs +24 -12
  45. package/crates/team-agent/src/state/persist.rs +73 -0
  46. package/package.json +4 -4
@@ -45,7 +45,7 @@ use super::*;
45
45
  /// params:
46
46
  /// spec_path: spec 路径,team 目录优先取 state 里的 team_dir
47
47
  /// session_name: 目标 tmux session
48
- /// returns: 已起席位清单,含目标 pane、启动模式、布局与显示信息;spawn 后 pane 已死的席位被跳过
48
+ /// returns: legacy 启动收据与独立 contract 已提交席位 ID;不为私有 socket 伪造 legacy pane
49
49
  /// errors: 命令拼装或 transport spawn 失败时返回 LifecycleError
50
50
  /// ---
51
51
  pub(super) fn spawn_agents(
@@ -54,7 +54,7 @@ pub(super) fn spawn_agents(
54
54
  spec: &Value,
55
55
  session_name: &SessionName,
56
56
  transport: &dyn Transport,
57
- ) -> Result<Vec<StartedAgent>, LifecycleError> {
57
+ ) -> Result<(Vec<StartedAgent>, Vec<AgentId>), LifecycleError> {
58
58
  // E5 解耦:team_dir(角色定义 + profiles 所在)≠ spec_path.parent()(spec 已迁出到 .team/runtime)。
59
59
  // 优先取 state.team_dir(角色目录),回落 spec_path.parent()(legacy 同目录布局)。
60
60
  let team_dir_buf = crate::state::persist::load_runtime_state(workspace)
@@ -74,6 +74,10 @@ pub(super) fn spawn_agents(
74
74
  Some(Value::Bool(true))
75
75
  );
76
76
  let mut started = Vec::new();
77
+ #[cfg(unix)]
78
+ let mut contract_started = Vec::new();
79
+ #[cfg(not(unix))]
80
+ let contract_started = Vec::new();
77
81
  for agent in spec_agent_values(spec) {
78
82
  let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
79
83
  continue;
@@ -81,6 +85,26 @@ pub(super) fn spawn_agents(
81
85
  if agent_is_paused(agent) {
82
86
  continue;
83
87
  }
88
+ #[cfg(unix)]
89
+ if agent
90
+ .get("provider")
91
+ .and_then(Value::as_str)
92
+ .is_some_and(|provider| {
93
+ crate::contract_runtime::registry::descriptor(provider).is_some()
94
+ })
95
+ {
96
+ let team = runtime_team_key_for_spec(spec_path, spec, session_name);
97
+ crate::contract_runtime::framework::start_role(
98
+ workspace,
99
+ team_dir,
100
+ &team,
101
+ agent_id_raw,
102
+ )?;
103
+ contract_started.push(AgentId::new(agent_id_raw));
104
+ // No legacy target is manufactured for the private native socket.
105
+ // persist_spawn_agent_state projects the actual K3 seat separately.
106
+ continue;
107
+ }
84
108
  let agent_id = AgentId::new(agent_id_raw);
85
109
  let provider = agent
86
110
  .get("provider")
@@ -99,11 +123,12 @@ pub(super) fn spawn_agents(
99
123
  // has both the role instruction AND the callable Team Agent MCP capability.
100
124
  // probe5 RED proved that `build_command(.., None, None, ..)` left the worker
101
125
  // without `report_result`; placeholders are substituted at spawn time.
102
- let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
103
- agent,
104
- Some(agent_id_raw),
105
- provider,
106
- )?;
126
+ let command_agent =
127
+ crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
128
+ agent,
129
+ Some(agent_id_raw),
130
+ provider,
131
+ )?;
107
132
  let system_prompt =
108
133
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
109
134
  // 0.5.66 bypass 单源:per-agent safety 只服务 env 注入与审计,不参与 argv 决策。
@@ -431,7 +456,7 @@ pub(super) fn spawn_agents(
431
456
  managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
432
457
  });
433
458
  }
434
- Ok(started)
459
+ Ok((started, contract_started))
435
460
  }
436
461
 
437
462
  /// ---
@@ -451,7 +476,11 @@ pub(crate) fn agent_id_spec_source_path(workspace: &Path, agent_id: &str) -> Str
451
476
  });
452
477
  team_dir
453
478
  .map(|dir| dir.join("agents").join(format!("{agent_id}.md")))
454
- .unwrap_or_else(|| workspace.join(".team/current/agents").join(format!("{agent_id}.md")))
479
+ .unwrap_or_else(|| {
480
+ workspace
481
+ .join(".team/current/agents")
482
+ .join(format!("{agent_id}.md"))
483
+ })
455
484
  .display()
456
485
  .to_string()
457
486
  }
@@ -119,6 +119,23 @@ pub(super) fn persist_spawn_agent_state(
119
119
  let Some(id) = agent.get("id").and_then(Value::as_str) else {
120
120
  continue;
121
121
  };
122
+ #[cfg(unix)]
123
+ if agent
124
+ .get("provider")
125
+ .and_then(Value::as_str)
126
+ .is_some_and(|provider| {
127
+ crate::contract_runtime::registry::descriptor(provider).is_some()
128
+ })
129
+ {
130
+ let projected = if agent_is_paused(agent) {
131
+ serde_json::json!({"agent_id":id,"provider":"kiro","runtime_family":"contract","status":"paused","pane_id":null})
132
+ } else {
133
+ crate::contract_runtime::framework::project_seat(workspace, &team_id, id)
134
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?
135
+ };
136
+ agents.insert(id.to_string(), projected);
137
+ continue;
138
+ }
122
139
  let provider = agent
123
140
  .get("provider")
124
141
  .and_then(Value::as_str)
@@ -133,7 +133,11 @@ pub fn launch_with_transport_in_workspace(
133
133
  // 所有角色都未声明 dangerously_skip_permissions=true → 写 warning 事件不阻塞。
134
134
  // TODO(0.6.0): remove(0.6.0 彻底删源头 A 后此警示无意义)。
135
135
  emit_dangerous_flag_no_role_declared_warning(workspace, &spec);
136
- phase_timer.emit(workspace, "launch.phase", super::restart::LifecyclePhase::CompileSpec);
136
+ phase_timer.emit(
137
+ workspace,
138
+ "launch.phase",
139
+ super::restart::LifecyclePhase::CompileSpec,
140
+ );
137
141
  let session_name = spec_session_name(&spec);
138
142
  if !dry_run && transport_has_session(transport, &session_name) {
139
143
  return Err(LifecycleError::SessionConflict(format!(
@@ -142,11 +146,16 @@ pub fn launch_with_transport_in_workspace(
142
146
  )));
143
147
  }
144
148
  let routes = spec_routes(&spec);
145
- let started = if dry_run {
146
- Vec::new()
149
+ let (started, contract_started) = if dry_run {
150
+ (Vec::new(), Vec::new())
147
151
  } else {
148
- phase_timer.emit(workspace, "launch.phase", super::restart::LifecyclePhase::SpawnAll);
149
- let started = spawn_agents(workspace, spec_path, &spec, &session_name, transport)?;
152
+ phase_timer.emit(
153
+ workspace,
154
+ "launch.phase",
155
+ super::restart::LifecyclePhase::SpawnAll,
156
+ );
157
+ let (started, contract_started) =
158
+ spawn_agents(workspace, spec_path, &spec, &session_name, transport)?;
150
159
  persist_spawn_agent_state(
151
160
  workspace,
152
161
  spec_path,
@@ -164,14 +173,16 @@ pub fn launch_with_transport_in_workspace(
164
173
  .first()
165
174
  .map(|agent| agent.spawned_at.as_str())
166
175
  .unwrap_or(session_name.as_str());
167
- transport
168
- .set_session_owner_with_generation(
169
- &session_name,
170
- workspace,
171
- &owner_team,
172
- generation,
173
- )
174
- .map_err(|error| LifecycleError::Transport(error.to_string()))?;
176
+ if !started.is_empty() {
177
+ transport
178
+ .set_session_owner_with_generation(
179
+ &session_name,
180
+ workspace,
181
+ &owner_team,
182
+ generation,
183
+ )
184
+ .map_err(|error| LifecycleError::Transport(error.to_string()))?;
185
+ }
175
186
  // 0.5.38: per-worker timing tags (source="launch") so operators can
176
187
  // trace which worker's spawn dominates wall time. Zeros for now on
177
188
  // the sub-timings — Step 1 first enables shape assertion; a later
@@ -208,7 +219,7 @@ pub fn launch_with_transport_in_workspace(
208
219
  "launch",
209
220
  );
210
221
  }
211
- started
222
+ (started, contract_started)
212
223
  };
213
224
  // 0.3.28 Step 1: topology invariant guard (warn-only during migration).
214
225
  // Logs each violation to stderr; never panics. Promoted to hard error at
@@ -223,6 +234,7 @@ pub fn launch_with_transport_in_workspace(
223
234
  Ok(LaunchReport {
224
235
  session_name,
225
236
  started,
237
+ contract_started,
226
238
  dry_run,
227
239
  tmux_endpoint: transport.tmux_endpoint(),
228
240
  routes,
@@ -348,13 +360,13 @@ pub use cursor_mcp_iso::{
348
360
  materialize_cursor_mcp_project,
349
361
  };
350
362
  mod cursor_mcp;
363
+ pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
351
364
  pub use cursor_mcp::{
352
365
  apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
353
366
  apply_cursor_workspace_physical_path, cursor_mcp_enable_argv, cursor_mcp_enable_working_dir,
354
367
  enable_cursor_workspace_mcp, enable_cursor_workspace_mcp_with_profile, physical_workspace_path,
355
368
  prepare_cursor_seat_mcp, refuse_second_cursor_occupant,
356
369
  };
357
- pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
358
370
 
359
371
  mod cursor_create_chat;
360
372
 
@@ -363,9 +375,9 @@ pub(super) use worker_env::*;
363
375
  pub(crate) use worker_env::{
364
376
  apply_copilot_instructions_overlay, apply_cursor_agent_rules_overlay,
365
377
  apply_cursor_subscription_proxy_env_with_profile, apply_mcp_auto_approval_env,
366
- apply_profile_launch_env, auth_mode_env_value,
367
- fill_spawn_placeholders, fill_spawn_placeholders_full, inherited_env_with_team_overrides,
368
- persist_command_plan_state, spawn_timestamp,
378
+ apply_profile_launch_env, auth_mode_env_value, fill_spawn_placeholders,
379
+ fill_spawn_placeholders_full, inherited_env_with_team_overrides, persist_command_plan_state,
380
+ spawn_timestamp,
369
381
  };
370
382
  pub use worker_env::{apply_cursor_subscription_proxy_env, CursorProxyPresence};
371
383
 
@@ -393,18 +405,18 @@ pub(crate) use quick_start_transport::{
393
405
  };
394
406
 
395
407
  pub mod readiness;
408
+ pub(super) use readiness::*;
396
409
  pub use readiness::{
397
410
  classify_leader_binding, launched_team_receiver_is_attached, selected_team_leader_receiver,
398
411
  LeaderBindingClass,
399
412
  };
400
- pub(super) use readiness::*;
401
413
 
402
414
  mod add_agent;
403
415
  pub(super) use add_agent::*;
404
416
  pub use add_agent::{add_agent, add_agent_force};
405
417
  pub(crate) use add_agent::{
406
- add_agent_with_transport, add_agent_with_transport_force,
407
- add_agent_with_transport_at_paths_locked, force_recreate_with_transport_locked,
418
+ add_agent_with_transport, add_agent_with_transport_at_paths_locked,
419
+ add_agent_with_transport_force, force_recreate_with_transport_locked,
408
420
  };
409
421
 
410
422
  pub(crate) mod add_agent_state;
@@ -428,21 +440,20 @@ pub(super) use ownership::*;
428
440
  pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state, state_path};
429
441
 
430
442
  pub mod spec_state;
431
- pub use spec_state::{
432
- seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
433
- };
434
443
  pub(crate) use spec_state::{
435
444
  effective_runtime_config_for_worker_spawn, effective_runtime_config_for_worker_spawn_json,
436
445
  };
437
446
  use spec_state::{
438
- env_nonempty, has_positive_caller_leader_env, initial_runtime_state,
439
- override_spec_runtime_str, seed_launched_owner_from_env,
440
- spec_agent_values, spec_agents, spec_default_assignee, spec_routes, spec_session_name,
441
- spec_tasks_json, team_workspace, yaml_value_to_json,
447
+ env_nonempty, has_positive_caller_leader_env, initial_runtime_state, override_spec_runtime_str,
448
+ seed_launched_owner_from_env, spec_agent_values, spec_agents, spec_default_assignee,
449
+ spec_routes, spec_session_name, spec_tasks_json, team_workspace, yaml_value_to_json,
442
450
  };
443
451
  pub(crate) use spec_state::{
444
452
  override_spec_session_name, override_spec_workspace, spec_agent_id_set, write_spec_atomic,
445
453
  };
454
+ pub use spec_state::{
455
+ seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
456
+ };
446
457
 
447
458
  #[cfg(test)]
448
459
  mod tests {
@@ -433,8 +433,11 @@ pub enum LifecycleError {
433
433
  #[derive(Debug, Clone, PartialEq, Eq)]
434
434
  pub struct LaunchReport {
435
435
  pub session_name: SessionName,
436
- /// 实际起的 worker(冷启;dry-run 时空)。
436
+ /// Legacy transport starts; their targets belong to the team transport.
437
437
  pub started: Vec<StartedAgent>,
438
+ /// Committed contract starts, without fabricating legacy pane/window targets.
439
+ /// Empty on dry-run; populated only after the contract startup returns success.
440
+ pub contract_started: Vec<AgentId>,
438
441
  /// 是否 dry-run(只解析路由/权限,不起进程)。
439
442
  pub dry_run: bool,
440
443
  /// Selected tmux endpoint for the team transport when known.
@@ -63,16 +63,16 @@ pub(crate) struct WorkerCommandAgent {
63
63
  }
64
64
 
65
65
  impl WorkerCommandAgent {
66
- /// ---
67
- /// purpose: 从 spec 的 YAML agent 节点读出命令上下文
68
- /// params:
69
- /// agent: 单个 agent 的 YAML 节点
70
- /// fallback_id: agent 节点没写 id 时的兜底 id
71
- /// provider: 已解析的 provider
72
- /// returns: 填好的 WorkerCommandAgent
73
- /// errors: communication_mode 取值非法时返回 LifecycleError
74
- /// contract_id: lifecycle.worker_command_agent.from_source
75
- /// ---
66
+ /// ---
67
+ /// purpose: 从 spec 的 YAML agent 节点读出命令上下文
68
+ /// params:
69
+ /// agent: 单个 agent 的 YAML 节点
70
+ /// fallback_id: agent 节点没写 id 时的兜底 id
71
+ /// provider: 已解析的 provider
72
+ /// returns: 填好的 WorkerCommandAgent
73
+ /// errors: communication_mode 取值非法时返回 LifecycleError
74
+ /// contract_id: lifecycle.worker_command_agent.from_source
75
+ /// ---
76
76
  pub(crate) fn from_yaml(
77
77
  agent: &crate::model::yaml::Value,
78
78
  fallback_id: Option<&str>,
@@ -117,16 +117,16 @@ impl WorkerCommandAgent {
117
117
  })
118
118
  }
119
119
 
120
- /// ---
121
- /// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
122
- /// params:
123
- /// agent: 单个 agent 的 JSON 节点
124
- /// fallback_id: agent 节点没写 id 时的兜底 id
125
- /// provider: 已解析的 provider
126
- /// returns: 填好的 WorkerCommandAgent
127
- /// errors: communication_mode 取值非法时返回 LifecycleError
128
- /// contract_id: lifecycle.worker_command_agent.from_source
129
- /// ---
120
+ /// ---
121
+ /// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
122
+ /// params:
123
+ /// agent: 单个 agent 的 JSON 节点
124
+ /// fallback_id: agent 节点没写 id 时的兜底 id
125
+ /// provider: 已解析的 provider
126
+ /// returns: 填好的 WorkerCommandAgent
127
+ /// errors: communication_mode 取值非法时返回 LifecycleError
128
+ /// contract_id: lifecycle.worker_command_agent.from_source
129
+ /// ---
130
130
  pub(crate) fn from_json(
131
131
  agent: &serde_json::Value,
132
132
  fallback_id: Option<&str>,
@@ -190,7 +190,8 @@ pub(crate) fn compile_worker_system_prompt(
190
190
  mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status")
191
191
  );
192
192
  let report_result = mcp_tool_name(agent.provider, "team_orchestrator", "report_result");
193
- let runtime_contract = runtime_contract_section(&send_message, &report_result, &get_team_status);
193
+ let runtime_contract =
194
+ runtime_contract_section(&send_message, &report_result, &get_team_status);
194
195
  let communication_contract = agent.communication_mode.runtime_contract(&send_message);
195
196
  let mut chunks = vec![
196
197
  identity_section(agent),
@@ -235,7 +236,7 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
235
236
  }
236
237
  }
237
238
 
238
- fn runtime_contract_section(
239
+ pub(crate) fn runtime_contract_section(
239
240
  send_message: &str,
240
241
  report_result: &str,
241
242
  get_team_status: &str,
@@ -255,10 +256,15 @@ fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, Lifecycl
255
256
  }
256
257
 
257
258
  fn identity_section(agent: &WorkerCommandAgent) -> String {
258
- format!(
259
- "You are Team Agent worker `{}` with role `{}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity.",
259
+ worker_identity_section(
260
260
  agent.id.as_deref().unwrap_or("unknown"),
261
- agent.role.as_deref().unwrap_or("developer")
261
+ agent.role.as_deref().unwrap_or("developer"),
262
+ )
263
+ }
264
+
265
+ pub(crate) fn worker_identity_section(id: &str, role: &str) -> String {
266
+ format!(
267
+ "You are Team Agent worker `{id}` with role `{role}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity."
262
268
  )
263
269
  }
264
270
 
@@ -344,7 +350,9 @@ mod tests {
344
350
  "Do not reply to pure ACKs, greetings, or unchanged status notices (such as \"paused\" or \"waiting\"); after reporting a blocker once or completing a task, remain silent until a new actionable instruction arrives."
345
351
  ));
346
352
  assert!(!prompt.contains("Silence and resumption (mandatory)"));
347
- assert!(!prompt.contains("When you receive a message from the leader or a teammate, you MUST respond"));
353
+ assert!(!prompt.contains(
354
+ "When you receive a message from the leader or a teammate, you MUST respond"
355
+ ));
348
356
  match mode {
349
357
  CommunicationMode::LeaderCentric => {
350
358
  assert!(prompt.contains(
@@ -352,7 +360,9 @@ mod tests {
352
360
  ));
353
361
  }
354
362
  CommunicationMode::Orchestrated => {
355
- assert!(prompt.contains("Respond to task-related messages through Team Agent MCP tools."));
363
+ assert!(prompt.contains(
364
+ "Respond to task-related messages through Team Agent MCP tools."
365
+ ));
356
366
  assert!(prompt.contains("A pure ACK, unrelated status, or non-task message does not require a response."));
357
367
  }
358
368
  }
@@ -5,6 +5,12 @@ fn main() -> anyhow::Result<()> {
5
5
  .skip(1)
6
6
  .map(|arg| arg.to_string_lossy().into_owned());
7
7
  let command = args.next();
8
+ #[cfg(unix)]
9
+ if matches!(command.as_deref(), Some("contract-mcp")) {
10
+ team_agent::contract_runtime::mcp::run(&args.collect::<Vec<_>>())
11
+ .map_err(|error| anyhow::anyhow!(error.to_string()))?;
12
+ return Ok(());
13
+ }
8
14
  if matches!(command.as_deref(), Some("fake-worker")) {
9
15
  let mut workspace = None;
10
16
  let mut agent_id = None;
@@ -1926,7 +1926,11 @@ fn delivery_transport_for_recipient<'a>(
1926
1926
  }
1927
1927
  return DeliveryTransport::Borrowed(product_transport);
1928
1928
  };
1929
- if socket == crate::tmux_backend::socket_name_for_workspace(workspace) {
1929
+ // An exact endpoint match can reuse the supplied channel; do not select
1930
+ // another server merely because it has a pane with the same numeric id.
1931
+ if product_transport.tmux_endpoint().as_deref() == Some(socket)
1932
+ || socket == crate::tmux_backend::socket_name_for_workspace(workspace)
1933
+ {
1930
1934
  DeliveryTransport::Borrowed(product_transport)
1931
1935
  } else {
1932
1936
  // 0.5.4 gate054 cross-team leak fix: when `leader_receiver.tmux_socket`
@@ -2523,6 +2527,51 @@ pub(crate) fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> b
2523
2527
  }
2524
2528
 
2525
2529
  fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
2530
+ if let Some(receiver) = leader_receiver_value(state).filter(|receiver| {
2531
+ receiver.get("provider").and_then(serde_json::Value::as_str) == Some("pi")
2532
+ && receiver.get("native_session").is_some()
2533
+ }) {
2534
+ let native = receiver.get("native_session")?;
2535
+ for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
2536
+ let bound = native.get(key)?;
2537
+ if bound.is_null() || receiver.get(key) != Some(bound) {
2538
+ return None;
2539
+ }
2540
+ }
2541
+ // Managed Pi's native ID is distinct from leader_session_uuid.
2542
+ // The JSONL appears asynchronously. Resolve only inside the recorded
2543
+ // root, by exact id/cwd/spawn time; ambiguity or an unknown
2544
+ // NativeDefault root stays unavailable (no HOME scan).
2545
+ let context = crate::provider::CaptureSessionContext {
2546
+ agent_id: "leader".into(),
2547
+ spawn_cwd: native.get("spawn_cwd")?.as_str()?.into(),
2548
+ pane_id: None,
2549
+ pane_pid: None,
2550
+ spawned_at: Some(native.get("spawned_at")?.as_str()?.into()),
2551
+ expected_session_id: Some(crate::provider::SessionId::new(
2552
+ native.get("expected_session_id")?.as_str()?,
2553
+ )),
2554
+ provider_projects_root: Some(native.get("provider_projects_root")?.as_str()?.into()),
2555
+ };
2556
+ if !context.spawn_cwd.is_absolute()
2557
+ || !context.provider_projects_root.as_ref()?.is_absolute()
2558
+ {
2559
+ return None;
2560
+ }
2561
+ let candidates = crate::provider::session_scan::scan_session_candidates_once(
2562
+ Provider::Pi,
2563
+ &context,
2564
+ )
2565
+ .ok()?;
2566
+ return match candidates.as_slice() {
2567
+ [candidate] => candidate
2568
+ .captured
2569
+ .rollout_path
2570
+ .as_ref()
2571
+ .map(|path| path.as_path().to_path_buf()),
2572
+ _ => None,
2573
+ };
2574
+ }
2526
2575
  state
2527
2576
  .get("leader")
2528
2577
  .and_then(|leader| leader.get("rollout_path"))
@@ -265,6 +265,16 @@ pub fn send_message(
265
265
  }
266
266
  }
267
267
  }
268
+ #[cfg(unix)]
269
+ if state
270
+ .get("agents")
271
+ .and_then(|agents| agents.get(recipient))
272
+ .is_some_and(crate::contract_runtime::framework::is_contract)
273
+ {
274
+ return crate::contract_runtime::framework::enqueue(
275
+ workspace, &state, recipient, content, opts,
276
+ );
277
+ }
268
278
  let stale_worker_target_missing =
269
279
  stale_worker_target_missing_preflight(workspace, &state, recipient)?;
270
280
  let coordinator_unavailable = if stale_worker_target_missing {
@@ -77,6 +77,131 @@ fn tmux_leader_submit_without_provider_receipt_stays_pending_then_accepts_same_r
77
77
  assert!(case.delivery_consumed_at(&message_id).is_some());
78
78
  }
79
79
 
80
+ fn pi_receipt_case(tag: &str) -> (Case, serde_json::Value) {
81
+ let mut case = Case::new_without_rollout(tag);
82
+ let root = case.workspace.join("pi-sessions");
83
+ std::fs::create_dir_all(&root).unwrap();
84
+ case.rollout = root.join("not-derived-from-the-session-id.jsonl");
85
+ let receiver = &mut case.state["leader_receiver"];
86
+ receiver["provider"] = serde_json::json!("pi");
87
+ receiver["tmux_socket"] = serde_json::json!(case.workspace.join("fixture-tmux.sock"));
88
+ receiver["leader_session_uuid"] = serde_json::json!("team-owner-not-native-id");
89
+ receiver["owner_epoch"] = serde_json::json!(1);
90
+ receiver["native_session"] = serde_json::json!({
91
+ "expected_session_id": "native-pi-session",
92
+ "provider_projects_root": root,
93
+ "spawn_cwd": case.workspace,
94
+ "spawned_at": "2026-10-10T03:59:07Z",
95
+ "pane_id": "%leader",
96
+ "tmux_socket": receiver["tmux_socket"],
97
+ "leader_session_uuid": "team-owner-not-native-id",
98
+ "owner_epoch": 1,
99
+ });
100
+ crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
101
+ let header = serde_json::json!({
102
+ "type": "session", "version": 3, "id": "native-pi-session",
103
+ "cwd": case.workspace, "timestamp": "2026-10-10T03:59:08Z",
104
+ });
105
+ (case, header)
106
+ }
107
+
108
+ #[test]
109
+ fn pi_leader_receipt_binds_exact_header_and_consumes_without_reinjection() {
110
+ let (case, header) = pi_receipt_case("pi-native-receipt");
111
+ let endpoint = case.state["leader_receiver"]["tmux_socket"].as_str().unwrap();
112
+ assert!(Path::new(endpoint).is_absolute());
113
+ let unbound = crate::messaging::resolve_live_leader_channel(
114
+ &case.workspace,
115
+ &case.state["leader_receiver"],
116
+ &RecordingTransport::new(""),
117
+ );
118
+ assert!(
119
+ matches!(
120
+ unbound,
121
+ crate::messaging::LeaderChannelResolution::Unbound(
122
+ crate::messaging::LeaderChannelUnbound::EndpointMismatch
123
+ )
124
+ ),
125
+ "{unbound:?}"
126
+ );
127
+ let transport = RecordingTransport {
128
+ endpoint: Some(endpoint.to_string()),
129
+ ..RecordingTransport::new("")
130
+ };
131
+ let message_id = case.seed_message("Pi receipt canary");
132
+ std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
133
+ let first = deliver_pending_message(
134
+ &case.workspace,
135
+ &case.store,
136
+ &transport,
137
+ &message_id,
138
+ &case.event_log,
139
+ &case.state,
140
+ )
141
+ .unwrap();
142
+ assert!(first.ok, "{first:?}");
143
+ assert_eq!(
144
+ case.message_status(&message_id),
145
+ "submitted_pending_acceptance"
146
+ );
147
+ assert_eq!(transport.inject_count(), 1);
148
+
149
+ let received = serde_json::json!({
150
+ "type": "message", "message": {"role": "user", "content": [
151
+ {"type": "text", "text": format!("5535 [team-agent-token:{message_id}]")}
152
+ ]}
153
+ });
154
+ std::fs::write(&case.rollout, format!("{header}\n{received}\n")).unwrap();
155
+ let second = deliver_pending_message(
156
+ &case.workspace,
157
+ &case.store,
158
+ &transport,
159
+ &message_id,
160
+ &case.event_log,
161
+ &case.state,
162
+ )
163
+ .unwrap();
164
+ assert!(second.ok, "{second:?}");
165
+ assert_eq!(case.message_status(&message_id), "delivered");
166
+ assert!(case.delivery_consumed_at(&message_id).is_some());
167
+ assert_eq!(transport.inject_count(), 1);
168
+ }
169
+
170
+ #[test]
171
+ fn pi_leader_receipt_refuses_missing_ambiguous_stale_and_other_owner_backing() {
172
+ use crate::messaging::delivery::{observe_leader_receipt, LeaderReceiptObservation::*};
173
+ let (mut case, header) = pi_receipt_case("pi-native-receipt-fences");
174
+ let old = case.workspace.join("old-leader.jsonl");
175
+ std::fs::write(&old, "[team-agent-token:canary]").unwrap();
176
+ case.state["leader"] = serde_json::json!({"rollout_path": old});
177
+ // An old root leader receipt cannot substitute for this native session.
178
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
179
+ for (field, value) in [
180
+ ("id", serde_json::json!("another-session")),
181
+ ("cwd", serde_json::json!(case.workspace.join("other"))),
182
+ ("timestamp", serde_json::json!("2026-10-10T03:59:06Z")),
183
+ ] {
184
+ let mut wrong = header.clone();
185
+ wrong[field] = value;
186
+ std::fs::write(&case.rollout, format!("{wrong}\n[team-agent-token:canary]\n")).unwrap();
187
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
188
+ }
189
+ std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
190
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), TokenAbsent);
191
+ let duplicate = case.rollout.with_file_name("duplicate.jsonl");
192
+ std::fs::write(&duplicate, format!("{header}\n[team-agent-token:canary]\n")).unwrap();
193
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
194
+ std::fs::remove_file(duplicate).unwrap();
195
+ for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
196
+ let mut moved = case.state.clone();
197
+ moved["leader_receiver"][key] = serde_json::json!("changed");
198
+ assert_eq!(observe_leader_receipt(&moved, "canary"), SourceUnavailable);
199
+ }
200
+ case.state["leader_receiver"]["native_session"]["provider_projects_root"] =
201
+ serde_json::Value::Null;
202
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
203
+ }
204
+
80
205
  #[test]
81
206
  fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
82
207
  let case = Case::new("mailbox");
@@ -428,6 +553,7 @@ impl Case {
428
553
  struct RecordingTransport {
429
554
  injects: Arc<AtomicUsize>,
430
555
  capture: Arc<Mutex<String>>,
556
+ endpoint: Option<String>,
431
557
  }
432
558
 
433
559
  impl RecordingTransport {
@@ -435,6 +561,7 @@ impl RecordingTransport {
435
561
  Self {
436
562
  injects: Arc::new(AtomicUsize::new(0)),
437
563
  capture: Arc::new(Mutex::new(capture.to_string())),
564
+ endpoint: None,
438
565
  }
439
566
  }
440
567
 
@@ -444,6 +571,10 @@ impl RecordingTransport {
444
571
  }
445
572
 
446
573
  impl Transport for RecordingTransport {
574
+ fn tmux_endpoint(&self) -> Option<String> {
575
+ self.endpoint.clone()
576
+ }
577
+
447
578
  fn kind(&self) -> BackendKind {
448
579
  BackendKind::Tmux
449
580
  }