agentirc-cli 0.18.0__tar.gz → 0.19.0__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.
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/CHANGELOG.md +19 -1
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/PKG-INFO +1 -1
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/daemon.py +89 -2
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/irc_transport.py +12 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/message_buffer.py +20 -1
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/daemon.py +89 -2
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/irc_transport.py +12 -0
- {agentirc_cli-0.18.0/agentirc/clients/codex → agentirc_cli-0.19.0/agentirc/clients/claude}/message_buffer.py +20 -1
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/daemon.py +89 -2
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/irc_transport.py +12 -0
- {agentirc_cli-0.18.0/agentirc/clients/claude → agentirc_cli-0.19.0/agentirc/clients/codex}/message_buffer.py +20 -1
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/daemon.py +89 -2
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/irc_transport.py +12 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/message_buffer.py +20 -1
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/commands.py +8 -0
- agentirc_cli-0.19.0/agentirc/protocol/extensions/threads.md +296 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/ircd.py +2 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/server_link.py +90 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skill.py +3 -0
- agentirc_cli-0.19.0/agentirc/server/skills/threads.py +598 -0
- agentirc_cli-0.19.0/agentirc/server/thread_store.py +50 -0
- agentirc_cli-0.19.0/docs/superpowers/plans/2026-04-02-conversation-threads.md +1885 -0
- agentirc_cli-0.19.0/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +326 -0
- agentirc_cli-0.19.0/docs/threads.md +200 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/daemon.py +96 -2
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/irc_transport.py +12 -0
- agentirc_cli-0.19.0/packages/agent-harness/message_buffer.py +65 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/pyproject.toml +1 -1
- agentirc_cli-0.19.0/tests/test_thread_buffer.py +56 -0
- agentirc_cli-0.19.0/tests/test_threads.py +383 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/uv.lock +1 -1
- agentirc_cli-0.18.0/packages/agent-harness/message_buffer.py +0 -46
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.gitignore +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/CLAUDE.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/CNAME +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/Gemfile +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/LICENSE +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/README.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/_config.yml +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/__main__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/cli.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/ipc.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/socket_server.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/supervisor.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/webhook.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/supervisor.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/ipc.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/socket_server.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/supervisor.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/webhook.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/ipc.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/webhook.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/credentials.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/learn_prompt.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/mesh_config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/observer.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/collector.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/model.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/renderer_text.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/renderer_web.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/web/style.css +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/persistence.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/tags.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/channel.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/remote_client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/room_store.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/rooms_util.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skills/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skills/rooms.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/skills/agentirc/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/agent-harness-spec.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/agentic-self-learn.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/ci.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/cli.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/acp/overview.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/codex-backend.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/copilot-backend.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/design.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/grow-your-agent.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/harness-conformance.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/ops-tooling.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/overview.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/rooms.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/10-grow-your-agent.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/index.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/claude-code/skills/agentirc/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/__init__.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/conftest.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_model.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_web.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_persistence.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_rooms.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_webhook.py +0 -0
|
@@ -4,11 +4,29 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## [0.
|
|
7
|
+
## [0.19.0] - 2026-04-03
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- Conversation threads — inline sub-conversations with [thread:name] prefix
|
|
13
|
+
- Breakout channel promotion from threads
|
|
14
|
+
- Thread-scoped agent context on @mention
|
|
15
|
+
- S2S federation for thread messages
|
|
16
|
+
- JSON persistence for threads across restarts
|
|
17
|
+
- Thread support in all 4 agent backends (claude, codex, copilot, acp)
|
|
18
|
+
|
|
19
|
+
## [0.18.0] - 2026-04-03
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Conversation threads — inline sub-conversations with [thread:name] prefix
|
|
25
|
+
- Breakout channel promotion from threads
|
|
26
|
+
- Thread-scoped agent context on @mention
|
|
27
|
+
- S2S federation for thread messages
|
|
28
|
+
- JSON persistence for threads across restarts
|
|
29
|
+
- Thread support in all 4 agent backends (claude, codex, copilot, acp)
|
|
12
30
|
- S2S link auto-reconnect with exponential backoff (5s to 120s)
|
|
13
31
|
- Declarative mesh.yaml configuration for multi-machine setup
|
|
14
32
|
- Cross-platform auto-start persistence (systemd, launchd, Windows schtasks)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentirc-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: 🌱 The space your agents deserve — an autonomous agent mesh where AI agents live, collaborate, and grow
|
|
5
5
|
Project-URL: Homepage, https://github.com/OriNachum/agentirc
|
|
6
6
|
Author: Ori Nachum
|
|
@@ -273,7 +273,7 @@ class ACPDaemon:
|
|
|
273
273
|
def _on_mention(self, target: str, sender: str, text: str) -> None:
|
|
274
274
|
"""Called by IRCTransport when the agent is @mentioned or DM'd.
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
When the mention is inside a thread, provides thread-scoped context.
|
|
277
277
|
"""
|
|
278
278
|
if self._paused:
|
|
279
279
|
return
|
|
@@ -282,7 +282,21 @@ class ACPDaemon:
|
|
|
282
282
|
# Enqueue relay target (FIFO matches prompt queue order)
|
|
283
283
|
self._mention_targets.append(target if target.startswith("#") else sender)
|
|
284
284
|
if target.startswith("#"):
|
|
285
|
-
|
|
285
|
+
import re
|
|
286
|
+
thread_match = re.match(r"^\[thread:([a-zA-Z0-9\-]+)\] ", text)
|
|
287
|
+
if thread_match and self._buffer:
|
|
288
|
+
thread_name = thread_match.group(1)
|
|
289
|
+
thread_msgs = self._buffer.read_thread(target, thread_name)
|
|
290
|
+
history = "\n".join(
|
|
291
|
+
f" <{m.nick}> {m.text}" for m in thread_msgs
|
|
292
|
+
)
|
|
293
|
+
prompt = (
|
|
294
|
+
f"[IRC @mention in {target}, thread:{thread_name}]\n"
|
|
295
|
+
f"Thread history:\n{history}\n"
|
|
296
|
+
f" <{sender}> {text}"
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
prompt = f"[IRC @mention in {target}] <{sender}> {text}"
|
|
286
300
|
else:
|
|
287
301
|
prompt = f"[IRC DM] <{sender}> {text}"
|
|
288
302
|
asyncio.create_task(self._agent_runner.send_prompt(prompt))
|
|
@@ -510,6 +524,21 @@ class ACPDaemon:
|
|
|
510
524
|
elif msg_type == "resume":
|
|
511
525
|
return await self._ipc_resume(req_id)
|
|
512
526
|
|
|
527
|
+
elif msg_type == "irc_thread_create":
|
|
528
|
+
return await self._ipc_irc_thread_create(req_id, msg)
|
|
529
|
+
|
|
530
|
+
elif msg_type == "irc_thread_reply":
|
|
531
|
+
return await self._ipc_irc_thread_reply(req_id, msg)
|
|
532
|
+
|
|
533
|
+
elif msg_type == "irc_threads":
|
|
534
|
+
return await self._ipc_irc_threads(req_id, msg)
|
|
535
|
+
|
|
536
|
+
elif msg_type == "irc_thread_close":
|
|
537
|
+
return await self._ipc_irc_thread_close(req_id, msg)
|
|
538
|
+
|
|
539
|
+
elif msg_type == "irc_thread_read":
|
|
540
|
+
return await self._ipc_irc_thread_read(req_id, msg)
|
|
541
|
+
|
|
513
542
|
elif msg_type == "shutdown":
|
|
514
543
|
asyncio.create_task(self._graceful_shutdown())
|
|
515
544
|
return make_response(req_id, ok=True)
|
|
@@ -642,6 +671,64 @@ class ACPDaemon:
|
|
|
642
671
|
await self._transport.part_channel(channel)
|
|
643
672
|
return make_response(req_id, ok=True)
|
|
644
673
|
|
|
674
|
+
async def _ipc_irc_thread_create(self, req_id: str, msg: dict) -> dict:
|
|
675
|
+
channel = msg.get("channel", "")
|
|
676
|
+
thread_name = msg.get("thread", "")
|
|
677
|
+
text = msg.get("message", "")
|
|
678
|
+
if not channel or not thread_name or not text:
|
|
679
|
+
return make_response(req_id, ok=False,
|
|
680
|
+
error="Missing 'channel', 'thread', or 'message'")
|
|
681
|
+
assert self._transport is not None
|
|
682
|
+
await self._transport.send_thread_create(channel, thread_name, text)
|
|
683
|
+
return make_response(req_id, ok=True)
|
|
684
|
+
|
|
685
|
+
async def _ipc_irc_thread_reply(self, req_id: str, msg: dict) -> dict:
|
|
686
|
+
channel = msg.get("channel", "")
|
|
687
|
+
thread_name = msg.get("thread", "")
|
|
688
|
+
text = msg.get("message", "")
|
|
689
|
+
if not channel or not thread_name or not text:
|
|
690
|
+
return make_response(req_id, ok=False,
|
|
691
|
+
error="Missing 'channel', 'thread', or 'message'")
|
|
692
|
+
assert self._transport is not None
|
|
693
|
+
await self._transport.send_thread_reply(channel, thread_name, text)
|
|
694
|
+
return make_response(req_id, ok=True)
|
|
695
|
+
|
|
696
|
+
async def _ipc_irc_threads(self, req_id: str, msg: dict) -> dict:
|
|
697
|
+
channel = msg.get("channel", "")
|
|
698
|
+
if not channel:
|
|
699
|
+
return make_response(req_id, ok=False, error="Missing 'channel'")
|
|
700
|
+
assert self._transport is not None
|
|
701
|
+
await self._transport.send_threads_list(channel)
|
|
702
|
+
return make_response(req_id, ok=True)
|
|
703
|
+
|
|
704
|
+
async def _ipc_irc_thread_close(self, req_id: str, msg: dict) -> dict:
|
|
705
|
+
channel = msg.get("channel", "")
|
|
706
|
+
thread_name = msg.get("thread", "")
|
|
707
|
+
summary = msg.get("summary", "")
|
|
708
|
+
if not channel or not thread_name:
|
|
709
|
+
return make_response(req_id, ok=False,
|
|
710
|
+
error="Missing 'channel' or 'thread'")
|
|
711
|
+
assert self._transport is not None
|
|
712
|
+
await self._transport.send_thread_close(channel, thread_name, summary)
|
|
713
|
+
return make_response(req_id, ok=True)
|
|
714
|
+
|
|
715
|
+
async def _ipc_irc_thread_read(self, req_id: str, msg: dict) -> dict:
|
|
716
|
+
channel = msg.get("channel", "")
|
|
717
|
+
thread_name = msg.get("thread", "")
|
|
718
|
+
limit = int(msg.get("limit", 50))
|
|
719
|
+
if not channel or not thread_name:
|
|
720
|
+
return make_response(req_id, ok=False,
|
|
721
|
+
error="Missing 'channel' or 'thread'")
|
|
722
|
+
assert self._buffer is not None
|
|
723
|
+
messages = self._buffer.read_thread(channel, thread_name, limit=limit)
|
|
724
|
+
return make_response(req_id, ok=True, data={
|
|
725
|
+
"messages": [
|
|
726
|
+
{"nick": m.nick, "text": m.text, "timestamp": m.timestamp,
|
|
727
|
+
"thread": m.thread}
|
|
728
|
+
for m in messages
|
|
729
|
+
]
|
|
730
|
+
})
|
|
731
|
+
|
|
645
732
|
async def _ipc_irc_channels(self, req_id: str) -> dict:
|
|
646
733
|
assert self._transport is not None
|
|
647
734
|
return make_response(req_id, ok=True, data={"channels": self._transport.channels})
|
|
@@ -73,6 +73,18 @@ class IRCTransport:
|
|
|
73
73
|
async def send_privmsg(self, target: str, text: str) -> None:
|
|
74
74
|
await self._send_raw(f"PRIVMSG {target} :{text}")
|
|
75
75
|
|
|
76
|
+
async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
|
|
77
|
+
await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
|
|
78
|
+
|
|
79
|
+
async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
|
|
80
|
+
await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
|
|
81
|
+
|
|
82
|
+
async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
|
|
83
|
+
await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
|
|
84
|
+
|
|
85
|
+
async def send_threads_list(self, channel: str) -> None:
|
|
86
|
+
await self._send_raw(f"THREADS {channel}")
|
|
87
|
+
|
|
76
88
|
async def join_channel(self, channel: str) -> None:
|
|
77
89
|
await self._send_raw(f"JOIN {channel}")
|
|
78
90
|
if channel not in self.channels:
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
import time
|
|
4
5
|
from collections import deque
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
|
|
8
|
+
_THREAD_PREFIX_RE = re.compile(r"^\[thread:([a-zA-Z0-9\-]+)\] ")
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
@dataclass
|
|
9
12
|
class BufferedMessage:
|
|
10
13
|
nick: str
|
|
11
14
|
text: str
|
|
12
15
|
timestamp: float
|
|
16
|
+
thread: str | None = None
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class MessageBuffer:
|
|
@@ -24,8 +28,13 @@ class MessageBuffer:
|
|
|
24
28
|
self._buffers[channel] = deque(maxlen=self.max_per_channel)
|
|
25
29
|
self._totals[channel] = 0
|
|
26
30
|
self._cursors[channel] = 0
|
|
31
|
+
thread = None
|
|
32
|
+
m = _THREAD_PREFIX_RE.match(text)
|
|
33
|
+
if m:
|
|
34
|
+
thread = m.group(1)
|
|
27
35
|
self._buffers[channel].append(
|
|
28
|
-
BufferedMessage(nick=nick, text=text, timestamp=time.time()
|
|
36
|
+
BufferedMessage(nick=nick, text=text, timestamp=time.time(),
|
|
37
|
+
thread=thread)
|
|
29
38
|
)
|
|
30
39
|
self._totals[channel] += 1
|
|
31
40
|
|
|
@@ -44,3 +53,13 @@ class MessageBuffer:
|
|
|
44
53
|
new_messages = new_messages[-limit:]
|
|
45
54
|
self._cursors[channel] = total
|
|
46
55
|
return new_messages
|
|
56
|
+
|
|
57
|
+
def read_thread(self, channel: str, thread_name: str,
|
|
58
|
+
limit: int = 50) -> list[BufferedMessage]:
|
|
59
|
+
buf = self._buffers.get(channel)
|
|
60
|
+
if not buf:
|
|
61
|
+
return []
|
|
62
|
+
matches = [m for m in buf if m.thread == thread_name]
|
|
63
|
+
if len(matches) > limit:
|
|
64
|
+
matches = matches[-limit:]
|
|
65
|
+
return matches
|
|
@@ -239,14 +239,28 @@ class AgentDaemon:
|
|
|
239
239
|
def _on_mention(self, target: str, sender: str, text: str) -> None:
|
|
240
240
|
"""Called by IRCTransport when the agent is @mentioned or DM'd.
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
When the mention is inside a thread, provides thread-scoped context.
|
|
243
243
|
"""
|
|
244
244
|
if self._paused:
|
|
245
245
|
return
|
|
246
246
|
if self._agent_runner and self._agent_runner.is_running():
|
|
247
247
|
self._last_activation = time.time()
|
|
248
248
|
if target.startswith("#"):
|
|
249
|
-
|
|
249
|
+
import re
|
|
250
|
+
thread_match = re.match(r"^\[thread:([a-zA-Z0-9\-]+)\] ", text)
|
|
251
|
+
if thread_match and self._buffer:
|
|
252
|
+
thread_name = thread_match.group(1)
|
|
253
|
+
thread_msgs = self._buffer.read_thread(target, thread_name)
|
|
254
|
+
history = "\n".join(
|
|
255
|
+
f" <{m.nick}> {m.text}" for m in thread_msgs
|
|
256
|
+
)
|
|
257
|
+
prompt = (
|
|
258
|
+
f"[IRC @mention in {target}, thread:{thread_name}]\n"
|
|
259
|
+
f"Thread history:\n{history}\n"
|
|
260
|
+
f" <{sender}> {text}"
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
prompt = f"[IRC @mention in {target}] <{sender}> {text}"
|
|
250
264
|
else:
|
|
251
265
|
prompt = f"[IRC DM] <{sender}> {text}"
|
|
252
266
|
asyncio.create_task(self._agent_runner.send_prompt(prompt))
|
|
@@ -451,6 +465,21 @@ class AgentDaemon:
|
|
|
451
465
|
elif msg_type == "resume":
|
|
452
466
|
return await self._ipc_resume(req_id)
|
|
453
467
|
|
|
468
|
+
elif msg_type == "irc_thread_create":
|
|
469
|
+
return await self._ipc_irc_thread_create(req_id, msg)
|
|
470
|
+
|
|
471
|
+
elif msg_type == "irc_thread_reply":
|
|
472
|
+
return await self._ipc_irc_thread_reply(req_id, msg)
|
|
473
|
+
|
|
474
|
+
elif msg_type == "irc_threads":
|
|
475
|
+
return await self._ipc_irc_threads(req_id, msg)
|
|
476
|
+
|
|
477
|
+
elif msg_type == "irc_thread_close":
|
|
478
|
+
return await self._ipc_irc_thread_close(req_id, msg)
|
|
479
|
+
|
|
480
|
+
elif msg_type == "irc_thread_read":
|
|
481
|
+
return await self._ipc_irc_thread_read(req_id, msg)
|
|
482
|
+
|
|
454
483
|
elif msg_type == "shutdown":
|
|
455
484
|
asyncio.create_task(self._graceful_shutdown())
|
|
456
485
|
return make_response(req_id, ok=True)
|
|
@@ -580,6 +609,64 @@ class AgentDaemon:
|
|
|
580
609
|
await self._transport.part_channel(channel)
|
|
581
610
|
return make_response(req_id, ok=True)
|
|
582
611
|
|
|
612
|
+
async def _ipc_irc_thread_create(self, req_id: str, msg: dict) -> dict:
|
|
613
|
+
channel = msg.get("channel", "")
|
|
614
|
+
thread_name = msg.get("thread", "")
|
|
615
|
+
text = msg.get("message", "")
|
|
616
|
+
if not channel or not thread_name or not text:
|
|
617
|
+
return make_response(req_id, ok=False,
|
|
618
|
+
error="Missing 'channel', 'thread', or 'message'")
|
|
619
|
+
assert self._transport is not None
|
|
620
|
+
await self._transport.send_thread_create(channel, thread_name, text)
|
|
621
|
+
return make_response(req_id, ok=True)
|
|
622
|
+
|
|
623
|
+
async def _ipc_irc_thread_reply(self, req_id: str, msg: dict) -> dict:
|
|
624
|
+
channel = msg.get("channel", "")
|
|
625
|
+
thread_name = msg.get("thread", "")
|
|
626
|
+
text = msg.get("message", "")
|
|
627
|
+
if not channel or not thread_name or not text:
|
|
628
|
+
return make_response(req_id, ok=False,
|
|
629
|
+
error="Missing 'channel', 'thread', or 'message'")
|
|
630
|
+
assert self._transport is not None
|
|
631
|
+
await self._transport.send_thread_reply(channel, thread_name, text)
|
|
632
|
+
return make_response(req_id, ok=True)
|
|
633
|
+
|
|
634
|
+
async def _ipc_irc_threads(self, req_id: str, msg: dict) -> dict:
|
|
635
|
+
channel = msg.get("channel", "")
|
|
636
|
+
if not channel:
|
|
637
|
+
return make_response(req_id, ok=False, error="Missing 'channel'")
|
|
638
|
+
assert self._transport is not None
|
|
639
|
+
await self._transport.send_threads_list(channel)
|
|
640
|
+
return make_response(req_id, ok=True)
|
|
641
|
+
|
|
642
|
+
async def _ipc_irc_thread_close(self, req_id: str, msg: dict) -> dict:
|
|
643
|
+
channel = msg.get("channel", "")
|
|
644
|
+
thread_name = msg.get("thread", "")
|
|
645
|
+
summary = msg.get("summary", "")
|
|
646
|
+
if not channel or not thread_name:
|
|
647
|
+
return make_response(req_id, ok=False,
|
|
648
|
+
error="Missing 'channel' or 'thread'")
|
|
649
|
+
assert self._transport is not None
|
|
650
|
+
await self._transport.send_thread_close(channel, thread_name, summary)
|
|
651
|
+
return make_response(req_id, ok=True)
|
|
652
|
+
|
|
653
|
+
async def _ipc_irc_thread_read(self, req_id: str, msg: dict) -> dict:
|
|
654
|
+
channel = msg.get("channel", "")
|
|
655
|
+
thread_name = msg.get("thread", "")
|
|
656
|
+
limit = int(msg.get("limit", 50))
|
|
657
|
+
if not channel or not thread_name:
|
|
658
|
+
return make_response(req_id, ok=False,
|
|
659
|
+
error="Missing 'channel' or 'thread'")
|
|
660
|
+
assert self._buffer is not None
|
|
661
|
+
messages = self._buffer.read_thread(channel, thread_name, limit=limit)
|
|
662
|
+
return make_response(req_id, ok=True, data={
|
|
663
|
+
"messages": [
|
|
664
|
+
{"nick": m.nick, "text": m.text, "timestamp": m.timestamp,
|
|
665
|
+
"thread": m.thread}
|
|
666
|
+
for m in messages
|
|
667
|
+
]
|
|
668
|
+
})
|
|
669
|
+
|
|
583
670
|
async def _ipc_irc_channels(self, req_id: str) -> dict:
|
|
584
671
|
assert self._transport is not None
|
|
585
672
|
return make_response(req_id, ok=True, data={"channels": self._transport.channels})
|
|
@@ -73,6 +73,18 @@ class IRCTransport:
|
|
|
73
73
|
async def send_privmsg(self, target: str, text: str) -> None:
|
|
74
74
|
await self._send_raw(f"PRIVMSG {target} :{text}")
|
|
75
75
|
|
|
76
|
+
async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
|
|
77
|
+
await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
|
|
78
|
+
|
|
79
|
+
async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
|
|
80
|
+
await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
|
|
81
|
+
|
|
82
|
+
async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
|
|
83
|
+
await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
|
|
84
|
+
|
|
85
|
+
async def send_threads_list(self, channel: str) -> None:
|
|
86
|
+
await self._send_raw(f"THREADS {channel}")
|
|
87
|
+
|
|
76
88
|
async def join_channel(self, channel: str) -> None:
|
|
77
89
|
await self._send_raw(f"JOIN {channel}")
|
|
78
90
|
if channel not in self.channels:
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
import time
|
|
4
5
|
from collections import deque
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
|
|
8
|
+
_THREAD_PREFIX_RE = re.compile(r"^\[thread:([a-zA-Z0-9\-]+)\] ")
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
@dataclass
|
|
9
12
|
class BufferedMessage:
|
|
10
13
|
nick: str
|
|
11
14
|
text: str
|
|
12
15
|
timestamp: float
|
|
16
|
+
thread: str | None = None
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class MessageBuffer:
|
|
@@ -24,8 +28,13 @@ class MessageBuffer:
|
|
|
24
28
|
self._buffers[channel] = deque(maxlen=self.max_per_channel)
|
|
25
29
|
self._totals[channel] = 0
|
|
26
30
|
self._cursors[channel] = 0
|
|
31
|
+
thread = None
|
|
32
|
+
m = _THREAD_PREFIX_RE.match(text)
|
|
33
|
+
if m:
|
|
34
|
+
thread = m.group(1)
|
|
27
35
|
self._buffers[channel].append(
|
|
28
|
-
BufferedMessage(nick=nick, text=text, timestamp=time.time()
|
|
36
|
+
BufferedMessage(nick=nick, text=text, timestamp=time.time(),
|
|
37
|
+
thread=thread)
|
|
29
38
|
)
|
|
30
39
|
self._totals[channel] += 1
|
|
31
40
|
|
|
@@ -44,3 +53,13 @@ class MessageBuffer:
|
|
|
44
53
|
new_messages = new_messages[-limit:]
|
|
45
54
|
self._cursors[channel] = total
|
|
46
55
|
return new_messages
|
|
56
|
+
|
|
57
|
+
def read_thread(self, channel: str, thread_name: str,
|
|
58
|
+
limit: int = 50) -> list[BufferedMessage]:
|
|
59
|
+
buf = self._buffers.get(channel)
|
|
60
|
+
if not buf:
|
|
61
|
+
return []
|
|
62
|
+
matches = [m for m in buf if m.thread == thread_name]
|
|
63
|
+
if len(matches) > limit:
|
|
64
|
+
matches = matches[-limit:]
|
|
65
|
+
return matches
|
|
@@ -249,7 +249,7 @@ class CodexDaemon:
|
|
|
249
249
|
def _on_mention(self, target: str, sender: str, text: str) -> None:
|
|
250
250
|
"""Called by IRCTransport when the agent is @mentioned or DM'd.
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
When the mention is inside a thread, provides thread-scoped context.
|
|
253
253
|
"""
|
|
254
254
|
if self._paused:
|
|
255
255
|
return
|
|
@@ -258,7 +258,21 @@ class CodexDaemon:
|
|
|
258
258
|
# Enqueue relay target (FIFO matches prompt queue order)
|
|
259
259
|
self._mention_targets.append(target if target.startswith("#") else sender)
|
|
260
260
|
if target.startswith("#"):
|
|
261
|
-
|
|
261
|
+
import re
|
|
262
|
+
thread_match = re.match(r"^\[thread:([a-zA-Z0-9\-]+)\] ", text)
|
|
263
|
+
if thread_match and self._buffer:
|
|
264
|
+
thread_name = thread_match.group(1)
|
|
265
|
+
thread_msgs = self._buffer.read_thread(target, thread_name)
|
|
266
|
+
history = "\n".join(
|
|
267
|
+
f" <{m.nick}> {m.text}" for m in thread_msgs
|
|
268
|
+
)
|
|
269
|
+
prompt = (
|
|
270
|
+
f"[IRC @mention in {target}, thread:{thread_name}]\n"
|
|
271
|
+
f"Thread history:\n{history}\n"
|
|
272
|
+
f" <{sender}> {text}"
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
prompt = f"[IRC @mention in {target}] <{sender}> {text}"
|
|
262
276
|
else:
|
|
263
277
|
prompt = f"[IRC DM] <{sender}> {text}"
|
|
264
278
|
asyncio.create_task(self._agent_runner.send_prompt(prompt))
|
|
@@ -478,6 +492,21 @@ class CodexDaemon:
|
|
|
478
492
|
elif msg_type == "resume":
|
|
479
493
|
return await self._ipc_resume(req_id)
|
|
480
494
|
|
|
495
|
+
elif msg_type == "irc_thread_create":
|
|
496
|
+
return await self._ipc_irc_thread_create(req_id, msg)
|
|
497
|
+
|
|
498
|
+
elif msg_type == "irc_thread_reply":
|
|
499
|
+
return await self._ipc_irc_thread_reply(req_id, msg)
|
|
500
|
+
|
|
501
|
+
elif msg_type == "irc_threads":
|
|
502
|
+
return await self._ipc_irc_threads(req_id, msg)
|
|
503
|
+
|
|
504
|
+
elif msg_type == "irc_thread_close":
|
|
505
|
+
return await self._ipc_irc_thread_close(req_id, msg)
|
|
506
|
+
|
|
507
|
+
elif msg_type == "irc_thread_read":
|
|
508
|
+
return await self._ipc_irc_thread_read(req_id, msg)
|
|
509
|
+
|
|
481
510
|
elif msg_type == "shutdown":
|
|
482
511
|
asyncio.create_task(self._graceful_shutdown())
|
|
483
512
|
return make_response(req_id, ok=True)
|
|
@@ -610,6 +639,64 @@ class CodexDaemon:
|
|
|
610
639
|
await self._transport.part_channel(channel)
|
|
611
640
|
return make_response(req_id, ok=True)
|
|
612
641
|
|
|
642
|
+
async def _ipc_irc_thread_create(self, req_id: str, msg: dict) -> dict:
|
|
643
|
+
channel = msg.get("channel", "")
|
|
644
|
+
thread_name = msg.get("thread", "")
|
|
645
|
+
text = msg.get("message", "")
|
|
646
|
+
if not channel or not thread_name or not text:
|
|
647
|
+
return make_response(req_id, ok=False,
|
|
648
|
+
error="Missing 'channel', 'thread', or 'message'")
|
|
649
|
+
assert self._transport is not None
|
|
650
|
+
await self._transport.send_thread_create(channel, thread_name, text)
|
|
651
|
+
return make_response(req_id, ok=True)
|
|
652
|
+
|
|
653
|
+
async def _ipc_irc_thread_reply(self, req_id: str, msg: dict) -> dict:
|
|
654
|
+
channel = msg.get("channel", "")
|
|
655
|
+
thread_name = msg.get("thread", "")
|
|
656
|
+
text = msg.get("message", "")
|
|
657
|
+
if not channel or not thread_name or not text:
|
|
658
|
+
return make_response(req_id, ok=False,
|
|
659
|
+
error="Missing 'channel', 'thread', or 'message'")
|
|
660
|
+
assert self._transport is not None
|
|
661
|
+
await self._transport.send_thread_reply(channel, thread_name, text)
|
|
662
|
+
return make_response(req_id, ok=True)
|
|
663
|
+
|
|
664
|
+
async def _ipc_irc_threads(self, req_id: str, msg: dict) -> dict:
|
|
665
|
+
channel = msg.get("channel", "")
|
|
666
|
+
if not channel:
|
|
667
|
+
return make_response(req_id, ok=False, error="Missing 'channel'")
|
|
668
|
+
assert self._transport is not None
|
|
669
|
+
await self._transport.send_threads_list(channel)
|
|
670
|
+
return make_response(req_id, ok=True)
|
|
671
|
+
|
|
672
|
+
async def _ipc_irc_thread_close(self, req_id: str, msg: dict) -> dict:
|
|
673
|
+
channel = msg.get("channel", "")
|
|
674
|
+
thread_name = msg.get("thread", "")
|
|
675
|
+
summary = msg.get("summary", "")
|
|
676
|
+
if not channel or not thread_name:
|
|
677
|
+
return make_response(req_id, ok=False,
|
|
678
|
+
error="Missing 'channel' or 'thread'")
|
|
679
|
+
assert self._transport is not None
|
|
680
|
+
await self._transport.send_thread_close(channel, thread_name, summary)
|
|
681
|
+
return make_response(req_id, ok=True)
|
|
682
|
+
|
|
683
|
+
async def _ipc_irc_thread_read(self, req_id: str, msg: dict) -> dict:
|
|
684
|
+
channel = msg.get("channel", "")
|
|
685
|
+
thread_name = msg.get("thread", "")
|
|
686
|
+
limit = int(msg.get("limit", 50))
|
|
687
|
+
if not channel or not thread_name:
|
|
688
|
+
return make_response(req_id, ok=False,
|
|
689
|
+
error="Missing 'channel' or 'thread'")
|
|
690
|
+
assert self._buffer is not None
|
|
691
|
+
messages = self._buffer.read_thread(channel, thread_name, limit=limit)
|
|
692
|
+
return make_response(req_id, ok=True, data={
|
|
693
|
+
"messages": [
|
|
694
|
+
{"nick": m.nick, "text": m.text, "timestamp": m.timestamp,
|
|
695
|
+
"thread": m.thread}
|
|
696
|
+
for m in messages
|
|
697
|
+
]
|
|
698
|
+
})
|
|
699
|
+
|
|
613
700
|
async def _ipc_irc_channels(self, req_id: str) -> dict:
|
|
614
701
|
assert self._transport is not None
|
|
615
702
|
return make_response(req_id, ok=True, data={"channels": self._transport.channels})
|
|
@@ -73,6 +73,18 @@ class IRCTransport:
|
|
|
73
73
|
async def send_privmsg(self, target: str, text: str) -> None:
|
|
74
74
|
await self._send_raw(f"PRIVMSG {target} :{text}")
|
|
75
75
|
|
|
76
|
+
async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
|
|
77
|
+
await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
|
|
78
|
+
|
|
79
|
+
async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
|
|
80
|
+
await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
|
|
81
|
+
|
|
82
|
+
async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
|
|
83
|
+
await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
|
|
84
|
+
|
|
85
|
+
async def send_threads_list(self, channel: str) -> None:
|
|
86
|
+
await self._send_raw(f"THREADS {channel}")
|
|
87
|
+
|
|
76
88
|
async def join_channel(self, channel: str) -> None:
|
|
77
89
|
await self._send_raw(f"JOIN {channel}")
|
|
78
90
|
if channel not in self.channels:
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
import time
|
|
4
5
|
from collections import deque
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
|
|
8
|
+
_THREAD_PREFIX_RE = re.compile(r"^\[thread:([a-zA-Z0-9\-]+)\] ")
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
@dataclass
|
|
9
12
|
class BufferedMessage:
|
|
10
13
|
nick: str
|
|
11
14
|
text: str
|
|
12
15
|
timestamp: float
|
|
16
|
+
thread: str | None = None
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class MessageBuffer:
|
|
@@ -24,8 +28,13 @@ class MessageBuffer:
|
|
|
24
28
|
self._buffers[channel] = deque(maxlen=self.max_per_channel)
|
|
25
29
|
self._totals[channel] = 0
|
|
26
30
|
self._cursors[channel] = 0
|
|
31
|
+
thread = None
|
|
32
|
+
m = _THREAD_PREFIX_RE.match(text)
|
|
33
|
+
if m:
|
|
34
|
+
thread = m.group(1)
|
|
27
35
|
self._buffers[channel].append(
|
|
28
|
-
BufferedMessage(nick=nick, text=text, timestamp=time.time()
|
|
36
|
+
BufferedMessage(nick=nick, text=text, timestamp=time.time(),
|
|
37
|
+
thread=thread)
|
|
29
38
|
)
|
|
30
39
|
self._totals[channel] += 1
|
|
31
40
|
|
|
@@ -44,3 +53,13 @@ class MessageBuffer:
|
|
|
44
53
|
new_messages = new_messages[-limit:]
|
|
45
54
|
self._cursors[channel] = total
|
|
46
55
|
return new_messages
|
|
56
|
+
|
|
57
|
+
def read_thread(self, channel: str, thread_name: str,
|
|
58
|
+
limit: int = 50) -> list[BufferedMessage]:
|
|
59
|
+
buf = self._buffers.get(channel)
|
|
60
|
+
if not buf:
|
|
61
|
+
return []
|
|
62
|
+
matches = [m for m in buf if m.thread == thread_name]
|
|
63
|
+
if len(matches) > limit:
|
|
64
|
+
matches = matches[-limit:]
|
|
65
|
+
return matches
|