agentwire-dev 1.65.0__tar.gz → 1.65.2__tar.gz

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 (509) hide show
  1. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/PKG-INFO +1 -1
  2. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/__init__.py +1 -1
  3. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/core.py +12 -0
  4. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/inbox.py +65 -32
  5. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/desktop.py +17 -1
  6. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/session_cli.py +23 -3
  7. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/desktop.js +19 -1
  8. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/notifications-panel.js +12 -3
  9. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/worktree.py +33 -0
  10. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/messaging.md +8 -2
  11. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/conftest.py +1 -1
  12. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_worktree_cmd.py +32 -0
  13. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_core_created_by_rooting.py +11 -0
  14. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_inbox.py +71 -7
  15. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_mcp_tools_args.py +29 -0
  16. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_portal_api.py +69 -3
  17. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree.py +43 -0
  18. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.agentwire.yml.example +0 -0
  19. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.claude/commands/handoff.md +0 -0
  20. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.claude/skills/wiki/SKILL.md +0 -0
  21. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/FUNDING.yml +0 -0
  22. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  23. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/ISSUE_TEMPLATE/question.md +0 -0
  25. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  26. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/pytest.yml +0 -0
  27. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/ruff.yml +0 -0
  28. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/security.yml +0 -0
  29. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/tts-smoke.yml +0 -0
  30. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.gitignore +0 -0
  31. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/AGENTS.md +0 -0
  32. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/CODE_OF_CONDUCT.md +0 -0
  33. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/CONTRIBUTING.md +0 -0
  34. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/Dockerfile.local +0 -0
  35. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/LICENSE +0 -0
  36. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/README.md +0 -0
  37. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/RELEASING.md +0 -0
  38. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/SECURITY.md +0 -0
  39. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/SPONSORS.md +0 -0
  40. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/__main__.py +0 -0
  41. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/agents/__init__.py +0 -0
  42. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/agents/base.py +0 -0
  43. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/agents/tmux.py +0 -0
  44. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/cached_status.py +0 -0
  45. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/__init__.py +0 -0
  46. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/base.py +0 -0
  47. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/email.py +0 -0
  48. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/push.py +0 -0
  49. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/quo.py +0 -0
  50. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels_cli.py +0 -0
  51. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/chrome_tabs.py +0 -0
  52. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/completion.py +0 -0
  53. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/config.py +0 -0
  54. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/config_cli.py +0 -0
  55. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/__init__.py +0 -0
  56. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/cli.py +0 -0
  57. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/inbox.py +0 -0
  58. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/minutes.py +0 -0
  59. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/state.py +0 -0
  60. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/view.py +0 -0
  61. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/devices.py +0 -0
  62. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/diff_cli.py +0 -0
  63. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/doctor_cli.py +0 -0
  64. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/doctor_voice.py +0 -0
  65. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/ensure_cli.py +0 -0
  66. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/fetch.py +0 -0
  67. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/__init__.py +0 -0
  68. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/git_state.py +0 -0
  69. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/instructions.py +0 -0
  70. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/parser.py +0 -0
  71. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/renderer.py +0 -0
  72. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/schema.py +0 -0
  73. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff_cli.py +0 -0
  74. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/history.py +0 -0
  75. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/history_cli.py +0 -0
  76. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/__init__.py +0 -0
  77. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/agentwire-permission.sh +0 -0
  78. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/__init__.py +0 -0
  79. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/audit_logger.py +0 -0
  80. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/bash-tool-damage-control.py +0 -0
  81. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/edit-tool-damage-control.py +0 -0
  82. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/mcp-tool-damage-control.py +0 -0
  83. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/read-tool-damage-control.py +0 -0
  84. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/agentwire.yaml +0 -0
  85. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/aws.yaml +0 -0
  86. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/cloud-hosting.yaml +0 -0
  87. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/containers.yaml +0 -0
  88. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/control-plane.yaml +0 -0
  89. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/core.yaml +0 -0
  90. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/databases.yaml +0 -0
  91. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/firebase.yaml +0 -0
  92. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/gcp.yaml +0 -0
  93. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/git.yaml +0 -0
  94. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/gws.yaml +0 -0
  95. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/infrastructure.yaml +0 -0
  96. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/outbound.yaml +0 -0
  97. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/publish.yaml +0 -0
  98. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/remote.yaml +0 -0
  99. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/write-tool-damage-control.py +0 -0
  100. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/idle-handler.sh +0 -0
  101. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/queue-processor.sh +0 -0
  102. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks_cli.py +0 -0
  103. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/limits_cli.py +0 -0
  104. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/listen.py +0 -0
  105. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/locking.py +0 -0
  106. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/machine_cli.py +0 -0
  107. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_channels.py +0 -0
  108. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_chrome.py +0 -0
  109. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_cli.py +0 -0
  110. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_core.py +0 -0
  111. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_council.py +0 -0
  112. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_desktop.py +0 -0
  113. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_handoff.py +0 -0
  114. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_history.py +0 -0
  115. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_listen.py +0 -0
  116. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_lock.py +0 -0
  117. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_machine.py +0 -0
  118. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_msg.py +0 -0
  119. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_notify.py +0 -0
  120. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_pane.py +0 -0
  121. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_projects.py +0 -0
  122. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_scheduler.py +0 -0
  123. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_scratchpad.py +0 -0
  124. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_server.py +0 -0
  125. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_services.py +0 -0
  126. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_session.py +0 -0
  127. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_status.py +0 -0
  128. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_task.py +0 -0
  129. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_tunnels.py +0 -0
  130. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_voice.py +0 -0
  131. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_wiki.py +0 -0
  132. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_worktree.py +0 -0
  133. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/msg_cli.py +0 -0
  134. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/network.py +0 -0
  135. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/notify_cli.py +0 -0
  136. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/onboarding.py +0 -0
  137. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/palette_cli.py +0 -0
  138. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/pane_cli.py +0 -0
  139. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/pane_manager.py +0 -0
  140. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/portal_cli.py +0 -0
  141. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/project_config.py +0 -0
  142. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/projects.py +0 -0
  143. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/prompt_router.py +0 -0
  144. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/prompts_cli.py +0 -0
  145. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/push_cli.py +0 -0
  146. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/push_store.py +0 -0
  147. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/repo_cli.py +0 -0
  148. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/research.py +0 -0
  149. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/__init__.py +0 -0
  150. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/agentwire.md +0 -0
  151. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/anchor.md +0 -0
  152. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/chatbot.md +0 -0
  153. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/contributor.md +0 -0
  154. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/correspondent.md +0 -0
  155. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-brain.md +0 -0
  156. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-conscience.md +0 -0
  157. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-critic.md +0 -0
  158. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-devils-advocate.md +0 -0
  159. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-gut.md +0 -0
  160. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-historian.md +0 -0
  161. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-member.md +0 -0
  162. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-orchestrator.md +0 -0
  163. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/init.md +0 -0
  164. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/notifications.md +0 -0
  165. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/orchestrator.md +0 -0
  166. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/reviewer-worktree.md +0 -0
  167. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/reviewer.md +0 -0
  168. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/soul.md +0 -0
  169. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/task-runner.md +0 -0
  170. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/voice.md +0 -0
  171. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/worker-worktree.md +0 -0
  172. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/worker.md +0 -0
  173. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles_cli.py +0 -0
  174. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/__init__.py +0 -0
  175. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/artifacts.py +0 -0
  176. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/config.py +0 -0
  177. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/council.py +0 -0
  178. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/history.py +0 -0
  179. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/machines.py +0 -0
  180. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/notify.py +0 -0
  181. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/palette.py +0 -0
  182. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/permission.py +0 -0
  183. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/projects.py +0 -0
  184. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/push.py +0 -0
  185. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/safety.py +0 -0
  186. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/scheduler.py +0 -0
  187. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/scratchpad.py +0 -0
  188. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/sessions.py +0 -0
  189. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/sessions_admin.py +0 -0
  190. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/voice.py +0 -0
  191. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety/__init__.py +0 -0
  192. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety/_core.py +0 -0
  193. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety_cli.py +0 -0
  194. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety_commands.py +0 -0
  195. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/__init__.py +0 -0
  196. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/dispatch.py +0 -0
  197. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/gates.py +0 -0
  198. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/loop.py +0 -0
  199. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/models.py +0 -0
  200. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/report.py +0 -0
  201. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/schedule.py +0 -0
  202. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/state.py +0 -0
  203. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/zombie.py +0 -0
  204. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler_cli.py +0 -0
  205. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scratchpad.py +0 -0
  206. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/security.py +0 -0
  207. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/send_cli.py +0 -0
  208. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/server.py +0 -0
  209. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/services.py +0 -0
  210. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/session_context.py +0 -0
  211. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/session_ready.py +0 -0
  212. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/ssh.py +0 -0
  213. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--agentwire-text.png +0 -0
  214. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--echo-claw-fg.png +0 -0
  215. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--echo.png +0 -0
  216. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--telephone.png +0 -0
  217. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--tree.png +0 -0
  218. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/announcements.json +0 -0
  219. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/apple-touch-icon.png +0 -0
  220. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/css/desktop.css +0 -0
  221. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/css/mobile.css +0 -0
  222. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/favicon-32.png +0 -0
  223. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/favicon-64.png +0 -0
  224. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/bear.webp +0 -0
  225. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/cat.webp +0 -0
  226. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/crown.webp +0 -0
  227. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/custom/agentwire-portal.webp +0 -0
  228. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/custom/agentwire-tts.webp +0 -0
  229. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/custom/agentwire.webp +0 -0
  230. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/deer.webp +0 -0
  231. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/drone.webp +0 -0
  232. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/eagle.webp +0 -0
  233. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/fox.webp +0 -0
  234. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/hawk.webp +0 -0
  235. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/horse.webp +0 -0
  236. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/lion.webp +0 -0
  237. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/rabbit.webp +0 -0
  238. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/robot.webp +0 -0
  239. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/tiger.webp +0 -0
  240. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/wolf.webp +0 -0
  241. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/img/icon-192.png +0 -0
  242. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/img/icon-512.png +0 -0
  243. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/.gitkeep +0 -0
  244. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/announcement-modal.js +0 -0
  245. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/api.js +0 -0
  246. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/artifact-window.js +0 -0
  247. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/card-terminal.js +0 -0
  248. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/collage.js +0 -0
  249. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/command-palette.js +0 -0
  250. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/council-window.js +0 -0
  251. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/dead-key-suppressor.js +0 -0
  252. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/desktop-manager.js +0 -0
  253. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/help-modal.js +0 -0
  254. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/icon-manager.js +0 -0
  255. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/lineage.js +0 -0
  256. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/mobile.js +0 -0
  257. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/notification-prefs.js +0 -0
  258. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/ptt.js +0 -0
  259. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/push-client.js +0 -0
  260. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/review-window.js +0 -0
  261. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/safety-shared.js +0 -0
  262. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/safety-window.js +0 -0
  263. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/scratchpad.js +0 -0
  264. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/service-classification.js +0 -0
  265. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud-controller.js +0 -0
  266. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud-notices.js +0 -0
  267. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud-spawn.js +0 -0
  268. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud.js +0 -0
  269. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-id.js +0 -0
  270. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-window.js +0 -0
  271. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/shortcuts.js +0 -0
  272. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/artifacts-section.js +0 -0
  273. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/config-section.js +0 -0
  274. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/council-section.js +0 -0
  275. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/machines-section.js +0 -0
  276. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/projects-section.js +0 -0
  277. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/safety-section.js +0 -0
  278. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/scheduler-section.js +0 -0
  279. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/services-section.js +0 -0
  280. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/sessions-section.js +0 -0
  281. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar.js +0 -0
  282. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/spawn-ghost.js +0 -0
  283. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/terminal-font-prefs.js +0 -0
  284. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/terminal-pane.js +0 -0
  285. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/tile-manager.js +0 -0
  286. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/toast.js +0 -0
  287. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/token-modal.js +0 -0
  288. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/topology-render.js +0 -0
  289. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/utils/ansi.js +0 -0
  290. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/utils/rich-text.js +0 -0
  291. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/utils/swipe.js +0 -0
  292. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/autosend-prefs.js +0 -0
  293. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/browser-stt.js +0 -0
  294. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/browser-tts.js +0 -0
  295. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/jargon.js +0 -0
  296. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/prompt.js +0 -0
  297. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/winbox.bundle.min.js +0 -0
  298. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/workspace-window.js +0 -0
  299. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/manifest.webmanifest +0 -0
  300. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/service-worker.js +0 -0
  301. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/topology-harness.html +0 -0
  302. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/__init__.py +0 -0
  303. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/base.py +0 -0
  304. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/cloud.py +0 -0
  305. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/engine.py +0 -0
  306. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/local.py +0 -0
  307. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/server_backend.py +0 -0
  308. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/stt_server.py +0 -0
  309. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/system_cli.py +0 -0
  310. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tabs_cli.py +0 -0
  311. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tasks.py +0 -0
  312. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tasks_cli.py +0 -0
  313. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/__init__.py +0 -0
  314. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/base.html +0 -0
  315. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/council/minutes.html.j2 +0 -0
  316. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/desktop.html +0 -0
  317. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/email_notification.html +0 -0
  318. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/handoff/show-the-story.html.j2 +0 -0
  319. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/handoff/theme.css.j2 +0 -0
  320. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/mobile.html +0 -0
  321. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/pair.html +0 -0
  322. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/tmux.conf +0 -0
  323. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templating.py +0 -0
  324. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/aws.yaml +0 -0
  325. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/docker.yaml +0 -0
  326. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/gcp.yaml +0 -0
  327. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/gh.yaml +0 -0
  328. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/git.yaml +0 -0
  329. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/gws.yaml +0 -0
  330. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/kubectl.yaml +0 -0
  331. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/npm.yaml +0 -0
  332. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/terraform.yaml +0 -0
  333. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/uv.yaml +0 -0
  334. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/__init__.py +0 -0
  335. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/audio.py +0 -0
  336. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/base.py +0 -0
  337. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/__init__.py +0 -0
  338. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/chatterbox.py +0 -0
  339. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/kokoro.py +0 -0
  340. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/zonos.py +0 -0
  341. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/kokoro_server.py +0 -0
  342. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/local.py +0 -0
  343. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/registry.py +0 -0
  344. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts_cli.py +0 -0
  345. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts_server.py +0 -0
  346. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tunnels.py +0 -0
  347. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tunnels_cli.py +0 -0
  348. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/usage_limit.py +0 -0
  349. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/__init__.py +0 -0
  350. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/chunker.py +0 -0
  351. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/event_log.py +0 -0
  352. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/file_io.py +0 -0
  353. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/paths.py +0 -0
  354. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/speech.py +0 -0
  355. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/subprocess.py +0 -0
  356. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/validation.py +0 -0
  357. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/voice_status.py +0 -0
  358. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/wiki.py +0 -0
  359. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/wiki_audit.py +0 -0
  360. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/wiki_cli.py +0 -0
  361. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/worktree_registry.py +0 -0
  362. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/decisions/obsidian-second-brain.md +0 -0
  363. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/design/session-card-fields.md +0 -0
  364. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/design/session-card-recommendation.md +0 -0
  365. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/logo.png +0 -0
  366. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/INDEX.md +0 -0
  367. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/architecture.md +0 -0
  368. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/briefing-mode.md +0 -0
  369. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/communication/channels.md +0 -0
  370. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/communication/hammerspoon.md +0 -0
  371. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/communication/handoff.md +0 -0
  372. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/concepts.md +0 -0
  373. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/council.md +0 -0
  374. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/deployment/remote-access.md +0 -0
  375. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/deployment/remote-machines.md +0 -0
  376. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/glossary.md +0 -0
  377. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/integrations/gws-google-workspace-cli.md +0 -0
  378. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/damage-control.md +0 -0
  379. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/parallel-refactor.md +0 -0
  380. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/portal.md +0 -0
  381. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/session-topology.md +0 -0
  382. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/shell-escaping.md +0 -0
  383. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/troubleshooting.md +0 -0
  384. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/window-collage.md +0 -0
  385. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/quickstart.md +0 -0
  386. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/research/briefing-mode-feasibility.md +0 -0
  387. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/research/orchestration-transport-alternatives.md +0 -0
  388. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/scheduling/scheduled-workloads.md +0 -0
  389. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/damage-control-hardening.md +0 -0
  390. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/pip-audit.md +0 -0
  391. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/remote-access-hardening.md +0 -0
  392. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/secrets.md +0 -0
  393. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/services.md +0 -0
  394. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/claude-code-auto-mode.md +0 -0
  395. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/prompt-routing.md +0 -0
  396. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/window-sizing.md +0 -0
  397. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/worktree-sessions.md +0 -0
  398. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/usage-limit-recovery.md +0 -0
  399. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/shim-contract.md +0 -0
  400. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/stt-cloud.md +0 -0
  401. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/stt-self-hosted.md +0 -0
  402. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/tts-self-hosted.md +0 -0
  403. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/pyproject.toml +0 -0
  404. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/e2e/test_portal_ui.py +0 -0
  405. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/fixtures/sample_agentwire.yml +0 -0
  406. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/fixtures/sample_config.yaml +0 -0
  407. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/fixtures/sample_scheduler.yaml +0 -0
  408. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_council_portal.py +0 -0
  409. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_scheduler_board.py +0 -0
  410. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_send_verified_tmux.py +0 -0
  411. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_server_websockets.py +0 -0
  412. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_announcements.py +0 -0
  413. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_build_agent_command.py +0 -0
  414. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_channels.py +0 -0
  415. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_chrome_tabs.py +0 -0
  416. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_cli_commands.py +0 -0
  417. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_cli_output.py +0 -0
  418. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_cli_smoke.py +0 -0
  419. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_config.py +0 -0
  420. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_config_cli.py +0 -0
  421. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_control_plane_protection.py +0 -0
  422. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_cli.py +0 -0
  423. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_inbox.py +0 -0
  424. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_minutes.py +0 -0
  425. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_state.py +0 -0
  426. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_view.py +0 -0
  427. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_damage_control_bypass.py +0 -0
  428. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_damage_control_hooks.py +0 -0
  429. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_damage_control_sync.py +0 -0
  430. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_dangling_worktrees.py +0 -0
  431. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_devices.py +0 -0
  432. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_diff_cli.py +0 -0
  433. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_doctor_scheduler_staleness.py +0 -0
  434. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_doctor_task_migration.py +0 -0
  435. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_doctor_voice.py +0 -0
  436. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_event_log.py +0 -0
  437. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_file_io.py +0 -0
  438. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_guarded_launch_command.py +0 -0
  439. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_git_state.py +0 -0
  440. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_instructions.py +0 -0
  441. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_parser.py +0 -0
  442. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_renderer.py +0 -0
  443. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_history.py +0 -0
  444. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_hooks_install.py +0 -0
  445. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_idle_handler.py +0 -0
  446. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_idle_nag.py +0 -0
  447. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_init_command.py +0 -0
  448. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_install_parity.py +0 -0
  449. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_limits_tick_isolation.py +0 -0
  450. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_locking.py +0 -0
  451. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_mcp_server.py +0 -0
  452. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_msg_cli.py +0 -0
  453. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_notify_cli.py +0 -0
  454. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_onboarding_reinit.py +0 -0
  455. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_palette_cli.py +0 -0
  456. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_pip_environment.py +0 -0
  457. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_posture_default.py +0 -0
  458. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_project_config.py +0 -0
  459. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_projects_registry.py +0 -0
  460. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_prompt_router.py +0 -0
  461. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_push.py +0 -0
  462. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_rebuild_guard.py +0 -0
  463. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_repo_cli.py +0 -0
  464. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_rich_text_js.py +0 -0
  465. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_roles.py +0 -0
  466. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_routes_snapshot.py +0 -0
  467. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_commands.py +0 -0
  468. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_disabled_rules.py +0 -0
  469. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_escape_hatch.py +0 -0
  470. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_heal.py +0 -0
  471. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_kill_switch.py +0 -0
  472. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scheduler.py +0 -0
  473. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scheduler_parsing.py +0 -0
  474. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scheduler_zombie.py +0 -0
  475. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scratchpad.py +0 -0
  476. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_security.py +0 -0
  477. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_server_async.py +0 -0
  478. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_server_pure.py +0 -0
  479. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_services.py +0 -0
  480. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_session_context.py +0 -0
  481. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_session_created_by_env.py +0 -0
  482. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_session_ready.py +0 -0
  483. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_shared_dir_guard.py +0 -0
  484. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_shim_selfheal.py +0 -0
  485. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_skill_install.py +0 -0
  486. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_speech_tags.py +0 -0
  487. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_ssh.py +0 -0
  488. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_stt_backend.py +0 -0
  489. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_stt_cloud.py +0 -0
  490. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_stt_engine.py +0 -0
  491. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_task_cli.py +0 -0
  492. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tasks.py +0 -0
  493. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tasks_cli.py +0 -0
  494. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_templating.py +0 -0
  495. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_terminal_resize.py +0 -0
  496. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tmux_template.py +0 -0
  497. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tts_engine_resolution.py +0 -0
  498. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tts_local.py +0 -0
  499. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tunnels_probe.py +0 -0
  500. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_unattended_env_isolation.py +0 -0
  501. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_usage_limit.py +0 -0
  502. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_validation.py +0 -0
  503. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_voice_status.py +0 -0
  504. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_wiki.py +0 -0
  505. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_wiki_audit.py +0 -0
  506. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_window_cycling_binding.py +0 -0
  507. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree_branch_cleanup.py +0 -0
  508. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree_layout.py +0 -0
  509. {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentwire-dev
3
- Version: 1.65.0
3
+ Version: 1.65.2
4
4
  Summary: Multi-session voice web interface for AI coding agents
5
5
  Project-URL: Homepage, https://agentwire.dev
6
6
  Project-URL: Repository, https://github.com/dotdevdotdev/agentwire-dev
@@ -1,3 +1,3 @@
1
1
  """AgentWire - Multi-session voice web interface for AI coding agents."""
2
2
 
3
- __version__ = "1.65.0"
3
+ __version__ = "1.65.2"
@@ -479,9 +479,21 @@ def resolve_default_created_by(caller: str | None, target_path: Path) -> str | N
479
479
  `{project}-{name}`, and would misjudge same/cross-project); if the
