agentirc-cli 4.3.7__tar.gz → 4.4.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 (319) hide show
  1. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/CHANGELOG.md +14 -0
  2. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/PKG-INFO +1 -1
  3. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/server.py +23 -4
  4. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/irc_transport.py +14 -4
  5. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/irc_transport.py +14 -4
  6. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/irc_transport.py +14 -4
  7. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/irc_transport.py +14 -4
  8. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/history.md +6 -1
  9. agentirc_cli-4.4.0/culture/server/history_store.py +91 -0
  10. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/history.py +48 -1
  11. agentirc_cli-4.4.0/docs/persistent-history.md +61 -0
  12. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/irc_transport.py +14 -4
  13. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/pyproject.toml +1 -1
  14. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_history.py +178 -0
  15. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_irc_transport.py +55 -0
  16. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/uv.lock +1 -1
  17. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.claude/skills/pr-review/SKILL.md +0 -0
  18. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.claude/skills/run-tests/SKILL.md +0 -0
  19. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  20. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.flake8 +0 -0
  21. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/pages.yml +0 -0
  22. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/publish.yml +0 -0
  23. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/security-checks.yml +0 -0
  24. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/tests.yml +0 -0
  25. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.gitignore +0 -0
  26. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.markdownlint-cli2.yaml +0 -0
  27. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.pr_agent.toml +0 -0
  28. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.pre-commit-config.yaml +0 -0
  29. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.pylintrc +0 -0
  30. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/CLAUDE.md +0 -0
  31. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/CNAME +0 -0
  32. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/Gemfile +0 -0
  33. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/Gemfile.lock +0 -0
  34. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/LICENSE +0 -0
  35. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/README.md +0 -0
  36. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/SECURITY.md +0 -0
  37. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/_config.yml +0 -0
  38. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/_sass/color_schemes/anthropic.scss +0 -0
  39. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/_sass/custom/custom.scss +0 -0
  40. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/__init__.py +0 -0
  41. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/__main__.py +0 -0
  42. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/aio.py +0 -0
  43. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/__init__.py +0 -0
  44. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/bot.py +0 -0
  45. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/bot_manager.py +0 -0
  46. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/config.py +0 -0
  47. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/http_listener.py +0 -0
  48. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/template_engine.py +0 -0
  49. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/virtual_client.py +0 -0
  50. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/__init__.py +0 -0
  51. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/agent.py +0 -0
  52. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/bot.py +0 -0
  53. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/channel.py +0 -0
  54. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/mesh.py +0 -0
  55. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/__init__.py +0 -0
  56. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/constants.py +0 -0
  57. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/display.py +0 -0
  58. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/formatting.py +0 -0
  59. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/ipc.py +0 -0
  60. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/mesh.py +0 -0
  61. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/process.py +0 -0
  62. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/skills.py +0 -0
  63. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/__init__.py +0 -0
  64. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/__init__.py +0 -0
  65. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/agent_runner.py +0 -0
  66. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/config.py +0 -0
  67. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/daemon.py +0 -0
  68. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/ipc.py +0 -0
  69. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/message_buffer.py +0 -0
  70. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/skill/SKILL.md +0 -0
  71. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/skill/__init__.py +0 -0
  72. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/skill/irc_client.py +0 -0
  73. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/socket_server.py +0 -0
  74. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/supervisor.py +0 -0
  75. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/webhook.py +0 -0
  76. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/__init__.py +0 -0
  77. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/__main__.py +0 -0
  78. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/agent_runner.py +0 -0
  79. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/config.py +0 -0
  80. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/daemon.py +0 -0
  81. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/ipc.py +0 -0
  82. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/message_buffer.py +0 -0
  83. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/skill/SKILL.md +0 -0
  84. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/skill/__init__.py +0 -0
  85. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/skill/irc_client.py +0 -0
  86. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/socket_server.py +0 -0
  87. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/supervisor.py +0 -0
  88. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/webhook.py +0 -0
  89. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/__init__.py +0 -0
  90. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/agent_runner.py +0 -0
  91. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/config.py +0 -0
  92. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/daemon.py +0 -0
  93. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/ipc.py +0 -0
  94. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/message_buffer.py +0 -0
  95. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/skill/SKILL.md +0 -0
  96. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/skill/__init__.py +0 -0
  97. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/skill/irc_client.py +0 -0
  98. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/socket_server.py +0 -0
  99. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/supervisor.py +0 -0
  100. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/webhook.py +0 -0
  101. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/__init__.py +0 -0
  102. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/agent_runner.py +0 -0
  103. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/config.py +0 -0
  104. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/daemon.py +0 -0
  105. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/ipc.py +0 -0
  106. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/message_buffer.py +0 -0
  107. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/skill/SKILL.md +0 -0
  108. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/skill/__init__.py +0 -0
  109. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/skill/irc_client.py +0 -0
  110. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/socket_server.py +0 -0
  111. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/supervisor.py +0 -0
  112. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/webhook.py +0 -0
  113. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/__init__.py +0 -0
  114. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/app.py +0 -0
  115. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/client.py +0 -0
  116. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/commands.py +0 -0
  117. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/__init__.py +0 -0
  118. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/chat.py +0 -0
  119. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/info_panel.py +0 -0
  120. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/sidebar.py +0 -0
  121. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/credentials.py +0 -0
  122. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/formatting.py +0 -0
  123. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/learn_prompt.py +0 -0
  124. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/mesh_config.py +0 -0
  125. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/observer.py +0 -0
  126. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/__init__.py +0 -0
  127. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/collector.py +0 -0
  128. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/model.py +0 -0
  129. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/renderer_text.py +0 -0
  130. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/renderer_web.py +0 -0
  131. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/web/style.css +0 -0
  132. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/persistence.py +0 -0
  133. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/pidfile.py +0 -0
  134. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/__init__.py +0 -0
  135. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/commands.py +0 -0
  136. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/federation.md +0 -0
  137. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/icons.md +0 -0
  138. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/rooms.md +0 -0
  139. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/tags.md +0 -0
  140. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/threads.md +0 -0
  141. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/message.py +0 -0
  142. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/protocol-index.md +0 -0
  143. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/replies.py +0 -0
  144. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/__init__.py +0 -0
  145. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/__main__.py +0 -0
  146. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/channel.py +0 -0
  147. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/client.py +0 -0
  148. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/config.py +0 -0
  149. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/ircd.py +0 -0
  150. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/remote_client.py +0 -0
  151. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/room_store.py +0 -0
  152. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/rooms_util.py +0 -0
  153. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/server_link.py +0 -0
  154. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skill.py +0 -0
  155. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/__init__.py +0 -0
  156. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/icon.py +0 -0
  157. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/rooms.py +0 -0
  158. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/threads.py +0 -0
  159. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/thread_store.py +0 -0
  160. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/skills/culture/SKILL.md +0 -0
  161. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/agent-lifecycle.md +0 -0
  162. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/agentic-self-learn.md +0 -0
  163. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/agent-client.md +0 -0
  164. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/agent-harness-spec.md +0 -0
  165. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/design.md +0 -0
  166. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/harness-conformance.md +0 -0
  167. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/index.md +0 -0
  168. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer1-core-irc.md +0 -0
  169. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer2-attention.md +0 -0
  170. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer3-skills.md +0 -0
  171. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer4-federation.md +0 -0
  172. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer5-agent-harness.md +0 -0
  173. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/server-architecture.md +0 -0
  174. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/threads.md +0 -0
  175. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/channel-polling.md +0 -0
  176. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/acp/overview.md +0 -0
  177. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/configuration.md +0 -0
  178. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/context-management.md +0 -0
  179. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/irc-tools.md +0 -0
  180. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/overview.md +0 -0
  181. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/setup.md +0 -0
  182. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/supervisor.md +0 -0
  183. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/webhooks.md +0 -0
  184. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/configuration.md +0 -0
  185. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/context-management.md +0 -0
  186. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/irc-tools.md +0 -0
  187. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/overview.md +0 -0
  188. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/setup.md +0 -0
  189. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/supervisor.md +0 -0
  190. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/webhooks.md +0 -0
  191. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/configuration.md +0 -0
  192. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/context-management.md +0 -0
  193. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/irc-tools.md +0 -0
  194. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/overview.md +0 -0
  195. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/setup.md +0 -0
  196. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/supervisor.md +0 -0
  197. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/webhooks.md +0 -0
  198. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/culture-cli.md +0 -0
  199. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/getting-started.md +0 -0
  200. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/index.md +0 -0
  201. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/SECURITY.md +0 -0
  202. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/bots.md +0 -0
  203. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/ci.md +0 -0
  204. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/cli.md +0 -0
  205. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/docs-site.md +0 -0
  206. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/index.md +0 -0
  207. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/ops-tooling.md +0 -0
  208. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/overview.md +0 -0
  209. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/publishing.md +0 -0
  210. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  211. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/rooms.md +0 -0
  212. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/server-rename.md +0 -0
  213. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  214. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  215. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
  216. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
  217. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
  218. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
  219. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
  220. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
  221. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
  222. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  223. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  224. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
  225. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
  226. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
  227. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
  228. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
  229. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
  230. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
  231. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
  232. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
  233. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
  234. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
  235. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/01-pair-programming.md +0 -0
  236. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
  237. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
  238. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
  239. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/05-the-observer.md +0 -0
  240. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/06-cross-server-ops.md +0 -0
  241. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
  242. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/08-apps-as-agents.md +0 -0
  243. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/09-research-swarm.md +0 -0
  244. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/10-agent-lifecycle.md +0 -0
  245. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases-index.md +0 -0
  246. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/what-is-culture.md +0 -0
  247. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/README.md +0 -0
  248. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/config.py +0 -0
  249. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/daemon.py +0 -0
  250. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/ipc.py +0 -0
  251. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/message_buffer.py +0 -0
  252. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/skill/SKILL.md +0 -0
  253. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/skill/irc_client.py +0 -0
  254. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/socket_server.py +0 -0
  255. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/webhook.py +0 -0
  256. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  257. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
  258. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  259. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
  260. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/sonar-project.properties +0 -0
  261. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/__init__.py +0 -0
  262. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/conftest.py +0 -0
  263. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_acp_daemon.py +0 -0
  264. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_agent_runner.py +0 -0
  265. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_archive.py +0 -0
  266. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bot.py +0 -0
  267. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bot_config.py +0 -0
  268. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bot_manager.py +0 -0
  269. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bots_integration.py +0 -0
  270. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_channel.py +0 -0
  271. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_codex_daemon.py +0 -0
  272. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_connection.py +0 -0
  273. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_client.py +0 -0
  274. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_commands.py +0 -0
  275. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_connection.py +0 -0
  276. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_icons.py +0 -0
  277. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_integration.py +0 -0
  278. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_copilot_daemon.py +0 -0
  279. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_daemon.py +0 -0
  280. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_daemon_config.py +0 -0
  281. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_daemon_ipc.py +0 -0
  282. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_discovery.py +0 -0
  283. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_federation.py +0 -0
  284. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_http_listener.py +0 -0
  285. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_integration_layer5.py +0 -0
  286. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_ipc.py +0 -0
  287. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_link_reconnect.py +0 -0
  288. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mention_alias.py +0 -0
  289. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mention_target_cleanup.py +0 -0
  290. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mentions.py +0 -0
  291. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mesh_config.py +0 -0
  292. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_message.py +0 -0
  293. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_message_buffer.py +0 -0
  294. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_messaging.py +0 -0
  295. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_modes.py +0 -0
  296. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_cli.py +0 -0
  297. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_collector.py +0 -0
  298. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_model.py +0 -0
  299. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_renderer.py +0 -0
  300. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_web.py +0 -0
  301. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_persistence.py +0 -0
  302. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_pidfile.py +0 -0
  303. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_poll_loop.py +0 -0
  304. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_room_persistence.py +0 -0
  305. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_rooms.py +0 -0
  306. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_rooms_federation.py +0 -0
  307. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_rooms_integration.py +0 -0
  308. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_server_icon_skill.py +0 -0
  309. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_setup_update_cli.py +0 -0
  310. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_skill_client.py +0 -0
  311. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_skills.py +0 -0
  312. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_socket_server.py +0 -0
  313. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_supervisor.py +0 -0
  314. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_template_engine.py +0 -0
  315. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_thread_buffer.py +0 -0
  316. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_threads.py +0 -0
  317. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_virtual_client.py +0 -0
  318. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_wait_for_port.py +0 -0
  319. {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_webhook.py +0 -0
@@ -4,6 +4,20 @@ 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
+ ## [4.4.0] - 2026-04-07
8
+
9
+
10
+ ### Added
11
+
12
+ - SQLite-backed persistent channel history (survives server restarts)
13
+ - --data-dir CLI flag for server start (default: ~/.culture/data)
14
+
15
+
16
+ ### Fixed
17
+
18
+ - Multi-line messages truncated to first line in send_privmsg and thread methods
19
+ - data_dir never wired to ServerConfig, silently disabling room/thread persistence
20
+
7
21
  ## [4.3.7] - 2026-04-07
8
22
 
9
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 4.3.7
3
+ Version: 4.4.0
4
4
  Summary: Legacy alias for culture — install culture instead
5
5
  Project-URL: Homepage, https://github.com/OriNachum/culture
6
6
  Author: Ori Nachum
@@ -74,6 +74,11 @@ def register(subparsers: argparse._SubParsersAction) -> None:
74
74
  action="store_true",
75
75
  help="Run in foreground (for service managers)",
76
76
  )
