@team-agent/installer 0.2.11 → 0.3.0

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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,209 @@
1
+ //! 私有内部 helpers (status wire / envelope 校验 / scheduled kind 解析 / activity 信号
2
+ //! 解析 / id 生成 / MessageStatusShadow 占位)。跨子模块/测试可见者升 `pub(crate)`。
3
+
4
+ use std::sync::atomic::{AtomicU64, Ordering};
5
+
6
+ use rusqlite::{params, OptionalExtension};
7
+
8
+ use crate::message_store::MessageStore;
9
+
10
+ use super::{
11
+ ActivityStatus, AgentActivity, DeliveryOutcome, DeliveryRefusal, DeliveryStatus,
12
+ MessagingError, ScheduledKind,
13
+ };
14
+
15
+ /// **PLACEHOLDER** — step 7 `messages.status` 行态 enum (`message_store` lane 的 `MessageStatus`)。
16
+ /// step 7 已落地 core 但尚未导出该 enum (当前用裸 `&str`);本 lane 不猜其精确 variant 集,
17
+ /// 用本地最小占位让 [`DeliveryOutcome::message_status`] 编得过。leader 集成时换成 step 7 的
18
+ /// 权威 `MessageStatus`(`accepted`/`target_resolved`/`injected`/`visible`/`submitted`/
19
+ /// `submitted_unverified`/`delivered`/`acknowledged`/`failed`/`queued_*` …)。
20
+ #[derive(Debug, Clone, PartialEq, Eq)]
21
+ pub struct MessageStatusShadow(pub String);
22
+
23
+ static RESULT_COUNTER: AtomicU64 = AtomicU64::new(0);
24
+
25
+ pub(crate) fn status_wire(status: DeliveryStatus) -> &'static str {
26
+ match status {
27
+ DeliveryStatus::Delivered => "delivered",
28
+ DeliveryStatus::Failed => "failed",
29
+ DeliveryStatus::Queued => "queued",
30
+ DeliveryStatus::Blocked => "blocked",
31
+ DeliveryStatus::Refused => "refused",
32
+ DeliveryStatus::RetryScheduled => "retry_scheduled",
33
+ DeliveryStatus::TrustAutoAnswerExhausted => "trust_auto_answer_exhausted",
34
+ DeliveryStatus::AlreadyDelivered => "already_delivered",
35
+ DeliveryStatus::FallbackLog => "fallback_log",
36
+ DeliveryStatus::BroadcastDelivered => "broadcast_delivered",
37
+ DeliveryStatus::BroadcastPartial => "broadcast_partial",
38
+ DeliveryStatus::FanoutDelivered => "fanout_delivered",
39
+ DeliveryStatus::FanoutPartial => "fanout_partial",
40
+ }
41
+ }
42
+
43
+ pub(crate) fn message_exists(store: &MessageStore, message_id: &str) -> Result<bool, MessagingError> {
44
+ let conn = crate::db::schema::open_db(store.db_path())?;
45
+ let found: Option<String> = conn
46
+ .query_row(
47
+ "select message_id from messages where message_id = ?1",
48
+ params![message_id],
49
+ |row| row.get(0),
50
+ )
51
+ .optional()?;
52
+ Ok(found.is_some())
53
+ }
54
+
55
+ pub(crate) fn next_result_id() -> String {
56
+ let n = RESULT_COUNTER.fetch_add(1, Ordering::Relaxed);
57
+ let nanos = std::time::SystemTime::now()
58
+ .duration_since(std::time::UNIX_EPOCH)
59
+ .map_or(0, |d| d.as_nanos());
60
+ format!("res_{:012x}", (nanos ^ u128::from(n)) & 0xFFFF_FFFF_FFFF)
61
+ }
62
+
63
+ pub(crate) fn next_run_id() -> String {
64
+ let id = next_result_id();
65
+ id.chars().filter(|c| *c != '_').take(12).collect()
66
+ }
67
+
68
+ pub(crate) fn required_str<'a>(value: &'a serde_json::Value, key: &str) -> Result<&'a str, MessagingError> {
69
+ value
70
+ .get(key)
71
+ .and_then(|v| v.as_str())
72
+ .filter(|s| !s.is_empty())
73
+ .ok_or_else(|| MessagingError::Validation(format!("missing required field: {key}")))
74
+ }
75
+
76
+ pub(crate) fn validate_result_envelope(envelope: &serde_json::Value) -> Result<(), MessagingError> {
77
+ let schema = required_str(envelope, "schema_version")?;
78
+ if schema != "result_envelope_v1" {
79
+ return Err(MessagingError::Validation(format!(
80
+ "unsupported schema_version: {schema}"
81
+ )));
82
+ }
83
+ for key in ["task_id", "agent_id", "status", "summary"] {
84
+ let _ = required_str(envelope, key)?;
85
+ }
86
+ for key in ["changes", "tests", "risks", "artifacts", "next_actions"] {
87
+ if !envelope.get(key).is_some_and(serde_json::Value::is_array) {
88
+ return Err(MessagingError::Validation(format!("missing required array field: {key}")));
89
+ }
90
+ }
91
+ Ok(())
92
+ }
93
+
94
+ pub(crate) fn parse_scheduled_kind(kind: &str) -> Result<ScheduledKind, MessagingError> {
95
+ match kind {
96
+ "send" => Ok(ScheduledKind::Send),
97
+ "health_ping" => Ok(ScheduledKind::HealthPing),
98
+ "trust_retry" => Ok(ScheduledKind::TrustRetry),
99
+ other => Err(MessagingError::Validation(format!(
100
+ "unknown scheduled event kind: {other}"
101
+ ))),
102
+ }
103
+ }
104
+
105
+ pub(crate) fn working_seconds(scrollback: &str) -> Option<u64> {
106
+ let lower = scrollback.to_ascii_lowercase();
107
+ let start = lower.find("working (")?;
108
+ let rest = scrollback.get(start + "Working (".len()..)?;
109
+ let seconds = rest.split_once('s')?.0;
110
+ seconds.parse::<u64>().ok()
111
+ }
112
+
113
+ pub(crate) fn non_provider_command(command: &str) -> Option<&str> {
114
+ let base = command.rsplit('/').next().unwrap_or(command);
115
+ let normalized = base.to_ascii_lowercase();
116
+ match normalized.as_str() {
117
+ "" | "codex" | "claude" | "gemini" | "openai" | "team-agent" => None,
118
+ _ => Some(base),
119
+ }
120
+ }
121
+
122
+ pub(crate) fn latest_prompt_signal(scrollback: &str) -> Option<AgentActivity> {
123
+ let lower = scrollback.to_ascii_lowercase();
124
+ let idle_pos = latest_idle_prompt_pos(scrollback);
125
+ let working_pos = ["working", "thinking", "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
126
+ .iter()
127
+ .filter_map(|needle| lower.rfind(needle))
128
+ .max();
129
+ match (idle_pos, working_pos) {
130
+ (Some(i), Some(w)) if i > w => Some(idle_activity()),
131
+ (Some(_), None) => Some(idle_activity()),
132
+ (_, Some(_)) => Some(AgentActivity {
133
+ status: ActivityStatus::Working,
134
+ confidence: 0.9,
135
+ rationale: "working_indicator".to_string(),
136
+ }),
137
+ (None, None) => None,
138
+ }
139
+ }
140
+
141
+ fn latest_idle_prompt_pos(scrollback: &str) -> Option<usize> {
142
+ scrollback
143
+ .match_indices('❯')
144
+ .map(|(idx, _)| idx)
145
+ .chain(scrollback.match_indices('›').map(|(idx, _)| idx))
146
+ .max()
147
+ }
148
+
149
+ fn idle_activity() -> AgentActivity {
150
+ AgentActivity {
151
+ status: ActivityStatus::Idle,
152
+ confidence: 0.9,
153
+ rationale: "idle_prompt".to_string(),
154
+ }
155
+ }
156
+
157
+ /// `_fail_leader_delivery` (`leader.py:394`) — **diagnostic ONLY** (#230 I-4 退化):
158
+ /// 历史上返回 `ok=True/status=FallbackLog/channel="fallback_inbox"` 被上游误读为
159
+ /// "已交付的 fallback log"。现新 leader-delivery primitive(`leader_receiver::send_to_leader_receiver`)
160
+ /// 不再调本函数。本函数保留只供老代码路径(scheduler / 兼容旧测试)使用,且其
161
+ /// outcome **不被视为 success**(I-3 反向断言要求 `leader_receiver.rs` 源文件不得含
162
+ /// `DeliveryStatus::FallbackLog` 或 `fallback_inbox` 字面量,所以本函数从 `leader_receiver.rs`
163
+ /// 搬到本 helpers 模块,字面量在此被允许)。
164
+ pub fn fail_leader_delivery(
165
+ workspace: &std::path::Path,
166
+ payload: &serde_json::Value,
167
+ reason: DeliveryRefusal,
168
+ error: Option<&str>,
169
+ ) -> Result<DeliveryOutcome, MessagingError> {
170
+ let store = MessageStore::open(workspace)?;
171
+ let sender = payload.get("sender").and_then(serde_json::Value::as_str).unwrap_or("system");
172
+ let content = payload.get("content").and_then(serde_json::Value::as_str).unwrap_or("");
173
+ let task_id = payload.get("task_id").and_then(serde_json::Value::as_str);
174
+ let message_id = match payload.get("message_id").and_then(serde_json::Value::as_str) {
175
+ Some(existing) => existing.to_string(),
176
+ None => store.create_message(task_id, sender, "leader", content, None, false, None)?,
177
+ };
178
+ store.mark(&message_id, "failed", error)?;
179
+ crate::event_log::EventLog::new(workspace).write(
180
+ "leader_receiver.delivery_failed",
181
+ serde_json::json!({"message_id": message_id, "reason": serde_json::to_value(reason).ok(), "error": error}),
182
+ )?;
183
+ Ok(DeliveryOutcome {
184
+ ok: true,
185
+ status: DeliveryStatus::FallbackLog,
186
+ message_status: MessageStatusShadow("failed".to_string()),
187
+ message_id: Some(message_id),
188
+ verification: None,
189
+ stage: None,
190
+ reason: Some(reason),
191
+ channel: Some("fallback_inbox".to_string()),
192
+ })
193
+ }
194
+
195
+ pub(crate) fn recent_rfc3339(ts: &str, max_age_seconds: i64) -> bool {
196
+ let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(ts) else {
197
+ return false;
198
+ };
199
+ let age = chrono::Utc::now().signed_duration_since(parsed.with_timezone(&chrono::Utc));
200
+ age.num_seconds() <= max_age_seconds
201
+ }
202
+
203
+ pub(crate) fn stale_rfc3339(ts: &str, min_age_seconds: i64) -> bool {
204
+ let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(ts) else {
205
+ return false;
206
+ };
207
+ let age = chrono::Utc::now().signed_duration_since(parsed.with_timezone(&chrono::Utc));
208
+ age.num_seconds() >= min_age_seconds
209
+ }
@@ -0,0 +1,340 @@
1
+ //! leader.py — leader pane 注入边界 + 恰好一次去重门 (card §21/§72)。
2
+
3
+ use std::path::Path;
4
+
5
+ use serde_json::Value;
6
+
7
+ use crate::event_log::EventLog;
8
+ use crate::message_store::{MessageStore, NotificationClaimParams};
9
+ use crate::model::ids::{OwnerEpoch, TaskId};
10
+ use crate::transport::Transport;
11
+
12
+ use super::helpers::MessageStatusShadow;
13
+ use super::{DeliveryOutcome, DeliveryRefusal, DeliveryStatus, MessagingError};
14
+
15
+ /// `_send_to_leader_receiver` (`leader.py:69`) — **N31/N32 funnel primitive**:所有 leader-bound
16
+ /// caller(send_message(to=leader) / report_result / request_human / idle reminder /
17
+ /// broadcast-to-leader / peer-mirror / worker.abnormal_exit)统一经过这里。
18
+ ///
19
+ /// 职责 = create_message + leader_notification_log dedup(result_id 时)+ audit + emit
20
+ /// `deliver_to_leader.submit`(funnel 指纹,契约 grep 用)。**不**预 claim:状态留 `accepted`,
21
+ /// 让后续 `deliver_pending_messages` 同一管道做物理 inject(MUST-13 单注入点,leader/worker 同路径
22
+ /// → #229 step2-gate 的 trust-defer 对 leader pane 自然适用)。**不**调 `fail_leader_delivery`
23
+ /// 走 fallback inbox 假绿:无可用 leader pane → 返 `Blocked + channel=rebind_required`(I-4)。
24
+ #[allow(clippy::too_many_arguments)]
25
+ pub fn send_to_leader_receiver(
26
+ workspace: &Path,
27
+ state: &serde_json::Value,
28
+ leader_id: &str,
29
+ content: &str,
30
+ task_id: Option<&TaskId>,
31
+ sender: &str,
32
+ requires_ack: bool,
33
+ result_id: Option<&str>,
34
+ event_log: &EventLog,
35
+ ) -> Result<DeliveryOutcome, MessagingError> {
36
+ let store = MessageStore::open(workspace)?;
37
+ let owner_team = active_team_key(workspace, state);
38
+ if requires_ack {
39
+ event_log.write(
40
+ "leader_receiver.no_ack_forced",
41
+ serde_json::json!({"sender": sender, "leader_id": leader_id, "result_id": result_id}),
42
+ )?;
43
+ }
44
+ let message_id = store.create_message(
45
+ task_id.map(TaskId::as_str),
46
+ sender,
47
+ leader_id,
48
+ content,
49
+ None,
50
+ false,
51
+ Some(&owner_team),
52
+ )?;
53
+ // #231 exactly-once across rebind: insert the leader_notification_log PK BEFORE the
54
+ // unbound-pane check. That way, if the result row gets blocked (I-4) and later the
55
+ // leader rebinds + reclaim replays this row, the dedup PK still gates any second
56
+ // report_result attempt with the same result_id (no duplicate notification).
57
+ if let Some(result_id) = result_id {
58
+ let claim = store.claim_leader_notification_delivery(NotificationClaimParams {
59
+ result_id,
60
+ owner_team_id: Some(&owner_team),
61
+ owner_epoch: owner_epoch_i64(state),
62
+ leader_session_uuid: leader_session_uuid(state),
63
+ proposed_message_id: &message_id,
64
+ envelope_hash: "",
65
+ pane_id: leader_pane_id(state),
66
+ })?;
67
+ if claim.status != "claimed_by_you" {
68
+ // Duplicate result_id: keep first winner's notified_message_id, drop this row
69
+ // by marking failed (deliver_pending will then skip it on its claim guard).
70
+ store.mark(&message_id, "failed", Some("already_notified_by"))?;
71
+ event_log.write(
72
+ "deliver_to_leader.submit",
73
+ serde_json::json!({
74
+ "message_id": claim.notified_message_id,
75
+ "leader_id": leader_id,
76
+ "owner_team_id": owner_team,
77
+ "result_id": result_id,
78
+ "dedup": "already_notified_by",
79
+ }),
80
+ )?;
81
+ return Ok(DeliveryOutcome {
82
+ ok: true,
83
+ status: DeliveryStatus::AlreadyDelivered,
84
+ message_status: MessageStatusShadow("already_notified".to_string()),
85
+ message_id: Some(claim.notified_message_id),
86
+ verification: None,
87
+ stage: None,
88
+ reason: None,
89
+ channel: Some("leader_receiver".to_string()),
90
+ });
91
+ }
92
+ }
93
+ // #230 funnel fingerprint: deliver_to_leader.submit is emitted on EVERY funnel call,
94
+ // including the I-4 unbound case below — the worker's intent IS to submit to leader,
95
+ // the rebind path will replay this row through deliver_pending once the leader pane
96
+ // is bound. Emitting submit only on bound would skew the funnel audit and miss the
97
+ // exactly-once guarantee for blocked-then-reclaimed deliveries.
98
+ event_log.write(
99
+ "deliver_to_leader.submit",
100
+ serde_json::json!({
101
+ "message_id": message_id,
102
+ "leader_id": leader_id,
103
+ "owner_team_id": owner_team,
104
+ "sender": sender,
105
+ "result_id": result_id,
106
+ }),
107
+ )?;
108
+ // I-4: unbound leader pane → rebind_required (not the legacy diagnostic success).
109
+ // The row IS persisted (caller / rebind audit / future replay need the message_id),
110
+ // but marked `failed` so `deliver_pending_messages` does NOT pick it up — pane stays
111
+ // untouched, ok=false, channel=rebind_required. #231 auto-reclaim's
112
+ // requeue_after_claim_leader flips this row back to `accepted` after rebind, and
113
+ // deliver_pending replays it through the same pipeline (same message_id, exactly once).
114
+ let pane_attached = leader_pane_id(state)
115
+ .filter(|pane_id| leader_pane_is_live(workspace, pane_id))
116
+ .is_some();
117
+ if !pane_attached {
118
+ let _ = store.mark(&message_id, "failed", Some("leader_not_attached"));
119
+ event_log.write(
120
+ "leader_receiver.delivery_blocked",
121
+ serde_json::json!({
122
+ "message_id": message_id,
123
+ "sender": sender,
124
+ "leader_id": leader_id,
125
+ "owner_team_id": owner_team,
126
+ "reason": "leader_not_attached",
127
+ "channel": "rebind_required",
128
+ "action": "run team-agent claim-leader or team-agent takeover",
129
+ }),
130
+ )?;
131
+ return Ok(DeliveryOutcome {
132
+ ok: false,
133
+ status: DeliveryStatus::Blocked,
134
+ message_status: MessageStatusShadow("blocked".to_string()),
135
+ message_id: Some(message_id),
136
+ verification: None,
137
+ stage: None,
138
+ reason: Some(DeliveryRefusal::LeaderNotAttached),
139
+ channel: Some("rebind_required".to_string()),
140
+ });
141
+ }
142
+ event_log.write(
143
+ "leader_receiver.queued",
144
+ serde_json::json!({
145
+ "message_id": message_id,
146
+ "leader_id": leader_id,
147
+ "owner_team_id": owner_team,
148
+ "result_id": result_id,
149
+ }),
150
+ )?;
151
+ Ok(DeliveryOutcome {
152
+ ok: true,
153
+ status: DeliveryStatus::Queued,
154
+ message_status: MessageStatusShadow("accepted".to_string()),
155
+ message_id: Some(message_id),
156
+ verification: None,
157
+ stage: None,
158
+ reason: None,
159
+ channel: Some("leader_receiver".to_string()),
160
+ })
161
+ }
162
+
163
+ /// `claim_leader_receiver` (`leader.py:348`):认领/接管 leader pane + `owner_epoch++`。身份判定
164
+ /// 借 step 10 原语 (`_target_matches_owner_identity`/`_leader_command_looks_usable`)。
165
+ pub fn claim_leader_receiver(
166
+ workspace: &Path,
167
+ state: &mut serde_json::Value,
168
+ candidate: &serde_json::Value,
169
+ event_log: &EventLog,
170
+ confirm: bool,
171
+ expected_epoch: Option<OwnerEpoch>,
172
+ ) -> Result<serde_json::Value, MessagingError> {
173
+ if !confirm {
174
+ return Ok(serde_json::json!({
175
+ "ok": false,
176
+ "status": "refused",
177
+ "reason": "confirm_required",
178
+ "action": "team-agent claim-leader --confirm",
179
+ }));
180
+ }
181
+ if let Some(expected) = expected_epoch {
182
+ if owner_epoch(state).is_some_and(|current| current != expected.0) {
183
+ let owner_epoch = owner_epoch(state).unwrap_or(0);
184
+ let bound_pane_id = leader_pane_id(state).map(ToString::to_string);
185
+ let value = serde_json::json!({
186
+ "ok": false,
187
+ "status": "refused",
188
+ "reason": "owner_epoch_advanced",
189
+ "owner_epoch": owner_epoch,
190
+ "bound_pane_id": bound_pane_id,
191
+ });
192
+ event_log.write("leader_receiver.claim_refused", value.clone())?;
193
+ return Ok(value);
194
+ }
195
+ }
196
+ let candidate_pane = candidate.get("pane_id").and_then(Value::as_str);
197
+ if candidate_pane.is_some() && candidate_pane == leader_pane_id(state) {
198
+ return Ok(serde_json::json!({
199
+ "ok": true,
200
+ "status": "already_bound",
201
+ "pane_id": candidate_pane,
202
+ "owner_epoch": owner_epoch(state).unwrap_or(0),
203
+ }));
204
+ }
205
+ let next_epoch = owner_epoch(state).unwrap_or(0).saturating_add(1);
206
+ let Some(root) = state.as_object_mut() else {
207
+ return Err(MessagingError::Routing("runtime state root is not an object".to_string()));
208
+ };
209
+ let owner = root
210
+ .entry("team_owner")
211
+ .or_insert_with(|| serde_json::json!({}));
212
+ if let Some(owner) = owner.as_object_mut() {
213
+ owner.insert("owner_epoch".to_string(), serde_json::json!(next_epoch));
214
+ }
215
+ let receiver = root
216
+ .entry("leader_receiver")
217
+ .or_insert_with(|| serde_json::json!({}));
218
+ if let Some(receiver) = receiver.as_object_mut() {
219
+ receiver.insert("mode".to_string(), serde_json::json!("direct_tmux"));
220
+ receiver.insert("owner_epoch".to_string(), serde_json::json!(next_epoch));
221
+ copy_candidate_field(receiver, candidate, "pane_id");
222
+ copy_candidate_field(receiver, candidate, "provider");
223
+ copy_candidate_field(receiver, candidate, "leader_session_uuid");
224
+ }
225
+ crate::state::persist::save_runtime_state(workspace, state)?;
226
+ event_log.write(
227
+ "leader_receiver.claimed",
228
+ serde_json::json!({"owner_epoch": next_epoch, "candidate": candidate}),
229
+ )?;
230
+ let receiver = state
231
+ .get("leader_receiver")
232
+ .cloned()
233
+ .unwrap_or_else(|| candidate.clone());
234
+ Ok(serde_json::json!({
235
+ "ok": true,
236
+ "status": "claimed",
237
+ "leader_receiver": receiver,
238
+ "owner_epoch": next_epoch,
239
+ }))
240
+ }
241
+
242
+ /// `_mirror_peer_message_to_leader` (`leader.py:31`):peer→peer 消息镜像到 leader receiver。
243
+ pub fn mirror_peer_message_to_leader(
244
+ workspace: &Path,
245
+ state: &serde_json::Value,
246
+ sender: &str,
247
+ recipient: &str,
248
+ content: &str,
249
+ task_id: Option<&TaskId>,
250
+ event_log: &EventLog,
251
+ ) -> Result<(), MessagingError> {
252
+ let _ = send_to_leader_receiver(
253
+ workspace,
254
+ state,
255
+ "leader",
256
+ content,
257
+ task_id,
258
+ sender,
259
+ false,
260
+ None,
261
+ event_log,
262
+ )?;
263
+ let _ = recipient;
264
+ Ok(())
265
+ }
266
+
267
+ pub(crate) fn active_team_key(workspace: &Path, state: &Value) -> String {
268
+ state
269
+ .get("active_team_key")
270
+ .and_then(Value::as_str)
271
+ .filter(|team| !team.is_empty())
272
+ .map(ToString::to_string)
273
+ .or_else(|| workspace.file_name().map(|name| name.to_string_lossy().to_string()))
274
+ .unwrap_or_else(|| "current".to_string())
275
+ }
276
+
277
+ fn owner_epoch(state: &Value) -> Option<u64> {
278
+ receiver_or_owner_field(state, "team_owner", "owner_epoch")
279
+ .and_then(Value::as_u64)
280
+ .or_else(|| receiver_or_owner_field(state, "leader_receiver", "owner_epoch").and_then(Value::as_u64))
281
+ }
282
+
283
+ fn receiver_or_owner_field<'a>(state: &'a Value, record: &str, field: &str) -> Option<&'a Value> {
284
+ state
285
+ .get(record)
286
+ .and_then(|v| v.get(field))
287
+ .or_else(|| {
288
+ let active = state.get("active_team_key").and_then(Value::as_str)?;
289
+ state
290
+ .get("teams")
291
+ .and_then(Value::as_object)
292
+ .and_then(|teams| teams.get(active))
293
+ .and_then(|team| team.get(record))
294
+ .and_then(|v| v.get(field))
295
+ })
296
+ }
297
+
298
+ fn leader_record_field<'a>(state: &'a Value, field: &str) -> Option<&'a Value> {
299
+ receiver_or_owner_field(state, "leader_receiver", field)
300
+ .or_else(|| receiver_or_owner_field(state, "team_owner", field))
301
+ }
302
+
303
+ fn owner_epoch_i64(state: &Value) -> Option<i64> {
304
+ owner_epoch(state).and_then(|epoch| i64::try_from(epoch).ok())
305
+ }
306
+
307
+ fn leader_session_uuid(state: &Value) -> Option<&str> {
308
+ leader_record_field(state, "leader_session_uuid").and_then(Value::as_str)
309
+ }
310
+
311
+ pub(crate) fn leader_pane_bound_but_not_live(workspace: &Path, state: &Value) -> bool {
312
+ leader_pane_id(state)
313
+ .is_some_and(|pane_id| !leader_pane_is_live(workspace, pane_id))
314
+ }
315
+
316
+ fn leader_pane_is_live(workspace: &Path, pane_id: &str) -> bool {
317
+ let mut targets = crate::tmux_backend::TmuxBackend::for_workspace(workspace)
318
+ .list_targets()
319
+ .unwrap_or_default();
320
+ targets.extend(
321
+ crate::tmux_backend::TmuxBackend::new()
322
+ .list_targets()
323
+ .unwrap_or_default(),
324
+ );
325
+ targets.iter().any(|target| target.pane_id.as_str() == pane_id)
326
+ }
327
+
328
+ fn leader_pane_id(state: &Value) -> Option<&str> {
329
+ leader_record_field(state, "pane_id").and_then(Value::as_str)
330
+ }
331
+
332
+ fn copy_candidate_field(
333
+ out: &mut serde_json::Map<String, Value>,
334
+ candidate: &Value,
335
+ key: &str,
336
+ ) {
337
+ if let Some(value) = candidate.get(key) {
338
+ out.insert(key.to_string(), value.clone());
339
+ }
340
+ }