480
480
  caller's real cwd can't be confirmed, treat it as unknown (no inheritance)
481
481
  rather than risk a wrong guess.
482
+
483
+ A service session (agentwire-portal, -tts, -stt, -kokoro, ...) never
484
+ qualifies as a parent: it's infrastructure, not an agent that will ever
485
+ drain its msg inbox, so anything parented to it dead-letters forever. A
486
+ subprocess the portal shells out to inherits its TMUX_PANE and resolves
487
+ ``caller`` right back to "agentwire-portal" — this was the root cause of
488
+ a 147-message dead-letter storm (each mailing the owner individually,
489
+ 2026-07-19).
482
490
  """
483
491
  if not caller:
484
492
  return None
493
+ from .services import is_service_session
494
+
495
+ if is_service_session(caller):
496
+ return None
485
497
  caller_path = _live_session_cwd(caller)
486
498
  if caller_path is None:
487
499
  return None
@@ -453,6 +453,7 @@ def gc_sender(sender: str) -> dict:
453
453
  # delivered, not lost. Skip GC for this recipient this round.
454
454
  continue
455
455
  try:
456
+ newly_dead: list[Message] = []
456
457
  for path in paths:
457
458
  if not path.exists():
458
459
  continue # delivered + unlinked just before we locked
@@ -472,7 +473,7 @@ def gc_sender(sender: str) -> dict:
472
473
  attempts=msg.attempts, reason="sender_exited",
473
474
  )
474
475
  dead += 1
475
- _escalate_dead_letter(msg, "sender_exited")
476
+ newly_dead.append(msg)
476
477
  except OSError:
477
478
  pass
478
479
  else:
@@ -481,6 +482,8 @@ def gc_sender(sender: str) -> dict:
481
482
  dropped += 1
482
483
  except OSError:
483
484
  pass
485
+ # One digest per recipient, not one email per message (#836).
486
+ _escalate_dead_letters(newly_dead, "sender_exited")
484
487
  finally:
485
488
  _release_lock(lock)
486
489
 
@@ -608,17 +611,32 @@ def _fmt_ts(ms: int) -> str:
608
611
  return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ms / 1000))
609
612
 
610
613
 
611
- def _escalate_dead_letter(msg: Message, reason: str) -> None:
612
- """Email the owner when a load-bearing report-back dead-letters.
614
+ # A single stuck recipient (e.g. a session wrongly parented to a service
615
+ # session that can never drain its inbox) can dead-letter dozens of messages
616
+ # in one drain pass — cap the per-message detail in the digest so the email
617
+ # stays readable instead of dumping an unbounded wall of text.
618
+ _ESCALATE_DIGEST_DETAIL_CAP = 10
619
+
620
+
621
+ def _escalate_dead_letters(messages: list[Message], reason: str) -> None:
622
+ """Email the owner once per batch when load-bearing report-backs dead-letter.
613
623
 
614
624
  ``done`` / ``request`` / ``escalation`` are load-bearing — a silently-dropped
615
625
  one is a real loss, so we surface it out-of-band via the shared Resend wiring
616
626
  (the same owner-escalation channel usage-limit parking uses). ``note`` and
617
- ``ingest`` are not escalated. Best-effort: a missing key or send failure must
618
- never break the drain — the corpse already sits in ``dead/`` for
619
- ``agentwire msg dead``.
627
+ ``ingest`` are not escalated.
628
+
629
+ *messages* is everything dead-lettered by one caller's batch (one drain
630
+ pass for one recipient, or one sender's GC sweep) — a single digest email
631
+ covers the whole batch instead of one email per message, so a recipient
632
+ that's been permanently undeliverable for a while (e.g. parented to a
633
+ service session that never drains) can't spam the owner's inbox one email
634
+ per stuck message (147 individual emails in ~2s, 2026-07-19). Best-effort:
635
+ a missing key or send failure must never break the drain — each corpse
636
+ already sits in ``dead/`` for ``agentwire msg dead``.
620
637
  """