77
+ srv_start.add_argument(
78
+ "--data-dir",
79
+ default=os.path.expanduser("~/.culture/data"),
80
+ help="Data directory for persistent storage (default: ~/.culture/data)",
81
+ )
77
82
 
78
83
  srv_stop = server_sub.add_parser("stop", help="Stop the IRC server daemon")
79
84
  srv_stop.add_argument("--name", default=None, help=_SERVER_NAME_HELP)
@@ -279,7 +284,9 @@ def _run_foreground(args: argparse.Namespace, pid_name: str, links: list) -> Non
279
284
  print(f" Webhook port: {args.webhook_port}")
280
285
  _maybe_set_default_server(args.name)
281
286
  try:
282
- asyncio.run(_run_server(args.name, args.host, args.port, links, args.webhook_port))
287
+ asyncio.run(
288
+ _run_server(args.name, args.host, args.port, links, args.webhook_port, args.data_dir)
289
+ )
283
290
  finally:
284
291
  remove_pid(pid_name)
285
292
 
@@ -369,7 +376,9 @@ def _daemonize_server(args: argparse.Namespace, pid_name: str, links: list) -> N
369
376
  write_pid(pid_name, os.getpid())
370
377
 
371
378
  try:
372
- asyncio.run(_run_server(args.name, args.host, args.port, links, args.webhook_port))
379
+ asyncio.run(
380
+ _run_server(args.name, args.host, args.port, links, args.webhook_port, args.data_dir)
381
+ )
373
382
  finally:
