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.
Files changed (241) hide show
  1. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/CHANGELOG.md +19 -1
  2. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/PKG-INFO +1 -1
  3. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/daemon.py +89 -2
  4. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/irc_transport.py +12 -0
  5. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/message_buffer.py +20 -1
  6. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/daemon.py +89 -2
  7. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/irc_transport.py +12 -0
  8. {agentirc_cli-0.18.0/agentirc/clients/codex → agentirc_cli-0.19.0/agentirc/clients/claude}/message_buffer.py +20 -1
  9. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/daemon.py +89 -2
  10. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/irc_transport.py +12 -0
  11. {agentirc_cli-0.18.0/agentirc/clients/claude → agentirc_cli-0.19.0/agentirc/clients/codex}/message_buffer.py +20 -1
  12. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/daemon.py +89 -2
  13. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/irc_transport.py +12 -0
  14. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/message_buffer.py +20 -1
  15. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/commands.py +8 -0
  16. agentirc_cli-0.19.0/agentirc/protocol/extensions/threads.md +296 -0
  17. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/ircd.py +2 -0
  18. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/server_link.py +90 -0
  19. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skill.py +3 -0
  20. agentirc_cli-0.19.0/agentirc/server/skills/threads.py +598 -0
  21. agentirc_cli-0.19.0/agentirc/server/thread_store.py +50 -0
  22. agentirc_cli-0.19.0/docs/superpowers/plans/2026-04-02-conversation-threads.md +1885 -0
  23. agentirc_cli-0.19.0/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +326 -0
  24. agentirc_cli-0.19.0/docs/threads.md +200 -0
  25. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/daemon.py +96 -2
  26. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/irc_transport.py +12 -0
  27. agentirc_cli-0.19.0/packages/agent-harness/message_buffer.py +65 -0
  28. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/pyproject.toml +1 -1
  29. agentirc_cli-0.19.0/tests/test_thread_buffer.py +56 -0
  30. agentirc_cli-0.19.0/tests/test_threads.py +383 -0
  31. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/uv.lock +1 -1
  32. agentirc_cli-0.18.0/packages/agent-harness/message_buffer.py +0 -46
  33. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.claude/skills/pr-review/SKILL.md +0 -0
  34. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.github/workflows/pages.yml +0 -0
  35. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.github/workflows/publish.yml +0 -0
  36. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.github/workflows/tests.yml +0 -0
  37. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.gitignore +0 -0
  38. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.markdownlint-cli2.yaml +0 -0
  39. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/.pr_agent.toml +0 -0
  40. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/CLAUDE.md +0 -0
  41. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/CNAME +0 -0
  42. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/Gemfile +0 -0
  43. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/Gemfile.lock +0 -0
  44. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/LICENSE +0 -0
  45. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/README.md +0 -0
  46. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/_config.yml +0 -0
  47. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/_sass/color_schemes/anthropic.scss +0 -0
  48. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/_sass/custom/custom.scss +0 -0
  49. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/__init__.py +0 -0
  50. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/__main__.py +0 -0
  51. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/cli.py +0 -0
  52. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/__init__.py +0 -0
  53. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/__init__.py +0 -0
  54. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/agent_runner.py +0 -0
  55. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/config.py +0 -0
  56. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/ipc.py +0 -0
  57. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/skill/SKILL.md +0 -0
  58. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/skill/__init__.py +0 -0
  59. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/skill/irc_client.py +0 -0
  60. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/socket_server.py +0 -0
  61. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/supervisor.py +0 -0
  62. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/acp/webhook.py +0 -0
  63. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/__init__.py +0 -0
  64. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/__main__.py +0 -0
  65. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/agent_runner.py +0 -0
  66. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/config.py +0 -0
  67. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/ipc.py +0 -0
  68. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
  69. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/skill/__init__.py +0 -0
  70. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
  71. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/socket_server.py +0 -0
  72. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/supervisor.py +0 -0
  73. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/claude/webhook.py +0 -0
  74. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/__init__.py +0 -0
  75. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/agent_runner.py +0 -0
  76. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/config.py +0 -0
  77. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/ipc.py +0 -0
  78. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
  79. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/skill/__init__.py +0 -0
  80. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
  81. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/socket_server.py +0 -0
  82. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/supervisor.py +0 -0
  83. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/codex/webhook.py +0 -0
  84. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/__init__.py +0 -0
  85. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/agent_runner.py +0 -0
  86. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/config.py +0 -0
  87. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/ipc.py +0 -0
  88. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
  89. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
  90. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
  91. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/socket_server.py +0 -0
  92. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/supervisor.py +0 -0
  93. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/clients/copilot/webhook.py +0 -0
  94. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/credentials.py +0 -0
  95. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/learn_prompt.py +0 -0
  96. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/mesh_config.py +0 -0
  97. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/observer.py +0 -0
  98. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/__init__.py +0 -0
  99. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/collector.py +0 -0
  100. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/model.py +0 -0
  101. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/renderer_text.py +0 -0
  102. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/renderer_web.py +0 -0
  103. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/overview/web/style.css +0 -0
  104. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/persistence.py +0 -0
  105. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/pidfile.py +0 -0
  106. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/__init__.py +0 -0
  107. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/federation.md +0 -0
  108. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/history.md +0 -0
  109. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/rooms.md +0 -0
  110. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/extensions/tags.md +0 -0
  111. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/message.py +0 -0
  112. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/protocol-index.md +0 -0
  113. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/protocol/replies.py +0 -0
  114. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/__init__.py +0 -0
  115. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/__main__.py +0 -0
  116. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/channel.py +0 -0
  117. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/client.py +0 -0
  118. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/config.py +0 -0
  119. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/remote_client.py +0 -0
  120. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/room_store.py +0 -0
  121. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/rooms_util.py +0 -0
  122. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skills/__init__.py +0 -0
  123. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skills/history.py +0 -0
  124. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/server/skills/rooms.py +0 -0
  125. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/agentirc/skills/agentirc/SKILL.md +0 -0
  126. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/agent-client.md +0 -0
  127. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/agent-harness-spec.md +0 -0
  128. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/agentic-self-learn.md +0 -0
  129. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/ci.md +0 -0
  130. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/cli.md +0 -0
  131. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/acp/overview.md +0 -0
  132. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/configuration.md +0 -0
  133. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/context-management.md +0 -0
  134. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/irc-tools.md +0 -0
  135. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/overview.md +0 -0
  136. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/setup.md +0 -0
  137. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/supervisor.md +0 -0
  138. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/claude/webhooks.md +0 -0
  139. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/configuration.md +0 -0
  140. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/context-management.md +0 -0
  141. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/irc-tools.md +0 -0
  142. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/overview.md +0 -0
  143. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/setup.md +0 -0
  144. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/supervisor.md +0 -0
  145. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/codex/webhooks.md +0 -0
  146. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/configuration.md +0 -0
  147. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/context-management.md +0 -0
  148. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/irc-tools.md +0 -0
  149. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/overview.md +0 -0
  150. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/setup.md +0 -0
  151. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/supervisor.md +0 -0
  152. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/clients/copilot/webhooks.md +0 -0
  153. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/codex-backend.md +0 -0
  154. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/copilot-backend.md +0 -0
  155. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/design.md +0 -0
  156. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/docs-site.md +0 -0
  157. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/getting-started.md +0 -0
  158. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/grow-your-agent.md +0 -0
  159. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/harness-conformance.md +0 -0
  160. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer1-core-irc.md +0 -0
  161. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer2-attention.md +0 -0
  162. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer3-skills.md +0 -0
  163. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer4-federation.md +0 -0
  164. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/layer5-agent-harness.md +0 -0
  165. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/ops-tooling.md +0 -0
  166. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/overview.md +0 -0
  167. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/publishing.md +0 -0
  168. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  169. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/rooms.md +0 -0
  170. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/server-architecture.md +0 -0
  171. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  172. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  173. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
  174. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
  175. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  176. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  177. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
  178. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
  179. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/01-pair-programming.md +0 -0
  180. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
  181. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
  182. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
  183. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/05-the-observer.md +0 -0
  184. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/06-cross-server-ops.md +0 -0
  185. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
  186. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/08-apps-as-agents.md +0 -0
  187. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/09-research-swarm.md +0 -0
  188. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases/10-grow-your-agent.md +0 -0
  189. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/docs/use-cases-index.md +0 -0
  190. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/index.md +0 -0
  191. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/README.md +0 -0
  192. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/config.py +0 -0
  193. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/ipc.py +0 -0
  194. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/skill/SKILL.md +0 -0
  195. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/skill/irc_client.py +0 -0
  196. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/socket_server.py +0 -0
  197. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/packages/agent-harness/webhook.py +0 -0
  198. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  199. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/claude-code/skills/agentirc/SKILL.md +0 -0
  200. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  201. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
  202. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/__init__.py +0 -0
  203. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/conftest.py +0 -0
  204. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_acp_daemon.py +0 -0
  205. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_agent_runner.py +0 -0
  206. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_channel.py +0 -0
  207. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_codex_daemon.py +0 -0
  208. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_connection.py +0 -0
  209. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_copilot_daemon.py +0 -0
  210. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_daemon.py +0 -0
  211. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_daemon_config.py +0 -0
  212. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_daemon_ipc.py +0 -0
  213. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_discovery.py +0 -0
  214. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_federation.py +0 -0
  215. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_history.py +0 -0
  216. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_integration_layer5.py +0 -0
  217. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_ipc.py +0 -0
  218. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_irc_transport.py +0 -0
  219. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_link_reconnect.py +0 -0
  220. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_mentions.py +0 -0
  221. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_mesh_config.py +0 -0
  222. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_message.py +0 -0
  223. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_message_buffer.py +0 -0
  224. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_messaging.py +0 -0
  225. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_modes.py +0 -0
  226. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_cli.py +0 -0
  227. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_collector.py +0 -0
  228. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_model.py +0 -0
  229. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_renderer.py +0 -0
  230. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_overview_web.py +0 -0
  231. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_persistence.py +0 -0
  232. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_room_persistence.py +0 -0
  233. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_rooms.py +0 -0
  234. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_rooms_federation.py +0 -0
  235. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_rooms_integration.py +0 -0
  236. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_setup_update_cli.py +0 -0
  237. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_skill_client.py +0 -0
  238. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_skills.py +0 -0
  239. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_socket_server.py +0 -0
  240. {agentirc_cli-0.18.0 → agentirc_cli-0.19.0}/tests/test_supervisor.py +0 -0
  241. {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.18.0] - 2026-04-02
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.18.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
- Formats a prompt and enqueues it so the ACP session picks it up.
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
- prompt = f"[IRC @mention in {target}] <{sender}> {text}"
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
- Formats a prompt and enqueues it so the SDK session picks it up.
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
- prompt = f"[IRC @mention in {target}] <{sender}> {text}"
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
- Formats a prompt and enqueues it so the Codex session picks it up.
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
- prompt = f"[IRC @mention in {target}] <{sender}> {text}"
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