621
- if msg.kind not in ESCALATE_KINDS:
638
+ batch = [m for m in messages if m.kind in ESCALATE_KINDS]
639
+ if not batch:
622
640
  return
623
641
  try:
624
642
  import socket
@@ -626,36 +644,47 @@ def _escalate_dead_letter(msg: Message, reason: str) -> None:
626
644
  from .channels.email import send_email
627
645
 
628
646
  host = socket.gethostname()
629
- subject = (
630
- f"[agentwire] undelivered {msg.kind}: {msg.sender} → {msg.to} (dead-lettered)"
631
- )
632
- body = "\n".join([
633
- f"A **{msg.kind}** message from **{msg.sender}** to **{msg.to}** on "
634
- f"`{host}` was never delivered after {msg.attempts} attempts and has "
635
- f"been dead-lettered.",
636
- "",
637
- f"- **Kind:** {msg.kind}",
638
- f"- **From:** {msg.sender}",
639
- f"- **To:** {msg.to}",
640
- f"- **Last defer reason:** {reason}",
641
- f"- **Sent:** {_fmt_ts(msg.ts)}",
642
- f"- **Dead-lettered:** {_fmt_ts(msg.dead_ts)}",
643
- "",
644
- "Message text:",
647
+ if len(batch) == 1:
648
+ msg = batch[0]
649
+ subject = (
650
+ f"[agentwire] undelivered {msg.kind}: {msg.sender} → {msg.to} (dead-lettered)"
651
+ )
652
+ else:
653
+ to = batch[0].to
654
+ subject = (
655
+ f"[agentwire] {len(batch)} undelivered messages → {to} (dead-lettered)"
656
+ )
657
+ noun = "message" if len(batch) == 1 else "messages"
658
+ verb = "was" if len(batch) == 1 else "were"
659
+ lines = [
660
+ f"{len(batch)} load-bearing {noun} on `{host}` {verb} never delivered "
661
+ f"and {'has' if len(batch) == 1 else 'have'} been dead-lettered "
662
+ f"(last defer reason: {reason}).",
645
663
  "",
646
- "```",
647
- msg.text,
648
- "```",
664
+ ]
665
+ for msg in batch[:_ESCALATE_DIGEST_DETAIL_CAP]:
666
+ lines += [
667
+ f"- **{msg.kind}** {msg.sender} → {msg.to}, sent {_fmt_ts(msg.ts)}, "
668
+ f"dead-lettered {_fmt_ts(msg.dead_ts)}, {msg.attempts} attempts:",
669
+ " ```",
670
+ f" {msg.text}",
671
+ " ```",
672
+ ]
673
+ remaining = len(batch) - _ESCALATE_DIGEST_DETAIL_CAP
674
+ if remaining > 0:
675
+ lines.append(f"- ...and {remaining} more.")
676
+ lines += [
649
677
  "",
650
- f"Saved in the dead-letter store — review with `agentwire msg dead -s {msg.to}`.",
651
- ])
652
- result = send_email(subject=subject, body=body)
678
+ f"Saved in the dead-letter store — review with "
679
+ f"`agentwire msg dead -s {batch[0].to}`.",
680
+ ]
681
+ result = send_email(subject=subject, body="\n".join(lines))
653
682
  _log_event(
654
- "dead_letter_escalated", id=msg.id, to=msg.to, kind=msg.kind,
683
+ "dead_letter_escalated", to=batch[0].to, count=len(batch), reason=reason,
655
684
  ok=bool(getattr(result, "success", False)),
656
685
  )