374
383
  remove_pid(pid_name)
375
384
  os._exit(0)
@@ -391,14 +400,24 @@ def _server_start(args: argparse.Namespace) -> None:
391
400
 
392
401
 
393
402
  async def _run_server(
394
- name: str, host: str, port: int, links: list | None = None, webhook_port: int = 7680
403
+ name: str,
404
+ host: str,
405
+ port: int,
406
+ links: list | None = None,
407
+ webhook_port: int = 7680,
408
+ data_dir: str = "",
395
409
  ) -> None:
396
410
  """Run the IRC server (called in the daemon child process)."""
397
411
  from culture.server.config import ServerConfig
398
412
  from culture.server.ircd import IRCd
399
413
 
400
414
  config = ServerConfig(
401
- name=name, host=host, port=port, webhook_port=webhook_port, links=links or []
415
+ name=name,
416
+ host=host,
417
+ port=port,
418
+ webhook_port=webhook_port,
419
+ links=links or [],
420
+ data_dir=data_dir,
402
421
  )
403
422
  ircd = IRCd(config)
404
423
  await ircd.start()
@@ -87,16 +87,26 @@ class IRCTransport:
87
87
  self.connected = False
88
88
 
89
89
  async def send_privmsg(self, target: str, text: str) -> None:
90
- await self._send_raw(f"PRIVMSG {target} :{text}")
90
+ for line in text.splitlines():
91
+ if line:
92
+ await self._send_raw(f"PRIVMSG {target} :{line}")
91
93
 
