@team-agent/installer 0.5.60 → 0.5.62

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 (265) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +49 -3
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +184 -0
  6. package/crates/team-agent/src/cli/emit.rs +104 -21
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +14 -2
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +296 -8
  11. package/crates/team-agent/src/cli/named_address.rs +26 -2
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +3 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +5 -1
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  25. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  27. package/crates/team-agent/src/cli/types.rs +36 -2
  28. package/crates/team-agent/src/communication_mode/mod.rs +54 -0
  29. package/crates/team-agent/src/compiler/tests.rs +5 -5
  30. package/crates/team-agent/src/compiler.rs +54 -1
  31. package/crates/team-agent/src/conpty/backend.rs +1 -0
  32. package/crates/team-agent/src/conpty/mod.rs +1 -0
  33. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  34. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  35. package/crates/team-agent/src/coordinator/health.rs +1 -0
  36. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  37. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  38. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  39. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  41. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  44. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  45. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  46. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  47. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  48. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  49. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  50. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  51. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  52. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  53. package/crates/team-agent/src/coordinator/types.rs +3 -176
  54. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  55. package/crates/team-agent/src/db/message_store.rs +47 -2
  56. package/crates/team-agent/src/db/migration.rs +5 -4
  57. package/crates/team-agent/src/db/mod.rs +1 -0
  58. package/crates/team-agent/src/db/schema.rs +1 -0
  59. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  60. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  61. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  62. package/crates/team-agent/src/fake_worker.rs +21 -1
  63. package/crates/team-agent/src/layout/manager.rs +3 -7
  64. package/crates/team-agent/src/layout/mod.rs +2 -2
  65. package/crates/team-agent/src/layout/overlay.rs +6 -1
  66. package/crates/team-agent/src/layout/placement.rs +1 -0
  67. package/crates/team-agent/src/layout/recovery.rs +3 -0
  68. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  69. package/crates/team-agent/src/layout/sessions.rs +8 -1
  70. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  71. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  72. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  73. package/crates/team-agent/src/leader/helpers.rs +1 -0
  74. package/crates/team-agent/src/leader/incident.rs +1 -0
  75. package/crates/team-agent/src/leader/lease.rs +27 -22
  76. package/crates/team-agent/src/leader/mod.rs +2 -3
  77. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  78. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  79. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  80. package/crates/team-agent/src/leader/registry.rs +3 -15
  81. package/crates/team-agent/src/leader/start.rs +1072 -186
  82. package/crates/team-agent/src/leader/takeover.rs +10 -232
  83. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  84. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  85. package/crates/team-agent/src/leader/tests/identity.rs +71 -59
  86. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  87. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  88. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  89. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  90. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  91. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  92. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  93. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  94. package/crates/team-agent/src/leader/types.rs +1 -28
  95. package/crates/team-agent/src/lib.rs +7 -0
  96. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  97. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  98. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  99. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  100. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  102. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  104. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  105. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  106. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -28
  107. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  108. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  109. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  110. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  111. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -4
  112. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  113. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  114. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  115. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  116. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  117. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  118. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  119. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  120. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  121. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  122. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  123. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  124. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  125. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  126. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  127. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  128. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  129. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  130. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  131. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  132. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  133. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  134. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  135. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  136. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  137. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  138. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  139. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  140. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  141. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  142. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  143. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  144. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  145. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  146. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  147. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  148. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  149. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  150. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  151. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  152. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  153. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  154. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  155. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  156. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  157. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  158. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  159. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  160. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  161. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  162. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  163. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  164. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  165. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  166. package/crates/team-agent/src/mcp_server/normalize.rs +5 -3
  167. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  168. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  169. package/crates/team-agent/src/mcp_server/tools.rs +101 -66
  170. package/crates/team-agent/src/mcp_server/types.rs +6 -18
  171. package/crates/team-agent/src/mcp_server/wire.rs +32 -26
  172. package/crates/team-agent/src/messaging/activity.rs +1 -0
  173. package/crates/team-agent/src/messaging/address.rs +1 -0
  174. package/crates/team-agent/src/messaging/delivery.rs +109 -41
  175. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  176. package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
  177. package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
  178. package/crates/team-agent/src/messaging/mod.rs +10 -14
  179. package/crates/team-agent/src/messaging/peers.rs +2 -0
  180. package/crates/team-agent/src/messaging/persist.rs +3 -1
  181. package/crates/team-agent/src/messaging/presentation.rs +111 -1
  182. package/crates/team-agent/src/messaging/results.rs +166 -17
  183. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  184. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  185. package/crates/team-agent/src/messaging/send.rs +135 -89
  186. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  187. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  188. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  189. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  190. package/crates/team-agent/src/messaging/types.rs +7 -2
  191. package/crates/team-agent/src/messaging/watchers.rs +24 -13
  192. package/crates/team-agent/src/model/enums.rs +1 -0
  193. package/crates/team-agent/src/model/errors.rs +1 -0
  194. package/crates/team-agent/src/model/ids.rs +1 -0
  195. package/crates/team-agent/src/model/mod.rs +2 -0
  196. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  197. package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -0
  198. package/crates/team-agent/src/model/paths.rs +1 -0
  199. package/crates/team-agent/src/model/permissions.rs +3 -2
  200. package/crates/team-agent/src/model/routing.rs +1 -0
  201. package/crates/team-agent/src/model/spec.rs +17 -0
  202. package/crates/team-agent/src/model/task_graph.rs +1 -0
  203. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  204. package/crates/team-agent/src/model/yaml.rs +1 -0
  205. package/crates/team-agent/src/packaging/install.rs +9 -147
  206. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  207. package/crates/team-agent/src/packaging/mod.rs +1 -0
  208. package/crates/team-agent/src/packaging/repair.rs +2 -0
  209. package/crates/team-agent/src/packaging/tests.rs +29 -218
  210. package/crates/team-agent/src/packaging/types.rs +8 -15
  211. package/crates/team-agent/src/platform/argv.rs +4 -0
  212. package/crates/team-agent/src/platform/errors.rs +10 -1
  213. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  214. package/crates/team-agent/src/platform/mod.rs +0 -61
  215. package/crates/team-agent/src/platform/process.rs +1 -0
  216. package/crates/team-agent/src/provider/adapter.rs +1 -0
  217. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  218. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  219. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  220. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  221. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  222. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  223. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  224. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  225. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  226. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  227. package/crates/team-agent/src/provider/classify.rs +1 -0
  228. package/crates/team-agent/src/provider/faults.rs +1 -26
  229. package/crates/team-agent/src/provider/helpers.rs +1 -0
  230. package/crates/team-agent/src/provider/mod.rs +1 -1
  231. package/crates/team-agent/src/provider/session/capture.rs +41 -22
  232. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  233. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  234. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  235. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  236. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  237. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  238. package/crates/team-agent/src/provider/session_scan/claude.rs +93 -3
  239. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  240. package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
  241. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  242. package/crates/team-agent/src/provider/session_scan.rs +6 -42
  243. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  244. package/crates/team-agent/src/provider/types.rs +1 -0
  245. package/crates/team-agent/src/provider/wire.rs +1 -0
  246. package/crates/team-agent/src/state/identity.rs +16 -5
  247. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  248. package/crates/team-agent/src/state/mod.rs +2 -0
  249. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  250. package/crates/team-agent/src/state/ownership.rs +10 -4
  251. package/crates/team-agent/src/state/paths.rs +12 -0
  252. package/crates/team-agent/src/state/persist.rs +9 -3
  253. package/crates/team-agent/src/state/projection.rs +15 -2
  254. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  255. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  256. package/crates/team-agent/src/state/repository.rs +7 -0
  257. package/crates/team-agent/src/state/selector.rs +1 -0
  258. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  259. package/crates/team-agent/src/tmux_backend.rs +63 -0
  260. package/crates/team-agent/src/topology.rs +3 -0
  261. package/crates/team-agent/src/transport.rs +15 -0
  262. package/package.json +4 -4
  263. package/skills/team-agent/command-coverage.json +379 -0
  264. package/crates/team-agent/src/leader/inject.rs +0 -33
  265. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -9,9 +9,10 @@ use crate::model::ids::{AgentId, TaskId, TeamKey};