657
686
  except Exception as exc: # escalation is best-effort; never break the drain
658
- _log_event("dead_letter_escalate_failed", id=msg.id, to=msg.to, error=str(exc))
687
+ _log_event("dead_letter_escalate_failed", to=batch[0].to, count=len(batch), error=str(exc))
659
688
 
660
689
 
661
690
  def _bump_attempts(messages: list[Message], reason: str = "") -> int:
@@ -675,6 +704,7 @@ def _bump_attempts(messages: list[Message], reason: str = "") -> int:
675
704
  burns out on schedule.
676
705
  """
677
706
  dead = 0
707
+ newly_dead: list[Message] = []
678
708
  for msg in messages:
679
709
  if msg.path is None:
680
710
  continue
@@ -705,7 +735,7 @@ def _bump_attempts(messages: list[Message], reason: str = "") -> int:
705
735
  attempts=msg.attempts, reason=reason,
706
736
  )
707
737
  dead += 1
708
- _escalate_dead_letter(msg, reason)
738
+ newly_dead.append(msg)
709
739
  except OSError:
710
740
  pass
711
741
  else:
@@ -713,6 +743,9 @@ def _bump_attempts(messages: list[Message], reason: str = "") -> int:
713
743
  _write_message(msg.path, msg)
714
744
  except OSError:
715
745
  pass
746
+ # One digest email for the whole batch (all share the same recipient —
747
+ # `messages` is always one session's inbox), not one per message (#836).
748
+ _escalate_dead_letters(newly_dead, reason)
716
749
  return dead
717
750
 
718
751
 
@@ -10,6 +10,22 @@ through the composed server's MRO.
10
10
  from aiohttp import web
11
11
 
12
12
 
13
+ def _artifact_url_hash(url: str) -> str:
14
+ """FNV-1a 32-bit hash of ``url``, as 8 lowercase hex chars.
15
+
16
+ Must match ``artifactUrlHash`` in static/js/desktop.js byte-for-byte —
17
+ the server and frontend both derive the same fallback artifact id from
18
+ the same URL, and a naive char-substitution slug (the prior approach)
19
+ let distinct URLs like "reports/jan.html" and "reports-jan.html" collide
20
+ onto the same id. A hash isn't lossy the way substitution is.
21
+ """
22
+ h = 0x811C9DC5
23
+ for b in url.encode("utf-8"):
24
+ h ^= b
25
+ h = (h * 0x01000193) & 0xFFFFFFFF
26
+ return f"{h:08x}"
27
+
28
+
13
29
  class DesktopRoutesMixin:
14
30
  async def api_desktop_windows(self, request):
15
31
  """GET /api/desktop/windows — query browser clients for open windows."""
@@ -173,7 +189,7 @@ class DesktopRoutesMixin:
173
189
  "url": url,
174
190
  "title": title,
175
191
  "artifact_id": artifact.get("artifact_id")
176
- or f"artifact-{url.replace('/', '-').replace('.', '-')}",
192
+ or f"artifact-{_artifact_url_hash(url)}",
177
193
  }
178
194
  if not text:
179
195
  text = f"**{title}** is ready — click to open"
@@ -62,6 +62,7 @@ from .worktree import (
62
62
  default_base_branch,
63
63
  ensure_worktree,
64
64
  git_root,
65
+ is_registered_worktree,
65
66
  is_valid_branch_name,
66
67
  parse_session_name,
67
68
  remove_worktree,
@@ -1561,7 +1562,17 @@ def _teardown_entry(
1561
1562
  Never reports worktree_removed=True while the dir is still on disk: if
1562
1563
  `git worktree remove --force` can't clear it, this fails LOUDLY
1563
1564
  (success: False) and leaves the registry entry in place, instead of
1564
- silently "unregistering" an orphan.
1565
+ silently "unregistering" an orphan — UNLESS git *already had no record of
1566
+ the path before we touched it* (see `is_registered_worktree`), in which
1567
+ case there's no registered worktree left to protect and the leftover
1568
+ directory (a stale build-tool cache, typically) is hard-deleted instead.
1569
+
1570
+ That pre-check matters: `git worktree prune` below runs unconditionally
1571
+ and clears any entry git marks "prunable" (e.g. one whose linked `.git`
1572
+ file was deleted) as a side effect of THIS call — checking registration
1573
+ after prune would misread a worktree that was real and registered right
1574
+ up until this attempt as a pre-existing orphan, and hard-delete content
1575
+ the fail-loud path exists to protect.
1565
1576
 
1566
1577
  The worktree removal is attempted BEFORE the session is killed, and the
1567
1578
  session is only killed once removal is confirmed — never the reverse.
@@ -1569,10 +1580,17 @@ def _teardown_entry(
1569
1580
  disk with no session left to notice, on any removal failure (bad
1570
1581
  project_path, a stuck worktree, ...) (#740).
1571
1582
  """