92
94
  async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
93
- await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
95
+ lines = [l for l in text.splitlines() if l]
96
+ if not lines:
97
+ return
98
+ await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{lines[0]}")
99
+ for line in lines[1:]:
100
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
94
101
 
95
102
  async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
96
- await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
103
+ for line in text.splitlines():
104
+ if line:
105
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
97
106
 
98
107
  async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
99
- await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
108
+ clean = " ".join(summary.splitlines()).strip()
109
+ await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{clean}")
100
110
 
101
111
  async def send_threads_list(self, channel: str) -> None:
102
112
  await self._send_raw(f"THREADS {channel}")
@@ -87,16 +87,26 @@ class IRCTransport:
87
87
  self.connected = False
88
88
 
89
89
  async def send_privmsg(self, target: str, text: str) -> None:
90
- await self._send_raw(f"PRIVMSG {target} :{text}")
90
+ for line in text.splitlines():
91
+ if line:
92
+ await self._send_raw(f"PRIVMSG {target} :{line}")
91
93
 
92
94
  async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
93
- await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
95
+ lines = [l for l in text.splitlines() if l]
96
+ if not lines:
97
+ return
98
+ await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{lines[0]}")
99
+ for line in lines[1:]:
100
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
94
101
 
95
102
  async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
96
- await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
103
+ for line in text.splitlines():
104
+ if line:
105
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
97
106
 
