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.
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/CHANGELOG.md +14 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/PKG-INFO +1 -1
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/server.py +23 -4
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/irc_transport.py +14 -4
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/irc_transport.py +14 -4
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/irc_transport.py +14 -4
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/irc_transport.py +14 -4
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/history.md +6 -1
- agentirc_cli-4.4.0/culture/server/history_store.py +91 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/history.py +48 -1
- agentirc_cli-4.4.0/docs/persistent-history.md +61 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/irc_transport.py +14 -4
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/pyproject.toml +1 -1
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_history.py +178 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_irc_transport.py +55 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/uv.lock +1 -1
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.flake8 +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/security-checks.yml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.gitignore +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.pr_agent.toml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/.pylintrc +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/CLAUDE.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/CNAME +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/Gemfile +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/Gemfile.lock +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/LICENSE +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/README.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/SECURITY.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/_config.yml +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/__main__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/aio.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/bot.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/bot_manager.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/http_listener.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/template_engine.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/bots/virtual_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/agent.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/bot.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/channel.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/mesh.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/constants.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/display.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/formatting.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/mesh.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/shared/process.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/cli/skills.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/message_buffer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/socket_server.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/supervisor.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/acp/webhook.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/__main__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/socket_server.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/supervisor.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/claude/webhook.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/socket_server.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/supervisor.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/codex/webhook.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/clients/copilot/webhook.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/app.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/commands.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/chat.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/info_panel.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/console/widgets/sidebar.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/credentials.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/formatting.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/learn_prompt.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/mesh_config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/observer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/collector.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/model.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/renderer_text.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/renderer_web.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/overview/web/style.css +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/persistence.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/pidfile.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/commands.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/federation.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/icons.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/tags.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/extensions/threads.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/message.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/protocol-index.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/protocol/replies.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/__main__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/channel.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/ircd.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/remote_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/room_store.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/rooms_util.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/server_link.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skill.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/icon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/rooms.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/skills/threads.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/server/thread_store.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/culture/skills/culture/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/agent-lifecycle.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/agentic-self-learn.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/agent-client.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/agent-harness-spec.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/harness-conformance.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/index.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer1-core-irc.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer2-attention.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer3-skills.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer4-federation.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/layer5-agent-harness.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/server-architecture.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/architecture/threads.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/channel-polling.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/acp/overview.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/culture-cli.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/getting-started.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/index.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/SECURITY.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/bots.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/ci.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/cli.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/docs-site.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/index.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/ops-tooling.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/overview.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/operations/publishing.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/rooms.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/server-rename.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases/10-agent-lifecycle.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/docs/what-is-culture.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/sonar-project.properties +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/__init__.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/conftest.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_archive.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bot.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bot_config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bot_manager.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_bots_integration.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_channel.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_connection.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_commands.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_connection.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_icons.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_console_integration.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_federation.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_http_listener.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mention_alias.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mention_target_cleanup.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_message.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_modes.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_model.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_overview_web.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_persistence.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_pidfile.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_poll_loop.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_rooms.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_skills.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_template_engine.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_threads.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_virtual_client.py +0 -0
- {agentirc_cli-4.3.7 → agentirc_cli-4.4.0}/tests/test_wait_for_port.py +0 -0
- {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
|
|
|
@@ -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(
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}")
|