1583
+ was_registered = is_registered_worktree(project_path, worktree_path)
1584
+
1572
1585
  # Force-remove the git worktree, then prune admin files either way.
1573
1586
  _, remove_error = remove_worktree(project_path, worktree_path, force=True)
1574
1587
  subprocess.run(["git", "-C", str(project_path), "worktree", "prune"], capture_output=True)
1575
1588
 
1589
+ hard_deleted_orphan = False
1590
+ if worktree_path.exists() and not was_registered:
1591
+ shutil.rmtree(worktree_path, ignore_errors=True)
1592
+ hard_deleted_orphan = not worktree_path.exists()
1593
+
1576
1594
  if worktree_path.exists():
1577
1595
  reason = remove_error or "worktree directory still present after `git worktree remove --force`"
1578
1596
  return {"success": False, "session": session_name, "path": str(worktree_path),
@@ -1605,7 +1623,7 @@ def _teardown_entry(
1605
1623
  return {"success": True, "session": session_name, "path": str(worktree_path),
1606
1624
  "killed": killed, "worktree_removed": True,
1607
1625
  "branch": branch, "branch_deleted": branch_deleted, "branch_note": branch_note,
1608
- "orphaned_tabs": orphaned_tabs}
1626
+ "orphaned_tabs": orphaned_tabs, "hard_deleted_orphan": hard_deleted_orphan}
1609
1627
 
1610
1628
 
1611
1629
  def _worktree_remove(args, project_path: Path, worktree_dir: Path, json_mode: bool) -> int:
@@ -1641,7 +1659,9 @@ def _worktree_remove(args, project_path: Path, worktree_dir: Path, json_mode: bo
1641
1659
  else:
1642
1660
  msg = (f"Removed worktree session '{result['session']}'"
1643
1661
  + (" (killed live session)" if result["killed"] else "")
1644
- + f"; worktree {result['path']} removed")
1662
+ + f"; worktree {result['path']} removed"
1663
+ + (" (git had no registration for it — hard-deleted the leftover directory)"
1664
+ if result.get("hard_deleted_orphan") else ""))
1645
1665
  if result["branch"]:
1646
1666
  msg += f"; branch '{result['branch']}' " + ("deleted" if result["branch_deleted"] else f"kept ({result['branch_note']})")
1647
1667
  if result["orphaned_tabs"]:
@@ -824,6 +824,24 @@ function _mountSessionWindow(session, mode, machine, id) {
824
824
  addTaskbarButton(id, sw);
825
825
  }
826
826
 
827
+ /**
828
+ * FNV-1a 32-bit hash of `url`, as 8 lowercase hex chars. Must match
829
+ * `_artifact_url_hash` in routes/desktop.py byte-for-byte — server and
830
+ * frontend both derive the same fallback artifact id from the same URL. A
831
+ * hash isn't lossy the way the prior char-substitution slug was (distinct
832
+ * URLs like "reports/jan.html" and "reports-jan.html" collided onto the
833
+ * same id).
834
+ */
835
+ function artifactUrlHash(url) {
836
+ let h = 0x811c9dc5;
837
+ const bytes = new TextEncoder().encode(url);
838
+ for (const b of bytes) {
839
+ h ^= b;
840
+ h = Math.imul(h, 0x01000193) >>> 0;
841
+ }
842
+ return h.toString(16).padStart(8, '0');
843
+ }
844
+
827
845
  /**
828
846
  * Open an artifact window (agent-generated HTML or external URL).
829
847
  *
@@ -832,7 +850,7 @@ function _mountSessionWindow(session, mode, machine, id) {
832
850
  * @param {string|null} artifactId - Optional explicit window ID
833
851
  */
834
852
  export function openArtifactWindow(url, title = 'Artifact', artifactId = null) {
835
- const id = artifactId || `artifact-${url.replace(/[\/\.]/g, '-')}`;
853
+ const id = artifactId || `artifact-${artifactUrlHash(url)}`;
836
854
 
837
855
  // Check if already open — restore if minimized, otherwise focus
838
856
  if (artifactWindows.has(id)) {
@@ -86,10 +86,19 @@ class NotificationsPanel {
86
86
  this._removeToast(id, false);
87
87
  }
88
88
 
89
- // Evict oldest if at capacity
89
+ // Evict the oldest toast NOT carrying an artifact notice, if at
90
+ // capacity. Artifact toasts are exempt regardless of priority: an
91
+ // unclicked deliverable must not silently fade (#817) — the cap
92
+ // only trims ordinary toasts (sticky `high`-priority ones included).
93
+ // If every current toast is an artifact notice, the panel
94
+ // temporarily exceeds MAX_TOASTS rather than dropping one.
90
95
  if (this.toasts.size >= MAX_TOASTS) {
91
- const oldest = this.toasts.keys().next().value;
92
- this._removeToast(oldest, false);
96
+ for (const id of this.toasts.keys()) {
97
+ if (!this.artifactNotices.has(id)) {
98
+ this._removeToast(id, false);
99
+ break;
100
+ }
101
+ }
93
102
  }
94
103
 
95
104
  // Resolve auto-fade: explicit timeout wins (0 = sticky); otherwise
@@ -309,6 +309,39 @@ def remove_worktree(project_path: Path, worktree_path: Path, *, force: bool = Tr
309
309
  return False, (result.stderr or result.stdout).strip()
310
310
 
311
311
 
312
+ def is_registered_worktree(project_path: Path, worktree_path: Path) -> bool:
313
+ """Does git's own worktree registry (still) know about ``worktree_path``?
314
+
315
+ A directory can outlive its registration — e.g. `git worktree remove`
316
+ succeeded on a prior teardown attempt but the `rm -rf` half crashed
317
+ before clearing a build-tool cache dir left inside, or the admin file
318
+ under `.git/worktrees/` was pruned independently. In that state git
319
+ reports "fatal: ... is not a working tree" for every future removal
320
+ attempt, forever — this lets a caller distinguish that from a real
321
+ registered worktree that `remove --force` merely failed to clear. It
322
+ says nothing about whether the directory holds valuable content — that
323
+ judgment (e.g. "orphaned + unregistered is safe to hard-delete") belongs
324
+ to the caller, not this function.
325
+
326
+ Fails closed on an inconclusive read: if `git worktree list` itself
327
+ errors (corrupt repo, I/O, lock contention), that's reported as
328
+ registered rather than not — a caller gating a destructive action on
329
+ this should default to "assume real" when unsure, not "assume orphan".
330
+ """
331
+ result = subprocess.run(
332
+ ["git", "worktree", "list", "--porcelain"],
333
+ cwd=project_path, capture_output=True, text=True,
334
+ )
335
+ if result.returncode != 0:
336
+ return True
337
+ target = str(worktree_path.resolve())
338
+ for line in result.stdout.splitlines():
339
+ if line.startswith("worktree "):
340
+ if str(Path(line[len("worktree "):]).resolve()) == target:
341
+ return True
342
+ return False
343
+
344
+
312
345
  def worktree_status(worktree_path: Path) -> dict:
313
346
  """Read-only git status for a worktree. Local git only — no network, no gh.
314
347
 
@@ -143,8 +143,14 @@ lands in a durable inbox and is injected only at a safe boundary.
143
143
  / `escalation`) does dead-letter, the owner is emailed via the shared Resend
144
144
  wiring (the same channel usage-limit parking uses) so the loss is surfaced
145
145
  even if nobody runs `msg dead`. `note` is fire-and-forget and `ingest` never
146
- auto-delivers, so neither is escalated. Escalation is best-effort a send
147
- failure is logged (`dead_letter_escalate_failed`) and never breaks the drain.
146
+ auto-delivers, so neither is escalated. Escalation is **batched per drain
147
+ pass, not per message** (#829/#830): every message dead-lettered in the
148
+ same batch for one recipient gets a single digest email (detail capped at
149
+ the first 10, "...and N more" beyond that) instead of one email each — a
150
+ recipient stuck permanently undeliverable (e.g. wrongly parented to a
151
+ service session that never drains) can't spam the owner one email per
152
+ stuck message. Escalation is best-effort — a send failure is logged
153
+ (`dead_letter_escalate_failed`) and never breaks the drain.
148
154
 
149
155
  ### `prompt_is_empty` — the collision detector
150
156
 
@@ -13,7 +13,7 @@ FIXTURES_DIR = Path(__file__).parent / "fixtures"
13
13
  def _no_real_outbound_email(request, monkeypatch):
14
14
  """No test may send real email — ever.
15
15
 
16
- ``_escalate_dead_letter`` (and friends) call the live Resend wiring, so a
16
+ ``_escalate_dead_letters`` (and friends) call the live Resend wiring, so a
17
17
  test that dead-letters a done/request/escalation message without mocking
18
18
  ``send_email`` silently emails the owner on every suite run (found the hard
19
19
  way: ``test_purge_leaves_ingest_and_dead`` flooded the inbox with
@@ -643,6 +643,38 @@ def test_remove_fails_loudly_when_dir_survives(tmp_path, monkeypatch, wt_env, ca
643
643
  assert payload["error"]
644
644
 
645
645
 
646
+ def test_remove_hard_deletes_unregistered_orphan_directory(tmp_path, monkeypatch, wt_env, capsys):
647
+ """A directory git no longer registers as a worktree at all (its
648
+ `.git/worktrees/<name>` admin entry is gone, e.g. from a prior teardown
649
+ that crashed mid-way) must not get stuck failing `git worktree remove`
650
+ forever — since git has nothing left to lose, the leftover directory is
651
+ hard-deleted and teardown proceeds and succeeds."""
652
+ import shutil
653
+
654
+ _, clone = _origin_and_clone(tmp_path, default_branch="develop")
655
+ wt_dir = tmp_path / "worktrees"
656
+ cfg = _config(wt_dir)
657
+ assert _run(monkeypatch, cfg, name="orphan", project=str(clone)) == 0
658
+ wt_path = wt_dir / "clone-repo" / "orphan"
659
+ assert wt_path.exists()
660
+
661
+ # Simulate the crashed-partial-teardown state: git's own registration is
662
+ # gone, but the directory (with leftover content) survives on disk.
663
+ shutil.rmtree(clone / ".git" / "worktrees" / "orphan")
664
+ (wt_path / "stale-cache-file").write_text("leftover\n")
665
+
666
+ capsys.readouterr()
667
+ rc = _run(monkeypatch, cfg, name="orphan", project=str(clone), remove=True)
668
+ assert rc == 0
669
+ assert not wt_path.exists()
670
+ assert reg.entries(clone.resolve()) == []
671
+
672
+ payload = json.loads(capsys.readouterr().out)
673
+ assert payload["success"] is True
674
+ assert payload["worktree_removed"] is True
675
+ assert payload["hard_deleted_orphan"] is True
676
+
677
+
646
678
  def test_remove_kills_alive_session(tmp_path, monkeypatch, wt_env, capsys):
647
679
  _, clone = _origin_and_clone(tmp_path, default_branch="develop")
648
680
  wt_dir = tmp_path / "worktrees"
@@ -115,3 +115,14 @@ class TestResolveDefaultCreatedBy:
115
115
  monkeypatch.setattr(core, "tmux_session_exists", lambda s: False)
116
116
  repo = _make_repo(tmp_path)
117
117
  assert core.resolve_default_created_by("myproject-fix-bug", repo) is None
118
+
119
+ def test_service_session_caller_returns_none(self, monkeypatch, tmp_path):
120
+ # Regression (2026-07-19): a session created via the portal web UI
121
+ # inherits the portal server subprocess's TMUX_PANE, so `caller`
122
+ # resolves to the "agentwire-portal" service session. Parenting to it
123
+ # is a bug even when same_project would otherwise match — a service
124
+ # session never drains its msg inbox, so anything parented to it
125
+ # dead-letters forever (147-message escalation-email storm).
126
+ repo = _make_repo(tmp_path)
127
+ monkeypatch.setattr(core, "_live_session_cwd", lambda s: repo)
128
+ assert core.resolve_default_created_by("agentwire-portal", repo) is None
@@ -760,6 +760,8 @@ class TestDeadLetterEscalation:
760
760
  assert len(inbox.list_dead("s")) == 1 # still archived for audit
761
761
 
762
762
  def test_request_and_escalation_also_email(self, isolate, monkeypatch):
763
+ # Both dead-letter in the same drain pass (same recipient) — one
764
+ # digest email covers the batch, not one email per kind (#836).
763
765
  inbox.enqueue("s", "need creds", kind="request", sender="w")
764
766
  inbox.enqueue("s", "stuck!", kind="escalation", sender="w")
765
767
  self._occupied_agent(monkeypatch)
@@ -767,8 +769,9 @@ class TestDeadLetterEscalation:
767
769
  self._capture_email(monkeypatch, sent)
768
770
  for _ in range(inbox.MAX_ATTEMPTS):
769
771
  inbox.flush_session("s")
770
- kinds = sorted(k["subject"].split("undelivered ")[1].split(":")[0] for k in sent)
771
- assert kinds == ["escalation", "request"]
772
+ assert len(sent) == 1
773
+ assert "2 undelivered messages" in sent[0]["subject"]
774
+ assert "request" in sent[0]["body"] and "escalation" in sent[0]["body"]
772
775
 
773
776
  def test_note_dead_letter_does_not_email(self, isolate, monkeypatch):
774
777
  inbox.enqueue("s", "fyi", kind="note", sender="worker")
@@ -792,6 +795,49 @@ class TestDeadLetterEscalation:
792
795
  inbox.flush_session("s")
793
796
  assert len(inbox.list_dead("s")) == 1 # drain survived; corpse archived
794
797
 
798
+ def test_large_batch_sends_one_digest_not_one_per_message(self, isolate, monkeypatch):
799
+ # Regression (2026-07-19): a recipient stuck permanently undeliverable
800
+ # (e.g. wrongly parented to a service session) can accumulate a large
801
+ # backlog that all crosses MAX_ATTEMPTS in the same drain pass. That
802
+ # must fire ONE digest email, not one per message (147 individual
803
+ # emails in ~2s was the real incident).
804
+ for i in range(20):
805
+ inbox.enqueue("s", f"report {i}", kind="done", sender="w")
806
+ self._occupied_agent(monkeypatch)
807
+ sent = []
808
+ self._capture_email(monkeypatch, sent)
809
+ for _ in range(inbox.MAX_ATTEMPTS):
810
+ inbox.flush_session("s")
811
+ assert len(inbox.list_dead("s")) == 20
812
+ assert len(sent) == 1
813
+ assert "20 undelivered messages" in sent[0]["subject"]
814
+
815
+ def _msg(self, i):
816
+ return inbox.Message(
817
+ id=f"id{i}", sender="w", to="s", kind="done", text=f"report {i}",
818
+ ts=1000 + i, dead_ts=2000 + i,
819
+ )
820
+
821
+ def test_digest_detail_cap_boundary(self, isolate, monkeypatch):
822
+ # Exactly at the cap: every message gets detail, no truncation line.
823
+ sent = []
824
+ self._capture_email(monkeypatch, sent)
825
+ batch = [self._msg(i) for i in range(inbox._ESCALATE_DIGEST_DETAIL_CAP)]
826
+ inbox._escalate_dead_letters(batch, "target_gone")
827
+ assert len(sent) == 1
828
+ assert "...and" not in sent[0]["body"]
829
+ assert all(f"report {i}" in sent[0]["body"] for i in range(len(batch)))
830
+
831
+ def test_digest_detail_cap_truncates_beyond_boundary(self, isolate, monkeypatch):
832
+ # One over the cap: detail for the first CAP, a single "...and 1 more."
833
+ sent = []
834
+ self._capture_email(monkeypatch, sent)
835
+ batch = [self._msg(i) for i in range(inbox._ESCALATE_DIGEST_DETAIL_CAP + 1)]
836
+ inbox._escalate_dead_letters(batch, "target_gone")
837
+ assert len(sent) == 1
838
+ assert "...and 1 more." in sent[0]["body"]
839
+ assert f"report {inbox._ESCALATE_DIGEST_DETAIL_CAP}" not in sent[0]["body"]
840
+
795
841
 
796
842
  class TestIdempotentDelivery:
797
843
  """#621: a delivery_unverified false-negative must NOT re-inject a landed
@@ -959,8 +1005,8 @@ class TestForceFlush:
959
1005
  class TestGcSender:
960
1006
  def test_gc_dead_letters_load_bearing(self, isolate, monkeypatch):
961
1007
  emailed = []
962
- monkeypatch.setattr(inbox, "_escalate_dead_letter",
963
- lambda m, r: emailed.append((m.kind, r)))
1008
+ monkeypatch.setattr(inbox, "_escalate_dead_letters",
1009
+ lambda batch, r: emailed.extend((m.kind, r) for m in batch))
964
1010
  inbox.enqueue("orch", "PR drafted", kind="done", sender="worker")
965
1011
  inbox.enqueue("orch", "need review", kind="request", sender="worker")
966
1012
  res = inbox.gc_sender("worker")
@@ -970,7 +1016,7 @@ class TestGcSender:
970
1016
  assert emailed and all(r == "sender_exited" for _, r in emailed)
971
1017
 
972
1018
  def test_gc_drops_non_load_bearing(self, isolate, monkeypatch):
973
- monkeypatch.setattr(inbox, "_escalate_dead_letter", lambda m, r: None)
1019
+ monkeypatch.setattr(inbox, "_escalate_dead_letters", lambda batch, r: None)
974
1020
  inbox.enqueue("orch", "fyi", kind="note", sender="worker")
975
1021
  res = inbox.gc_sender("worker")
976
1022
  assert res["dropped"] == 1 and res["dead"] == 0
@@ -981,8 +1027,8 @@ class TestGcSender:
981
1027
  # A flush draining this inbox holds the per-session lock; gc must NOT
982
1028
  # dead-letter (and email) a message that flush is mid-delivery on.
983
1029
  emailed = []
984
- monkeypatch.setattr(inbox, "_escalate_dead_letter",
985
- lambda m, r: emailed.append(m.id))
1030
+ monkeypatch.setattr(inbox, "_escalate_dead_letters",
1031
+ lambda batch, r: emailed.extend(m.id for m in batch))
986
1032
  inbox.enqueue("orch", "in flight", kind="done", sender="worker")
987
1033
  held = inbox._acquire_lock("orch") # simulate an in-flight flush
988
1034
  try:
@@ -1001,6 +1047,24 @@ class TestGcSender:
1001
1047
  assert len(inbox.list_messages("orch")) == 1 # other sender's done kept
1002
1048
  assert inbox.list_ingest("orch") # ingest untouched
1003
1049
 
1050
+ def test_gc_batches_one_email_per_recipient(self, isolate, monkeypatch):
1051
+ # Regression (#829/#830): gc_sender must send ONE digest email per
1052
+ # recipient, not one per dead-lettered message — exercised against the
1053
+ # real send_email seam (not a stubbed _escalate_dead_letters), so a
1054
+ # regression back to per-message escalation inside the gc loop would
1055
+ # actually be caught here.
1056
+ sent = []
1057
+ monkeypatch.setattr(
1058
+ "agentwire.channels.email.send_email",
1059
+ lambda **kw: sent.append(kw) or SimpleNamespace(success=True),
1060
+ )
1061
+ inbox.enqueue("orch", "PR drafted", kind="done", sender="worker")
1062
+ inbox.enqueue("orch", "need review", kind="request", sender="worker")
1063
+ res = inbox.gc_sender("worker")
1064
+ assert res["dead"] == 2
1065
+ assert len(sent) == 1
1066
+ assert "2 undelivered messages" in sent[0]["subject"]
1067
+
1004
1068
 
1005
1069
  # =============================================================================
1006
1070
  # Session-name validation — path-traversal hardening
@@ -212,6 +212,35 @@ class TestVoiceTools:
212
212
  assert "parked" in result
213
213
 
214
214
 
215
+ class TestNotifyUserArtifactParams:
216
+ """#822: notify_user's artifact_url/artifact_title params (#821) had zero
217
+ test coverage — cover the body it builds for /api/desktop/notification."""
218
+
219
+ @patch("agentwire.mcp_notify._portal_request")
220
+ def test_artifact_url_sets_artifact_body_with_default_title(self, mock_req):
221
+ from agentwire.mcp_notify import notify_user
222
+ mock_req.return_value = _success(id="n1", clients=1)
223
+ notify_user(text="ready", artifact_url="report.html")
224
+ body = mock_req.call_args[0][2]
225
+ assert body["artifact"] == {"url": "report.html", "title": "Artifact"}
226
+
227
+ @patch("agentwire.mcp_notify._portal_request")
228
+ def test_artifact_title_overrides_default(self, mock_req):
229
+ from agentwire.mcp_notify import notify_user
230
+ mock_req.return_value = _success(id="n1", clients=1)
231
+ notify_user(text="ready", artifact_url="report.html", artifact_title="Q3 Report")
232
+ body = mock_req.call_args[0][2]
233
+ assert body["artifact"] == {"url": "report.html", "title": "Q3 Report"}
234
+
235
+ @patch("agentwire.mcp_notify._portal_request")
236
+ def test_no_artifact_url_omits_artifact_body(self, mock_req):
237
+ from agentwire.mcp_notify import notify_user
238
+ mock_req.return_value = _success(id="n1", clients=1)
239
+ notify_user(text="plain toast", artifact_title="ignored without a url")
240
+ body = mock_req.call_args[0][2]
241
+ assert "artifact" not in body
242
+
243
+
215
244
  # ---------------------------------------------------------------------------
216
245
  # Desktop tools
217
246
  # ---------------------------------------------------------------------------