98
107
  async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
99
- await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
108
+ clean = " ".join(summary.splitlines()).strip()
109
+ await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{clean}")
100
110
 
101
111
  async def send_threads_list(self, channel: str) -> None:
102
112
  await self._send_raw(f"THREADS {channel}")
@@ -87,16 +87,26 @@ class IRCTransport:
87
87
  self.connected = False
88
88
 
89
89
  async def send_privmsg(self, target: str, text: str) -> None:
90
- await self._send_raw(f"PRIVMSG {target} :{text}")
90
+ for line in text.splitlines():
91
+ if line:
92
+ await self._send_raw(f"PRIVMSG {target} :{line}")
91
93
 
92
94
  async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
93
- await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
95
+ lines = [l for l in text.splitlines() if l]
96
+ if not lines:
97
+ return
98
+ await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{lines[0]}")
99
+ for line in lines[1:]:
100
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
94
101
 
95
102
  async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
96
- await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
103
+ for line in text.splitlines():
104
+ if line:
105
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
97
106
 
98
107
  async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
99
- await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
108
+ clean = " ".join(summary.splitlines()).strip()
109
+ await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{clean}")
100
110
 
101
111
  async def send_threads_list(self, channel: str) -> None:
102
112
  await self._send_raw(f"THREADS {channel}")
@@ -87,16 +87,26 @@ class IRCTransport:
87
87
  self.connected = False
88
88
 
89
89
  async def send_privmsg(self, target: str, text: str) -> None:
90
- await self._send_raw(f"PRIVMSG {target} :{text}")
90
+ for line in text.splitlines():
91
+ if line:
92
+ await self._send_raw(f"PRIVMSG {target} :{line}")
91
93
 
92
94
  async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
93
- await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
95
+ lines = [l for l in text.splitlines() if l]
96
+ if not lines:
97
+ return
98
+ await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{lines[0]}")
99
+ for line in lines[1:]:
100
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
94
101
 