9
9
  use crate::transport::{PaneId, Transport};
10
10
 
11
11
  use super::helpers::{status_wire, MessageStatusShadow};
12
- use super::leader_receiver::{send_to_leader_receiver, send_to_leader_receiver_with_presentation};
12
+ use super::leader_receiver::send_to_leader_receiver_with_presentation;
13
13
  use super::presentation::{
14
- decide_presentation, PresentationRequest, PresentationSink, PresentationSource,
14
+ decide_presentation, PresentationDecision, PresentationRequest, PresentationSink,
15
+ PresentationSource,
15
16
  };
16
17
  use super::{
17
18
  persist_resolved_send, DeliveryBlocker, DeliveryOutcome, DeliveryRefusal, DeliveryStatus,
@@ -35,21 +36,54 @@ pub enum MessageTarget {
35
36
  /// Public CLI/MCP inputs never construct this value from a caller-supplied
36
37
  /// string. The wrapper keeps that trust boundary visible throughout delivery
37
38
  /// instead of degrading the identity back to an untyped option field.
38
- #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
39
- #[serde(transparent)]
40
- pub struct TrustedSender(AgentId);
39
+ #[derive(Debug, Clone, PartialEq, Eq)]
40
+ pub struct TrustedSender {
41
+ agent_id: AgentId,
42
+ full_name: Option<String>,
43
+ }
41
44
 
42
45
  impl TrustedSender {
43
46
  pub fn from_runtime_identity(agent_id: AgentId) -> Self {
44
- Self(agent_id)
47
+ Self {
48
+ agent_id,
49
+ full_name: None,
50
+ }
51
+ }
52
+
53
+ pub fn from_runtime_identity_with_source(
54
+ agent_id: AgentId,
55
+ workspace: impl std::fmt::Display,
56
+ team: &str,
57
+ ) -> Self {
58
+ let full_name = format!("{}::{}/{}", workspace, team, agent_id.as_str());
59
+ Self {
60
+ agent_id,
61
+ full_name: Some(full_name),
62
+ }
45
63
  }
46
64
 
47
65
  pub fn leader() -> Self {
48
- Self(AgentId::new("leader"))
66
+ Self {
67
+ agent_id: AgentId::new("leader"),
68
+ full_name: None,
69
+ }
49
70
  }
50
71
 
51
72
  pub fn as_str(&self) -> &str {
52
- self.0.as_str()
73
+ self.agent_id.as_str()
74
+ }
75
+
76
+ pub fn display_name(&self) -> &str {
77
+ self.full_name.as_deref().unwrap_or_else(|| self.as_str())
78
+ }
79
+ }
80
+
81
+ impl serde::Serialize for TrustedSender {
82
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83
+ where
84
+ S: serde::Serializer,
85
+ {
86
+ serializer.serialize_str(self.display_name())
53
87
  }
54
88
  }
55
89
 
@@ -125,49 +159,14 @@ pub fn send_message(
125
159
  MessageTarget::Single(target) if target == "leader" => {
126
160
  let presentation = decide_presentation(&opts.presentation, PresentationSource::Send);
127
161
  if presentation.effective_sink != PresentationSink::Leader {
128
- let mut intent = ResolvedSendIntent::accepted(
129
- opts.origin,
162
+ return persist_stored_only_send(
130
163
  workspace,
131
- opts.team.clone(),
132
164
  LogicalRecipient::Leader,
133
- opts.sender.clone(),
134
- opts.task_id.clone(),
135
165
  content,
136
- None,
137
- false,
138
- opts.message_id.clone(),
166
+ opts,
167
+ &presentation,
168
+ &event_log,
139
169
  );
140
- intent.initial_disposition = InitialDisposition::StoredOnly;
141
- intent.presentation = presentation.clone();
142
- let persisted = match persist_resolved_send(&intent)? {
143
- PersistResolution::Persisted(persisted) => persisted,
144
- PersistResolution::Duplicate(message_id) => {
145
- return Ok(refused_outcome_with_id(
146
- DeliveryRefusal::Duplicate,
147
- Some(message_id),
148
- ));
149
- }
150
- };
151
- event_log.write(
152
- "presentation.stored_without_live_inject",
153
- serde_json::json!({
154
- "message_id": persisted.message_id,
155
- "requested_sink": presentation.requested_sink,
156
- "effective_sink": presentation.effective_sink,
157
- "class": presentation.class,
158
- "policy_reason": presentation.policy_reason,
159
- }),
160
- )?;
161
- return Ok(DeliveryOutcome {
162
- ok: true,
163
- status: DeliveryStatus::StoredOnly,
164
- message_status: MessageStatusShadow("stored_only".to_string()),
165
- message_id: Some(persisted.message_id),
166
- verification: Some("durable_without_live_inject".to_string()),
167
- stage: None,
168
- reason: None,
169
- channel: Some(presentation.effective_sink.as_str().to_string()),
170
- });
171
170
  }
172
171
  let outcome = send_to_leader_receiver_with_presentation(
173
172
  workspace,
@@ -175,7 +174,7 @@ pub fn send_message(
175
174
  "leader",
176
175
  content,
177
176
  opts.task_id.as_ref(),
178
- opts.sender.as_str(),
177
+ opts.sender.display_name(),
179
178
  opts.requires_ack,
180
179
  None,
181
180
  opts.message_id.as_deref(),
@@ -201,15 +200,7 @@ pub fn send_message(
201
200
  MessageTarget::Single(target) => target,
202
201
  MessageTarget::Broadcast => {
203
202
  let recipients = broadcast_recipients(&state, opts.sender.as_str(), opts.team.as_ref());
204
- return fanout_send(
205
- workspace,
206
- &state,
207
- &recipients,
208
- content,
209
- opts,
210
- &event_log,
211
- "*",
212
- );
203
+ return fanout_send(workspace, &recipients, content, opts, "*");
213
204
  }
214
205
  MessageTarget::Fanout(recipients) if recipients.is_empty() => {
215
206
  // swallow batch 3 ②: a failed send carries its reason (Python send error
@@ -226,9 +217,7 @@ pub fn send_message(
226
217
  });
227
218
  }
228
219
  MessageTarget::Fanout(recipients) => {
229
- return fanout_send(
230
- workspace, &state, recipients, content, opts, &event_log, "fanout",
231
- );
220
+ return fanout_send(workspace, recipients, content, opts, "fanout");
232
221
  }
233
222
  };
234
223
  // send.py:259-261 — a non-leader target that is NOT a known team agent is refused
@@ -240,6 +229,17 @@ pub fn send_message(
240
229
  if !in_team {
241
230
  return Ok(refused_outcome(DeliveryRefusal::TargetNotInTeam));
242
231
  }
232
+ let presentation = decide_presentation(&opts.presentation, PresentationSource::Send);
233
+ if presentation.effective_sink != PresentationSink::Leader {
234
+ return persist_stored_only_send(
235
+ workspace,
236
+ LogicalRecipient::from_resolved(recipient),
237
+ content,
238
+ opts,
239
+ &presentation,
240
+ &event_log,
241
+ );
242
+ }
243
243
  if let Some(outcome) = session_drift_refusal(
244
244
  &state,
245
245
  recipient,
@@ -354,6 +354,59 @@ pub fn send_message(
354
354
  })
355
355
  }
356
356
 
357
+ fn persist_stored_only_send(
358
+ workspace: &Path,
359
+ recipient: LogicalRecipient,
360
+ content: &str,
361
+ opts: &SendOptions,
362
+ presentation: &PresentationDecision,
363
+ event_log: &EventLog,
364
+ ) -> Result<DeliveryOutcome, MessagingError> {
365
+ let mut intent = ResolvedSendIntent::accepted(
366
+ opts.origin,
367
+ workspace,
368
+ opts.team.clone(),
369
+ recipient,
370
+ opts.sender.clone(),
371
+ opts.task_id.clone(),
372
+ content,
373
+ None,
374
+ opts.requires_ack,
375
+ opts.message_id.clone(),
376
+ );
377
+ intent.initial_disposition = InitialDisposition::StoredOnly;
378
+ intent.presentation = presentation.clone();
379
+ let persisted = match persist_resolved_send(&intent)? {
380
+ PersistResolution::Persisted(persisted) => persisted,
381
+ PersistResolution::Duplicate(message_id) => {
382
+ return Ok(refused_outcome_with_id(
383
+ DeliveryRefusal::Duplicate,
384
+ Some(message_id),
385
+ ));
386
+ }
387
+ };
388
+ event_log.write(
389
+ "presentation.stored_without_live_inject",
390
+ serde_json::json!({
391
+ "message_id": persisted.message_id,
392
+ "requested_sink": presentation.requested_sink,
393
+ "effective_sink": presentation.effective_sink,
394
+ "class": presentation.class,
395
+ "policy_reason": presentation.policy_reason,
396
+ }),
397
+ )?;
398
+ Ok(DeliveryOutcome {
399
+ ok: true,
400
+ status: DeliveryStatus::StoredOnly,
401
+ message_status: MessageStatusShadow("stored_only".to_string()),
402
+ message_id: Some(persisted.message_id),
403
+ verification: Some("durable_without_live_inject".to_string()),
404
+ stage: None,
405
+ reason: None,
406
+ channel: Some(presentation.effective_sink.as_str().to_string()),
407
+ })
408
+ }
409
+
357
410
  fn stale_worker_target_missing_preflight(
358
411
  workspace: &Path,
359
412
  state: &serde_json::Value,
@@ -722,7 +775,7 @@ pub fn apply_worker_sender_bypass(
722
775
 
723
776
  /// `session_drift_refusal` (`session_drift.py:69`):send 时检测会话漂移并拒绝。
724
777
  /// `None` = 无漂移 (放行);`Some` = 拒绝 [`DeliveryOutcome`] (reason=`SessionDrift`)。
725
- pub fn session_drift_refusal(
778
+ pub(crate) fn session_drift_refusal(
726
779
  state: &serde_json::Value,
727
780
  target: &str,
728
781
  leader_id: &str,
@@ -804,11 +857,9 @@ fn broadcast_recipients(
804
857
 
805
858
  fn fanout_send(
806
859
  workspace: &Path,
807
- state: &serde_json::Value,
808
860
  recipients: &[String],
809
861
  content: &str,
810
862
  opts: &SendOptions,
811
- event_log: &EventLog,
812
863
  channel_label: &str,
813
864
  ) -> Result<DeliveryOutcome, MessagingError> {
814
865
  let mut last_message_id: Option<String> = None;
@@ -821,30 +872,17 @@ fn fanout_send(
821
872
  continue;
822
873
  }
823
874
  attempted_count = attempted_count.saturating_add(1);
824
- let outcome = if recipient == "leader" {
825
- send_to_leader_receiver(
826
- workspace,
827
- state,
828
- recipient,
829
- content,
830
- opts.task_id.as_ref(),
831
- opts.sender.as_str(),
832
- opts.requires_ack,
833
- None,
834
- event_log,
835
- )?
836
- } else {
837
- // single-recipient re-entry — strip fanout metadata to avoid recursion + ensure
838
- // each row gets its own caller-supplied message_id (none) so SQLite PK doesn't clash.
839
- let mut inner_opts = opts.clone();
840
- inner_opts.message_id = None;
841
- super::send::send_message(
842
- workspace,
843
- &MessageTarget::Single(recipient.clone()),
844
- content,
845
- &inner_opts,
846
- )?
847
- };
875
+ // Every expanded recipient re-enters the same presentation-aware send
876
+ // funnel. Strip the caller idempotency key so each durable fanout row
877
+ // receives its own stable message id.
878
+ let mut inner_opts = opts.clone();
879
+ inner_opts.message_id = None;
880
+ let outcome = super::send::send_message(
881
+ workspace,
882
+ &MessageTarget::Single(recipient.clone()),
883
+ content,
884
+ &inner_opts,
885
+ )?;
848
886
  if outcome.ok {
849
887
  delivered_count = delivered_count.saturating_add(1);
850
888
  if let Some(mid) = outcome.message_id.clone() {
@@ -862,8 +900,12 @@ fn fanout_send(
862
900
  return Ok(outcome);
863
901
  }
864
902
  }
903
+ let presentation = decide_presentation(&opts.presentation, PresentationSource::Send);
904
+ let stored_only = presentation.effective_sink != PresentationSink::Leader;
865
905
  let status = if any_failure {
866
906
  DeliveryStatus::FanoutPartial
907
+ } else if delivered_count > 0 && stored_only {
908
+ DeliveryStatus::StoredOnly
867
909
  } else if delivered_count > 0 {
868
910
  DeliveryStatus::FanoutDelivered
869
911
  } else {
@@ -874,9 +916,13 @@ fn fanout_send(
874
916
  status,
875
917
  message_status: MessageStatusShadow(status_wire(status).to_string()),
876
918
  message_id: last_message_id,
877
- verification: None,
919
+ verification: stored_only.then(|| "durable_without_live_inject".to_string()),
878
920
  stage: None,
879
921
  reason: None,
880
- channel: Some(channel_label.to_string()),
922
+ channel: Some(if stored_only {
923
+ presentation.effective_sink.as_str().to_string()
924
+ } else {
925
+ channel_label.to_string()
926
+ }),
881
927
  })
882
928
  }
@@ -101,10 +101,10 @@ fn legacy_receiver_without_status_still_rejects_a_foreign_workspace() {
101
101
  .with_tmux_endpoint(socket)
102
102
  .with_targets(vec![observed]);
103
103
 
104
- assert_eq!(
104
+ assert!(matches!(
105
105
  resolve_live_leader_channel(workspace, &receiver, &transport),
106
- LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch)
107
- );
106
+ LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch(_))
107
+ ));
108
108
  }
109
109
 
110
110
  #[test]
@@ -162,8 +162,8 @@ fn same_socket_and_pane_in_a_foreign_workspace_is_not_a_live_leader() {
162
162
  .with_tmux_endpoint(socket)
163
163
  .with_targets(vec![observed]);
164
164
 
165
- assert_eq!(
165
+ assert!(matches!(
166
166
  resolve_live_leader_channel(workspace, &receiver(socket), &transport),
167
- LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch)
168
- );
167
+ LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch(_))
168
+ ));
169
169
  }
@@ -99,6 +99,141 @@ fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
99
99
  assert!(case.events().contains("leader_receiver.mailbox_requeued"));
100
100
  }
101
101
 
102
+ #[test]
103
+ fn leader_submit_without_receipt_source_cannot_park_forever_without_a_completion_writer() {
104
+ let case = Case::new_without_rollout("missing-receipt-source");
105
+ let transport = RecordingTransport::new("");
106
+ let message_id = case.seed_message("missing receipt source canary");
107
+
108
+ let first = deliver_pending_message(
109
+ &case.workspace,
110
+ &case.store,
111
+ &transport,
112
+ &message_id,
113
+ &case.event_log,
114
+ &case.state,
115
+ )
116
+ .expect("verified leader transport submission");
117
+
118
+ assert_ne!(
119
+ case.message_status(&message_id),
120
+ "submitted_pending_acceptance",
121
+ "F4 acceptance-stall: a verified leader transport submit with no reachable \
122
+ receipt source must not enter submitted_pending_acceptance, because that \
123
+ protected state has no completion writer. The runtime may close the \
124
+ transport-acceptance fact or expose a provider-session receipt writer, \
125
+ but it must not report an indefinitely retryable fact-terminal. outcome={first:?}"
126
+ );
127
+ assert_eq!(
128
+ transport.inject_count(),
129
+ 1,
130
+ "the acceptance repair must not obtain progress by physically injecting twice"
131
+ );
132
+
133
+ let _ = deliver_pending_message(
134
+ &case.workspace,
135
+ &case.store,
136
+ &transport,
137
+ &message_id,
138
+ &case.event_log,
139
+ &case.state,
140
+ );
141
+ assert_eq!(
142
+ transport.inject_count(),
143
+ 1,
144
+ "a second coordinator observation must not replay the already-submitted leader message"
145
+ );
146
+ }
147
+
148
+ #[test]
149
+ fn nonempty_missing_receipt_source_is_not_token_absent() {
150
+ assert_unavailable_rollout_converges(
151
+ "missing-rollout",
152
+ PathBuf::from("definitely-missing-leader-rollout.jsonl"),
153
+ false,
154
+ );
155
+ }
156
+
157
+ #[test]
158
+ fn nonempty_unreadable_receipt_source_is_not_token_absent() {
159
+ assert_unavailable_rollout_converges(
160
+ "unreadable-rollout",
161
+ PathBuf::from("leader-rollout-is-a-directory"),
162
+ true,
163
+ );
164
+ }
165
+
166
+ fn assert_unavailable_rollout_converges(tag: &str, rollout: PathBuf, directory: bool) {
167
+ let case = Case::new_with_unavailable_rollout(tag, rollout, directory);
168
+ let transport = RecordingTransport::new("");
169
+ let message_id = case.seed_message("unavailable receipt source canary");
170
+
171
+ let first = deliver_pending_message(
172
+ &case.workspace,
173
+ &case.store,
174
+ &transport,
175
+ &message_id,
176
+ &case.event_log,
177
+ &case.state,
178
+ )
179
+ .expect("verified leader transport submission");
180
+
181
+ assert_eq!(
182
+ case.message_status(&message_id),
183
+ "injected_awaiting_receipt",
184
+ "F4 receipt observation must distinguish an unreadable nonempty rollout_path \
185
+ from a readable rollout that lacks the token. The former is \
186
+ receipt-source-unavailable and must converge to the typed parked state, \
187
+ not remain submitted_pending_acceptance. shape={tag} outcome={first:?}"
188
+ );
189
+ assert_eq!(
190
+ first.verification.as_deref(),
191
+ Some("leader_receipt_source_unavailable"),
192
+ "the outcome must expose read-unavailable rather than TokenAbsent; shape={tag}"
193
+ );
194
+ assert_eq!(transport.inject_count(), 1);
195
+
196
+ let _ = deliver_pending_message(
197
+ &case.workspace,
198
+ &case.store,
199
+ &transport,
200
+ &message_id,
201
+ &case.event_log,
202
+ &case.state,
203
+ );
204
+ assert_eq!(
205
+ transport.inject_count(),
206
+ 1,
207
+ "read-unavailable convergence must not obtain progress by a second physical \
208
+ injection; shape={tag}"
209
+ );
210
+ }
211
+
212
+ #[test]
213
+ fn worker_consumption_writer_remains_a_positive_control() {
214
+ let case = Case::new("worker-positive-control");
215
+ let message_id = case
216
+ .store
217
+ .create_message(
218
+ None,
219
+ "leader",
220
+ "worker",
221
+ "worker consume canary",
222
+ None,
223
+ false,
224
+ None,
225
+ )
226
+ .unwrap();
227
+
228
+ case.store.mark(&message_id, "consumed", None).unwrap();
229
+
230
+ assert_eq!(
231
+ case.message_status(&message_id),
232
+ "consumed",
233
+ "positive control: the independent worker consumption writer must remain reachable"
234
+ );
235
+ }
236
+
102
237
  struct Case {
103
238
  workspace: PathBuf,
104
239
  rollout: PathBuf,
@@ -134,6 +269,58 @@ impl Case {
134
269
  }
135
270
  }
136
271
 
272
+ fn new_without_rollout(tag: &str) -> Self {
273
+ let workspace = tmp_ws(&format!("leader-inject-acceptance-{tag}"));
274
+ let rollout = workspace.join("absent-leader.jsonl");
275
+ let state = serde_json::json!({
276
+ "active_team_key": "acceptance-team",
277
+ "session_name": "team-acceptance",
278
+ "leader_receiver": {
279
+ "pane_id": "%leader",
280
+ "status": "attached",
281
+ "provider": "claude"
282
+ }
283
+ });
284
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
285
+ let store = MessageStore::open(&workspace).unwrap();
286
+ let event_log = EventLog::new(&workspace);
287
+ Self {
288
+ workspace,
289
+ rollout,
290
+ store,
291
+ event_log,
292
+ state,
293
+ }
294
+ }
295
+
296
+ fn new_with_unavailable_rollout(tag: &str, relative: PathBuf, directory: bool) -> Self {
297
+ let workspace = tmp_ws(&format!("leader-inject-acceptance-{tag}"));
298
+ let rollout = workspace.join(relative);
299
+ if directory {
300
+ std::fs::create_dir_all(&rollout).unwrap();
301
+ }
302
+ let state = serde_json::json!({
303
+ "active_team_key": "acceptance-team",
304
+ "session_name": "team-acceptance",
305
+ "leader_receiver": {
306
+ "pane_id": "%leader",
307
+ "status": "attached",
308
+ "provider": "claude",
309
+ "rollout_path": rollout,
310
+ }
311
+ });
312
+ crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
313
+ let store = MessageStore::open(&workspace).unwrap();
314
+ let event_log = EventLog::new(&workspace);
315
+ Self {
316
+ workspace,
317
+ rollout,
318
+ store,
319
+ event_log,
320
+ state,
321
+ }
322
+ }
323
+
137
324
  fn seed_message(&self, content: &str) -> String {
138
325
  self.store
139
326
  .create_message(None, "worker", "leader", content, None, false, None)
@@ -1,5 +1,10 @@
1
1
  #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2
2
  use super::*;
3
+ use super::delivery::tmux_pane_width;
4
+ use super::helpers::fail_leader_delivery;
5
+ use super::selftest::evaluate_idle_behavior;
6
+ use super::send::session_drift_refusal;
7
+ use super::watchers::{delivered_result_message, notify_result_watchers};
3
8
  use std::collections::BTreeMap;
4
9
  use std::path::{Path, PathBuf};
5
10
 
@@ -2976,9 +2976,8 @@ fn gate054_attached_status_still_accepts_when_pane_is_live() {
2976
2976
  // Injection-case revision: transport submit success is NOT a provider
2977
2977
  // receipt, so the old `delivered` pin was a false-positive lock
2978
2978
  // (MUST-10: physical submit is the framework's fact; acceptance is the
2979
- // provider's). The un-refused path now parks the SAME row in
2980
- // submitted_pending_acceptance until a provider receipt or retry
2981
- // schedule advances it.
2979
+ // provider's). The un-refused path now records the SAME row in the typed
2980
+ // post-injection fact state until a provider receipt advances it.
2982
2981
  let ws = tmp_ws("gate054ok");
2983
2982
  let store = store_for(&ws);
2984
2983
  let log = EventLog::new(&ws);
@@ -3012,10 +3011,18 @@ fn gate054_attached_status_still_accepts_when_pane_is_live() {
3012
3011
  1,
3013
3012
  "gate054: exactly one physical inject must have happened (MUST-10 anchor)"
3014
3013
  );
3014
+ let injected_awaiting_receipt = crate::db::message_store::MessageRowStatus::ALL
3015
+ .iter()
3016
+ .map(|status| status.as_str())
3017
+ .find(|status| *status == "injected_awaiting_receipt")
3018
+ .expect(
3019
+ "gate054 signed reanchor: typed status directory must expose \
3020
+ injected_awaiting_receipt",
3021
+ );
3015
3022
  assert_eq!(
3016
- out.message_status.0, "submitted_pending_acceptance",
3017
- "gate054 (injection-case revision): a successful physical submit without a provider \
3018
- receipt must park as submitted_pending_acceptance, never claim delivered: {out:?}"
3023
+ out.message_status.0, injected_awaiting_receipt,
3024
+ "gate054 signed reanchor: a successful physical inject without a provider receipt \
3025
+ must remain non-delivered as injected_awaiting_receipt, never claim delivered: {out:?}"
3019
3026
  );
3020
3027
  }
3021
3028
 
@@ -53,6 +53,11 @@ pub enum DeliveryRefusal {
53
53
  TmuxTargetMissing,
54
54
  MessageAlreadyClaimed,
55
55
  LeaderNotAttached,
56
+ /// The recorded leader pane is live but belongs to another workspace.
57
+ /// Keep this typed cause distinct from an actually unattached leader so
58
+ /// the send surface can return the existing recovery action.
59
+ #[serde(rename = "PaneWorkspaceMismatch")]
60
+ PaneWorkspaceMismatch,
56
61
  NoCallerPane,
57
62
  TeamOwnerMismatch,
58
63
  Ambiguous,
@@ -402,14 +407,14 @@ pub struct IdleEvaluation {
402
407
 
403
408
  /// 抑制快照 (card §50;`scheduler.py:383` `_agent_alert_snapshot`)。清除判据靠 snapshot diff。
404
409
  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405
- pub struct AlertSnapshot {
410
+ pub(crate) struct AlertSnapshot {
406
411
  pub assigned_task_ids: Vec<TaskId>,
407
412
  pub delivered_message_ids: Vec<String>,
408
413
  }
409
414
 
410
415
  /// 告警抑制条目 (card §50;`scheduler.py:287`)。
411
416
  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412
- pub struct AlertSuppression {
417
+ pub(crate) struct AlertSuppression {
413
418
  pub suppressed_at: String,
414
419
  pub suppressed_by: String,
415
420
  pub snapshot: AlertSnapshot,