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.
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/PKG-INFO +1 -1
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/__init__.py +1 -1
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/core.py +12 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/inbox.py +65 -32
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/desktop.py +17 -1
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/session_cli.py +23 -3
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/desktop.js +19 -1
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/notifications-panel.js +12 -3
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/worktree.py +33 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/messaging.md +8 -2
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/conftest.py +1 -1
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_worktree_cmd.py +32 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_core_created_by_rooting.py +11 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_inbox.py +71 -7
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_mcp_tools_args.py +29 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_portal_api.py +69 -3
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree.py +43 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.agentwire.yml.example +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.claude/commands/handoff.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.claude/skills/wiki/SKILL.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/FUNDING.yml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/pytest.yml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/ruff.yml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/security.yml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.github/workflows/tts-smoke.yml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/.gitignore +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/AGENTS.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/CODE_OF_CONDUCT.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/CONTRIBUTING.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/Dockerfile.local +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/LICENSE +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/README.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/RELEASING.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/SECURITY.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/SPONSORS.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/__main__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/agents/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/agents/base.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/agents/tmux.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/cached_status.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/base.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/email.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/push.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels/quo.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/channels_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/chrome_tabs.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/completion.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/config.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/config_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/inbox.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/minutes.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/state.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/council/view.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/devices.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/diff_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/doctor_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/doctor_voice.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/ensure_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/fetch.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/git_state.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/instructions.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/parser.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/renderer.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff/schema.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/handoff_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/history.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/history_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/agentwire-permission.sh +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/audit_logger.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/bash-tool-damage-control.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/edit-tool-damage-control.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/mcp-tool-damage-control.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/read-tool-damage-control.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/agentwire.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/aws.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/cloud-hosting.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/containers.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/control-plane.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/core.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/databases.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/firebase.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/gcp.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/git.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/gws.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/infrastructure.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/outbound.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/publish.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/rules/remote.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/damage-control/write-tool-damage-control.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/idle-handler.sh +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks/queue-processor.sh +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/hooks_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/limits_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/listen.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/locking.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/machine_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_channels.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_chrome.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_core.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_council.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_desktop.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_handoff.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_history.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_listen.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_lock.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_machine.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_msg.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_notify.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_pane.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_projects.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_scheduler.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_scratchpad.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_server.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_services.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_session.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_status.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_task.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_tunnels.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_voice.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_wiki.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/mcp_worktree.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/msg_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/network.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/notify_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/onboarding.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/palette_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/pane_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/pane_manager.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/portal_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/project_config.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/projects.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/prompt_router.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/prompts_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/push_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/push_store.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/repo_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/research.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/agentwire.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/anchor.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/chatbot.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/contributor.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/correspondent.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-brain.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-conscience.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-critic.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-devils-advocate.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-gut.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-historian.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-member.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/council-orchestrator.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/init.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/notifications.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/orchestrator.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/reviewer-worktree.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/reviewer.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/soul.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/task-runner.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/voice.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/worker-worktree.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles/worker.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/roles_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/artifacts.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/config.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/council.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/history.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/machines.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/notify.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/palette.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/permission.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/projects.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/push.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/safety.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/scheduler.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/scratchpad.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/sessions.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/sessions_admin.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/routes/voice.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety/_core.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/safety_commands.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/dispatch.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/gates.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/loop.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/models.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/report.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/schedule.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/state.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler/zombie.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scheduler_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/scratchpad.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/security.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/send_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/server.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/services.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/session_context.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/session_ready.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/ssh.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--agentwire-text.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--echo-claw-fg.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--echo.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--telephone.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/agentwire-splash-logo-layers--tree.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/announcements.json +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/apple-touch-icon.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/css/desktop.css +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/css/mobile.css +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/favicon-32.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/favicon-64.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/bear.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/cat.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/crown.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/custom/agentwire-portal.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/custom/agentwire-tts.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/custom/agentwire.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/deer.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/drone.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/eagle.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/fox.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/hawk.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/horse.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/lion.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/rabbit.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/robot.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/tiger.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/icons/sessions/wolf.webp +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/img/icon-192.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/img/icon-512.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/.gitkeep +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/announcement-modal.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/api.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/artifact-window.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/card-terminal.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/collage.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/command-palette.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/council-window.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/dead-key-suppressor.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/desktop-manager.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/help-modal.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/icon-manager.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/lineage.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/mobile.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/notification-prefs.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/ptt.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/push-client.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/review-window.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/safety-shared.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/safety-window.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/scratchpad.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/service-classification.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud-controller.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud-notices.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud-spawn.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-hud.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-id.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/session-window.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/shortcuts.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/artifacts-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/config-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/council-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/machines-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/projects-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/safety-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/scheduler-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/services-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar/sessions-section.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/sidebar.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/spawn-ghost.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/terminal-font-prefs.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/terminal-pane.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/tile-manager.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/toast.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/token-modal.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/topology-render.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/utils/ansi.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/utils/rich-text.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/utils/swipe.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/autosend-prefs.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/browser-stt.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/browser-tts.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/jargon.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/voice/prompt.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/winbox.bundle.min.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/js/workspace-window.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/manifest.webmanifest +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/service-worker.js +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/static/topology-harness.html +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/base.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/cloud.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/engine.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/local.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/server_backend.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/stt/stt_server.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/system_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tabs_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tasks.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tasks_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/base.html +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/council/minutes.html.j2 +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/desktop.html +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/email_notification.html +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/handoff/show-the-story.html.j2 +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/handoff/theme.css.j2 +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/mobile.html +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/pair.html +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templates/tmux.conf +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/templating.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/aws.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/docker.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/gcp.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/gh.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/git.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/gws.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/kubectl.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/npm.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/terraform.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tooldefs/uv.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/audio.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/base.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/chatterbox.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/kokoro.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/engines/zonos.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/kokoro_server.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/local.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts/registry.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tts_server.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tunnels.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/tunnels_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/usage_limit.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/__init__.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/chunker.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/event_log.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/file_io.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/paths.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/speech.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/utils/subprocess.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/validation.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/voice_status.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/wiki.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/wiki_audit.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/wiki_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/agentwire/worktree_registry.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/decisions/obsidian-second-brain.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/design/session-card-fields.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/design/session-card-recommendation.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/logo.png +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/INDEX.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/architecture.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/briefing-mode.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/communication/channels.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/communication/hammerspoon.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/communication/handoff.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/concepts.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/council.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/deployment/remote-access.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/deployment/remote-machines.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/glossary.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/integrations/gws-google-workspace-cli.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/damage-control.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/parallel-refactor.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/portal.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/session-topology.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/shell-escaping.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/troubleshooting.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/internals/window-collage.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/quickstart.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/research/briefing-mode-feasibility.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/research/orchestration-transport-alternatives.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/scheduling/scheduled-workloads.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/damage-control-hardening.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/pip-audit.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/remote-access-hardening.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/security/secrets.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/services.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/claude-code-auto-mode.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/prompt-routing.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/window-sizing.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/sessions/worktree-sessions.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/usage-limit-recovery.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/shim-contract.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/stt-cloud.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/stt-self-hosted.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/docs/wiki/voice/tts-self-hosted.md +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/pyproject.toml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/e2e/test_portal_ui.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/fixtures/sample_agentwire.yml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/fixtures/sample_config.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/fixtures/sample_scheduler.yaml +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_council_portal.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_scheduler_board.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_send_verified_tmux.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/integration/test_server_websockets.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_announcements.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_build_agent_command.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_channels.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_chrome_tabs.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_cli_commands.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_cli_output.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_cli_smoke.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_config.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_config_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_control_plane_protection.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_inbox.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_minutes.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_state.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_council_view.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_damage_control_bypass.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_damage_control_hooks.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_damage_control_sync.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_dangling_worktrees.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_devices.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_diff_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_doctor_scheduler_staleness.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_doctor_task_migration.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_doctor_voice.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_event_log.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_file_io.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_guarded_launch_command.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_git_state.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_instructions.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_parser.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_handoff_renderer.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_history.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_hooks_install.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_idle_handler.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_idle_nag.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_init_command.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_install_parity.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_limits_tick_isolation.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_locking.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_mcp_server.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_msg_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_notify_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_onboarding_reinit.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_palette_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_pip_environment.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_posture_default.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_project_config.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_projects_registry.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_prompt_router.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_push.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_rebuild_guard.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_repo_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_rich_text_js.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_roles.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_routes_snapshot.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_commands.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_disabled_rules.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_escape_hatch.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_heal.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_safety_kill_switch.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scheduler.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scheduler_parsing.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scheduler_zombie.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_scratchpad.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_security.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_server_async.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_server_pure.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_services.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_session_context.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_session_created_by_env.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_session_ready.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_shared_dir_guard.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_shim_selfheal.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_skill_install.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_speech_tags.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_ssh.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_stt_backend.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_stt_cloud.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_stt_engine.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_task_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tasks.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tasks_cli.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_templating.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_terminal_resize.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tmux_template.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tts_engine_resolution.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tts_local.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_tunnels_probe.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_unattended_env_isolation.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_usage_limit.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_validation.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_voice_status.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_wiki.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_wiki_audit.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_window_cycling_binding.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree_branch_cleanup.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree_layout.py +0 -0
- {agentwire_dev-1.65.0 → agentwire_dev-1.65.2}/tests/unit/test_worktree_registry.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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.
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
f"
|
|
643
|
-
""
|
|
644
|
-
"
|
|
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
|
-
|
|
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
|
|
651
|
-
|
|
652
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
92
|
-
|
|
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
|
|
147
|
-
|
|
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
|
-
``
|
|
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
|
-
|
|
771
|
-
assert
|
|
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, "
|
|
963
|
-
lambda
|
|
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, "
|
|
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, "
|
|
985
|
-
lambda
|
|
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
|
# ---------------------------------------------------------------------------
|