95
102
  async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
96
- await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
103
+ for line in text.splitlines():
104
+ if line:
105
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
97
106
 
98
107
  async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
99
- await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
108
+ clean = " ".join(summary.splitlines()).strip()
109
+ await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{clean}")
100
110
 
101
111
  async def send_threads_list(self, channel: str) -> None:
102
112
  await self._send_raw(f"THREADS {channel}")
@@ -107,6 +107,11 @@ An empty result set returns only the HISTORYEND line.
107
107
 
108
108
  - History is stored in memory with a configurable maximum per channel
109
109
  (default: 10,000 entries per channel)
110
- - History does not persist across server restarts
110
+ - When `data_dir` is configured (default: `~/.culture/data/`), history is
111
+ persisted to SQLite and survives server restarts
112
+ - Entries older than 30 days (configurable via `retention_days`) are
113
+ automatically pruned on startup
114
+ - The in-memory buffer remains the primary read cache; SQLite provides
115
+ durability
111
116
  - Both PRIVMSG and NOTICE to channels are recorded
112
117
  - Direct messages are never recorded
@@ -0,0 +1,91 @@
1
+ """SQLite disk persistence for channel message history."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sqlite3
7
+ import time
8
+ from collections import deque
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class HistoryStore:
15
+ """Save and load channel message history to/from SQLite."""
16
+
17
+ def __init__(self, data_dir: str):
18
+ db_dir = Path(data_dir)
19
+ db_dir.mkdir(parents=True, exist_ok=True)
20
+ self._db_path = db_dir / "history.db"
21
+ self._conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
22
+ self._conn.execute("PRAGMA journal_mode=WAL")
23
+ self._conn.execute("""CREATE TABLE IF NOT EXISTS history (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ channel TEXT NOT NULL,
26
+ nick TEXT NOT NULL,
27
+ text TEXT NOT NULL,
28
+ timestamp REAL NOT NULL
29
+ )""")
30
+ self._conn.execute(
31
+ "CREATE INDEX IF NOT EXISTS idx_history_channel_ts ON history(channel, timestamp, id)"
32
+ )
33
+ self._conn.commit()
34
+
35
+ def append(self, channel: str, nick: str, text: str, timestamp: float) -> None:
36
+ """Insert a single history entry (batched — not committed per call)."""
37
+ self._conn.execute(
38
+ "INSERT INTO history (channel, nick, text, timestamp) VALUES (?, ?, ?, ?)",
39
+ (channel, nick, text, timestamp),
40
+ )
41
+
42
+ def get_recent(self, channel: str, count: int) -> list[dict]:
43
+ """Return the last *count* entries for a channel, in chronological order."""
44
+ cur = self._conn.execute(
45
+ "SELECT nick, text, timestamp FROM history "
46
+ "WHERE channel = ? ORDER BY timestamp DESC, id DESC LIMIT ?",
47
+ (channel, count),
48
+ )
49
+ rows = cur.fetchall()
50
+ return [{"nick": r[0], "text": r[1], "timestamp": r[2]} for r in reversed(rows)]
51
+
52
+ def search(self, channel: str, term: str) -> list[dict]:
53
+ """Case-insensitive substring search within a channel."""
54
+ escaped = term.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
55
+ cur = self._conn.execute(
56
+ "SELECT nick, text, timestamp FROM history "
57
+ "WHERE channel = ? AND text LIKE ? ESCAPE '\\' ORDER BY timestamp ASC",
58
+ (channel, f"%{escaped}%"),
59
+ )
60
+ return [{"nick": r[0], "text": r[1], "timestamp": r[2]} for r in cur]
61
+
62
+ def load_channels(self, maxlen: int) -> dict[str, deque]:
63
+ """Load the last *maxlen* entries per channel for startup restore.
64
+
65
+ Returns a dict mapping channel names to deques of
66
+ ``{"nick": ..., "text": ..., "timestamp": ...}`` dicts.
67
+ """
68
+ cur = self._conn.execute("SELECT DISTINCT channel FROM history")
69
+ channels: dict[str, deque] = {}
70
+ for (channel,) in cur:
71
+ entries = self.get_recent(channel, maxlen)
72
+ channels[channel] = deque(entries, maxlen=maxlen)
73
+ return channels
74
+
75
+ def prune(self, max_age_days: int) -> int:
76
+ """Delete entries older than *max_age_days*. Returns rows deleted."""
77
+ cutoff = time.time() - (max_age_days * 86400)
78
+ cur = self._conn.execute("DELETE FROM history WHERE timestamp < ?", (cutoff,))
79
+ self._conn.commit()
80
+ deleted = cur.rowcount
81
+ if deleted:
82
+ logger.info("Pruned %d history entries older than %d days", deleted, max_age_days)
83
+ return deleted
84
+
85
+ def close(self) -> None:
86
+ """Flush pending writes and close the database connection."""
87
+ try:
88
+ self._conn.commit()
89
+ except sqlite3.Error:
90
+ pass
91
+ self._conn.close()
@@ -1,6 +1,7 @@
1
1
  # server/skills/history.py
2
2
  from __future__ import annotations
3
3
 
4
+ import logging
4
5
  from collections import deque
5
6
  from dataclasses import dataclass
6
7
  from typing import TYPE_CHECKING
@@ -12,6 +13,8 @@ from culture.server.skill import Event, EventType, Skill
12
13
  if TYPE_CHECKING:
13
14
  from culture.server.client import Client
14
15
 
16
+ logger = logging.getLogger(__name__)
17
+
15
18
 
16
19
  @dataclass
17
20
  class HistoryEntry:
@@ -24,9 +27,51 @@ class HistorySkill(Skill):
24
27
  name = "history"
25
28
  commands = {"HISTORY"}
26
29
 
27
- def __init__(self, maxlen: int = 10000):
30
+ def __init__(self, maxlen: int = 10000, retention_days: int = 30):
28
31
  self.maxlen = maxlen
32
+ self.retention_days = retention_days
29
33
  self._channels: dict[str, deque[HistoryEntry]] = {}
34
+ self._store = None
35
+
36
+ async def start(self, server) -> None:
37
+ await super().start(server)
38
+ self._restore_history()
39
+
40
+ async def stop(self) -> None:
41
+ if self._store is not None:
42
+ self._store.close()
43
+ self._store = None
44
+
45
+ def _restore_history(self) -> None:
46
+ """Reload persisted history from SQLite on startup."""
47
+ if not self.server.config.data_dir:
48
+ return
49
+ from culture.server.history_store import HistoryStore
50
+
51
+ try:
52
+ store = HistoryStore(self.server.config.data_dir)
53
+ store.prune(self.retention_days)
54
+ channel_data = store.load_channels(self.maxlen)
55
+ except Exception:
56
+ logger.warning(
57
+ "Failed to open history database — falling back to in-memory",
58
+ exc_info=True,
59
+ )
60
+ return
61
+
62
+ self._store = store
63
+ for channel, entries in channel_data.items():
64
+ buf = deque(maxlen=self.maxlen)
65
+ for e in entries:
66
+ buf.append(HistoryEntry(nick=e["nick"], text=e["text"], timestamp=e["timestamp"]))
67
+ self._channels[channel] = buf
68
+ total = sum(len(d) for d in self._channels.values())
69
+ if total:
70
+ logger.info(
71
+ "Restored %d history entries across %d channels",
72
+ total,
73
+ len(self._channels),
74
+ )
30
75
 
31
76
  async def on_event(self, event: Event) -> None:
32
77
  if event.type == EventType.MESSAGE and event.channel is not None:
@@ -38,6 +83,8 @@ class HistorySkill(Skill):
38
83
  timestamp=event.timestamp,
39
84
  )
40
85
  )
86
+ if self._store is not None:
87
+ self._store.append(event.channel, event.nick, event.data["text"], event.timestamp)
41
88
 
42
89
  def get_recent(self, channel: str, count: int) -> list[HistoryEntry]:
43
90
  if count <= 0:
@@ -0,0 +1,61 @@
1
+ # Persistent Channel History
2
+
3
+ Channel message history is now backed by SQLite, surviving server restarts.
4
+
5
+ ## How It Works
6
+
7
+ The server maintains an in-memory buffer (deque, 10K entries/channel) as the
8
+ hot read cache. When `data_dir` is configured, every channel message is also
9
+ appended to `{data_dir}/history.db`. On startup, the deque is restored from
10
+ SQLite.
11
+
12
+ Writes are batched (no per-message commit) to avoid blocking the event loop.
13
+ Pending writes are flushed on shutdown.
14
+
15
+ ## Configuration
16
+
17
+ The `--data-dir` flag on `culture server start` controls where persistent data
18
+ is stored. It defaults to `~/.culture/data/`.
19
+
20
+ ```bash
21
+ # Default (persistence enabled)
22
+ culture server start --name spark
23
+
24
+ # Custom path
25
+ culture server start --name spark --data-dir /var/lib/culture/data
26
+
27
+ # Disable persistence (empty string)
28
+ culture server start --name spark --data-dir ""
29
+ ```
30
+
31
+ When `data_dir` is empty, the server operates in-memory only (original
32
+ behavior). All other persistent features (rooms, threads) also use this
33
+ directory.
34
+
35
+ ## Retention
36
+
37
+ Entries older than 30 days are automatically pruned on startup. The retention
38
+ period is configurable via the `retention_days` parameter on `HistorySkill`
39
+ (default: 30).
40
+
41
+ ## Multi-line Messages
42
+
43
+ Agent transports (`send_privmsg`, `send_thread_reply`, etc.) now split
44
+ multi-line text into separate IRC messages using `str.splitlines()`. This
45
+ handles `\n`, `\r\n`, and `\r` uniformly, preventing truncation and CRLF
46
+ injection.
47
+
48
+ For `send_thread_create`, the first line becomes the CREATE command and
49
+ subsequent lines are sent as REPLY commands. For `send_thread_close`, all
50
+ line breaks are collapsed to spaces.
51
+
52
+ ## Protocol
53
+
54
+ No protocol changes. The `HISTORY RECENT` and `HISTORY SEARCH` commands work
55
+ identically. See `protocol/extensions/history.md` for the wire format.
56
+
57
+ ## Graceful Degradation
58
+
59
+ If the SQLite database is corrupt, locked, or inaccessible at startup, the
60
+ server logs a warning and falls back to in-memory history. The server will not
61
+ crash due to a database error.
@@ -88,16 +88,26 @@ class IRCTransport:
88
88
  self.connected = False
89
89
 
90
90
  async def send_privmsg(self, target: str, text: str) -> None:
91
- await self._send_raw(f"PRIVMSG {target} :{text}")
91
+ for line in text.splitlines():
92
+ if line:
93
+ await self._send_raw(f"PRIVMSG {target} :{line}")
92
94
 
93
95
  async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None:
94
- await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{text}")
96
+ lines = [l for l in text.splitlines() if l]
97
+ if not lines:
98
+ return
99
+ await self._send_raw(f"THREAD CREATE {channel} {thread_name} :{lines[0]}")
100
+ for line in lines[1:]:
101
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
95
102
 
96
103
  async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None:
97
- await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{text}")
104
+ for line in text.splitlines():
105
+ if line:
106
+ await self._send_raw(f"THREAD REPLY {channel} {thread_name} :{line}")
98
107
 
99
108
  async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None:
100
- await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{summary}")
109
+ clean = " ".join(summary.splitlines()).strip()
110
+ await self._send_raw(f"THREADCLOSE {channel} {thread_name} :{clean}")
101
111
 
102
112
  async def send_threads_list(self, channel: str) -> None:
103
113
  await self._send_raw(f"THREADS {channel}")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentirc-cli"
3
- version = "4.3.7"
3
+ version = "4.4.0"
4
4
  description = "Legacy alias for culture — install culture instead"
5
5
  readme = "README.md"
6
6
  license = "MIT"