agentirc-cli 8.3.0__tar.gz → 8.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-8.3.0 → agentirc_cli-8.4.0}/CHANGELOG.md +15 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/PKG-INFO +1 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/client.py +31 -2
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/config.py +2 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/ircd.py +16 -2
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/server_link.py +75 -14
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/tracing.md +1 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/telemetry/__init__.py +3 -0
- agentirc_cli-8.4.0/culture/telemetry/metrics.py +225 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/telemetry/tracing.py +4 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/agentirc/telemetry.md +45 -6
- agentirc_cli-8.4.0/docs/superpowers/plans/2026-04-26-otel-metrics.md +303 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/pyproject.toml +1 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/conftest.py +25 -0
- agentirc_cli-8.4.0/tests/telemetry/_metrics_helpers.py +66 -0
- agentirc_cli-8.4.0/tests/telemetry/test_metrics_clients.py +79 -0
- agentirc_cli-8.4.0/tests/telemetry/test_metrics_events.py +81 -0
- agentirc_cli-8.4.0/tests/telemetry/test_metrics_init.py +58 -0
- agentirc_cli-8.4.0/tests/telemetry/test_metrics_irc.py +99 -0
- agentirc_cli-8.4.0/tests/telemetry/test_metrics_s2s.py +160 -0
- agentirc_cli-8.4.0/tests/telemetry/test_metrics_trace_inbound.py +127 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/uv.lock +1 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.claude/agents/doc-test-alignment.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.flake8 +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.github/workflows/docs-check.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.github/workflows/security-checks.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.gitignore +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.pr_agent.toml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/.pylintrc +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/CLAUDE.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/Gemfile +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/Gemfile.lock +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/LICENSE +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/README.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/SECURITY.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/_config.base.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/_config.culture.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/_data/sites.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/_includes/head_custom.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/_sass/color_schemes/dark-terminal.scss +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/assets/images/IMG_3183.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/assets/images/apple-touch-icon.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/assets/images/favicon-16x16.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/assets/images/favicon-32x32.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/assets/images/favicon.ico +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/assets/images/og-agentirc.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/assets/images/og-culture.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/__main__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/CLAUDE.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/__main__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/channel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/docs/agentirc-architecture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/docs/agentirc-features.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/docs/agentirc-skill.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/docs/agentirc.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/events.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/history_store.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/remote_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/room_store.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/rooms_util.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/skill.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/skills/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/skills/history.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/skills/icon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/skills/rooms.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/skills/threads.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/agentirc/thread_store.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/aio.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/bot.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/bot_manager.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/filter_dsl.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/http_listener.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/system/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/system/welcome/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/system/welcome/bot.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/system/welcome/handler.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/template_engine.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/bots/virtual_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/_passthrough.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/afi.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/agent.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/bot.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/channel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/devex.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/introspect.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/mesh.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/shared/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/shared/constants.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/shared/display.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/shared/formatting.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/shared/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/shared/mesh.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/shared/process.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/cli/skills.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/acp/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/__main__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/claude/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/codex/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/clients/copilot/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/app.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/commands.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/status.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/widgets/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/widgets/chat.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/widgets/info_panel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/console/widgets/sidebar.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/constants.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/credentials.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/formatting.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/learn_prompt.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/mesh_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/observer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/overview/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/overview/collector.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/overview/model.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/overview/renderer_text.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/overview/renderer_web.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/overview/web/style.css +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/persistence.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/pidfile.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/commands.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/events.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/federation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/history.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/icons.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/tags.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/extensions/threads.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/message.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/protocol-index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/protocol/replies.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/skills/culture/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/culture/telemetry/context.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/README.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/agentirc/architecture-overview.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/agentirc/bots.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/agentirc/events.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/agentirc/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/agentirc/otelcol-template.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/agentirc/why-agentirc.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/agent-lifecycle.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/choose-a-harness.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/features.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/mental-model.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/operate.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/patterns.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/quickstart.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/reflective-development.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/vision-patterns-index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/vision.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/culture/what-is-culture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/architecture/agent-harness-spec.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/architecture/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/architecture/layers.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/architecture/subsites.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/architecture/threads.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/cli/afi.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/cli/commands.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/cli/devex.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/cli/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/console.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/harnesses/acp.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/harnesses/claude.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/harnesses/codex.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/harnesses/copilot.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/harnesses/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/server/architecture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/server/config.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/server/deployment.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/server/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/reference/server/security.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/resources/positioning.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/concepts/federation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/concepts/harnesses.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/concepts/humans-and-agents.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/concepts/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/concepts/persistence.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/concepts/rooms.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/demos/magic-demo.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/guides/first-session.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/guides/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/guides/join-as-human.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/guides/local-setup.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/guides/multi-machine.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases/10-agent-lifecycle.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/shared/use-cases-index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-12-console-enhancements.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-15-mesh-events.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-18-culture-dev-positioning.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-22-agex-integration.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-24-otel-foundation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/plans/2026-04-25-otel-federation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-12-console-enhancements-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-15-mesh-events-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-17-sites-repositioning-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-18-culture-dev-positioning-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-22-agex-integration-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/docs/superpowers/specs/2026-04-24-otel-observability-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/favicon.ico +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/robots.txt +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/sitemap-agentirc.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/sitemap-main.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/sitemap.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/sonar-project.properties +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/_fakes.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_config_load.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_context.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_dispatch_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_emit_event_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_federation_propagation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_outbound_inject.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_parse_error.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_privmsg_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_s2s_dispatch_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_s2s_relay_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_s2s_session_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_server_init.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_server_link_inject.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_session_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/telemetry/test_tracing.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_archive.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_bot.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_bot_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_bot_config_fires_event_toplevel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_bot_manager.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_bots_integration.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_channel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_channel_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_cli_afi.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_cli_devex.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_cli_introspect.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_cli_passthrough.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_connection.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_chat_markdown.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_commands.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_connection.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_fixes_224_227.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_icons.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_integration.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_console_status.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_credentials.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_culture_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_display.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_basic.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_bot_chain.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_bot_trigger.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_cap_fallback.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_catalog.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_federation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_history.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_lifecycle.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_events_reserved_nick.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_federation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_filter_dsl.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_history.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_http_listener.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_irc_transport_tags.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_learn_prompt.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_manifest_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_mention_alias.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_mention_target_cleanup.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_mention_warning.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_mesh_readiness.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_message.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_message_tags.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_migrate_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_modes.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_overview_model.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_overview_web.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_persistence.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_persistence_timeout.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_pidfile.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_poll_loop.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_register_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_rooms.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_skill_docs.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_skills.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_template_engine.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_threads.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_virtual_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_wait_for_port.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.4.0}/tests/test_welcome_bot.py +0 -0
|
@@ -4,6 +4,21 @@ 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
|
+
## [8.4.0] - 2026-04-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `culture/telemetry/metrics.py`: `init_metrics(config)` + `MetricsRegistry` dataclass for all 15 server-side instruments — mirrors `tracing.py`'s idempotency + no-op pattern.
|
|
12
|
+
- Public `culture.telemetry.MetricsRegistry` and `culture.telemetry.init_metrics`.
|
|
13
|
+
- `TelemetryConfig.metrics_enabled` (default `True`) and `metrics_export_interval_ms` (default 10000).
|
|
14
|
+
- Message-flow metrics: `culture.irc.bytes_sent`, `culture.irc.bytes_received`, `culture.irc.message.size`, `culture.privmsg.delivered`.
|
|
15
|
+
- Events metrics: `culture.events.emitted`, `culture.events.render.duration`.
|
|
16
|
+
- Federation metrics: `culture.s2s.messages` (inbound), `culture.s2s.relay_latency`, `culture.s2s.links_active`, `culture.s2s.link_events`.
|
|
17
|
+
- Client metrics: `culture.clients.connected`, `culture.client.session.duration`, `culture.client.command.duration`.
|
|
18
|
+
- `culture.trace.inbound` counter — closes Plan 2's deferral.
|
|
19
|
+
- `tests/conftest.py` `metrics_reader` fixture parallel to `tracing_exporter`.
|
|
20
|
+
- `tests/telemetry/_metrics_helpers.py` — `get_counter_value`, `get_histogram_count`, `get_up_down_value`.
|
|
21
|
+
|
|
7
22
|
## [8.3.0] - 2026-04-25
|
|
8
23
|
|
|
9
24
|
### Added
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
|
+
import time
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
10
|
from opentelemetry import trace as _otel_trace
|
|
@@ -77,8 +78,12 @@ class Client:
|
|
|
77
78
|
if tp is not None:
|
|
78
79
|
_inject_traceparent(message, traceparent=tp, tracestate=None)
|
|
79
80
|
try:
|
|
80
|
-
|
|
81
|
+
wire = message.format().encode("utf-8")
|
|
82
|
+
self.writer.write(wire)
|
|
81
83
|
await self.writer.drain()
|
|
84
|
+
# Record bytes after a successful drain so we don't count
|
|
85
|
+
# writes that immediately faulted.
|
|
86
|
+
self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2c"})
|
|
82
87
|
except OSError:
|
|
83
88
|
pass # Client disconnected; cleanup happens in ircd._handle_connection
|
|
84
89
|
|
|
@@ -96,8 +101,10 @@ class Client:
|
|
|
96
101
|
# block; prefix a fresh @tag.
|
|
97
102
|
line = f"@{_TP_TAG_NAME}={tp} {line}"
|
|
98
103
|
try:
|
|
99
|
-
|
|
104
|
+
wire = f"{line}\r\n".encode("utf-8")
|
|
105
|
+
self.writer.write(wire)
|
|
100
106
|
await self.writer.drain()
|
|
107
|
+
self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2c"})
|
|
101
108
|
except OSError:
|
|
102
109
|
pass # Client disconnected; cleanup happens in ircd._handle_connection
|
|
103
110
|
|
|
@@ -142,6 +149,13 @@ class Client:
|
|
|
142
149
|
},
|
|
143
150
|
)
|
|
144
151
|
continue
|
|
152
|
+
# Record received bytes + message size for every successfully-parsed
|
|
153
|
+
# line. +2 accounts for the \r\n that was stripped during line-split.
|
|
154
|
+
line_bytes = len(line.encode("utf-8")) + 2
|
|
155
|
+
self.server.metrics.irc_bytes_received.add(line_bytes, {"direction": "c2s"})
|
|
156
|
+
self.server.metrics.irc_message_size.record(
|
|
157
|
+
line_bytes, {"verb": msg.command, "direction": "c2s"}
|
|
158
|
+
)
|
|
145
159
|
if msg.command:
|
|
146
160
|
await self._dispatch(msg)
|
|
147
161
|
return buffer
|
|
@@ -149,6 +163,9 @@ class Client:
|
|
|
149
163
|
async def handle(self, initial_msg: str | None = None) -> None:
|
|
150
164
|
peer_info = self.writer.get_extra_info("peername")
|
|
151
165
|
remote_addr = f"{peer_info[0]}:{peer_info[1]}" if peer_info else ""
|
|
166
|
+
kind = "human" # Plan 5/6 will refine to bot/harness
|
|
167
|
+
self.server.metrics.clients_connected.add(1, {"kind": kind})
|
|
168
|
+
session_started = time.perf_counter()
|
|
152
169
|
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
153
170
|
"irc.client.session",
|
|
154
171
|
attributes={"irc.client.remote_addr": remote_addr},
|
|
@@ -172,9 +189,15 @@ class Client:
|
|
|
172
189
|
buffer = await self._process_buffer(buffer)
|
|
173
190
|
except (ConnectionError, asyncio.IncompleteReadError):
|
|
174
191
|
pass
|
|
192
|
+
finally:
|
|
193
|
+
self.server.metrics.clients_connected.add(-1, {"kind": kind})
|
|
194
|
+
self.server.metrics.client_session_duration.record(
|
|
195
|
+
time.perf_counter() - session_started, {"kind": kind}
|
|
196
|
+
)
|
|
175
197
|
|
|
176
198
|
async def _dispatch(self, msg: Message) -> None:
|
|
177
199
|
extract = extract_traceparent_from_tags(msg, peer=None)
|
|
200
|
+
self.server.metrics.trace_inbound.add(1, {"result": extract.status, "peer": ""})
|
|
178
201
|
if extract.status == "valid":
|
|
179
202
|
parent_ctx: _OtelContext | None = context_from_traceparent(extract.traceparent)
|
|
180
203
|
else:
|
|
@@ -190,6 +213,7 @@ class Client:
|
|
|
190
213
|
attrs["culture.trace.dropped_reason"] = extract.status
|
|
191
214
|
|
|
192
215
|
# Per-call get_tracer: test fixture swaps provider between tests.
|
|
216
|
+
cmd_started = time.perf_counter()
|
|
193
217
|
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
194
218
|
f"irc.command.{verb}",
|
|
195
219
|
context=parent_ctx,
|
|
@@ -211,6 +235,9 @@ class Client:
|
|
|
211
235
|
await self.send_numeric(
|
|
212
236
|
replies.ERR_UNKNOWNCOMMAND, msg.command, "Unknown command"
|
|
213
237
|
)
|
|
238
|
+
self.server.metrics.client_command_duration.record(
|
|
239
|
+
(time.perf_counter() - cmd_started) * 1000.0, {"verb": verb}
|
|
240
|
+
)
|
|
214
241
|
|
|
215
242
|
async def _handle_ping(self, msg: Message) -> None:
|
|
216
243
|
token = msg.params[0] if msg.params else ""
|
|
@@ -724,6 +751,7 @@ class Client:
|
|
|
724
751
|
for member in list(channel.members):
|
|
725
752
|
if member is not self:
|
|
726
753
|
await member.send(relay)
|
|
754
|
+
self.server.metrics.privmsg_delivered.add(1, {"kind": "channel", "channel": target})
|
|
727
755
|
event_data = {"text": text}
|
|
728
756
|
if is_notice:
|
|
729
757
|
event_data["notice"] = True
|
|
@@ -758,6 +786,7 @@ class Client:
|
|
|
758
786
|
)
|
|
759
787
|
else:
|
|
760
788
|
await recipient.send(relay)
|
|
789
|
+
self.server.metrics.privmsg_delivered.add(1, {"kind": "dm"})
|
|
761
790
|
event_data = {"text": text, "target": target}
|
|
762
791
|
if is_notice:
|
|
763
792
|
event_data["notice"] = True
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import time
|
|
8
9
|
from collections import deque
|
|
9
10
|
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
@@ -25,6 +26,9 @@ from culture.protocol.message import Message
|
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
29
|
+
# Span/metric attribute keys defined once so a future rename has one edit point.
|
|
30
|
+
_ATTR_EVENT_TYPE = "event.type"
|
|
31
|
+
|
|
28
32
|
if TYPE_CHECKING:
|
|
29
33
|
from culture.agentirc.client import Client
|
|
30
34
|
from culture.agentirc.remote_client import RemoteClient
|
|
@@ -35,10 +39,11 @@ class IRCd:
|
|
|
35
39
|
"""The culture IRC server."""
|
|
36
40
|
|
|
37
41
|
def __init__(self, config: ServerConfig):
|
|
38
|
-
from culture.telemetry import init_telemetry
|
|
42
|
+
from culture.telemetry import init_metrics, init_telemetry
|
|
39
43
|
|
|
40
44
|
self.config = config
|
|
41
45
|
self.tracer = init_telemetry(config)
|
|
46
|
+
self.metrics = init_metrics(config)
|
|
42
47
|
self.clients: dict[str, Client | VirtualClient] = {} # nick -> Client
|
|
43
48
|
self.channels: dict[str, Channel] = {} # name -> Channel
|
|
44
49
|
self.skills: list[Skill] = []
|
|
@@ -178,7 +183,7 @@ class IRCd:
|
|
|
178
183
|
# server_link.py).
|
|
179
184
|
event_type_str = event.type.value if hasattr(event.type, "value") else str(event.type)
|
|
180
185
|
attrs: dict[str, str] = {
|
|
181
|
-
|
|
186
|
+
_ATTR_EVENT_TYPE: event_type_str,
|
|
182
187
|
"event.origin": "federated" if origin_tag else "local",
|
|
183
188
|
}
|
|
184
189
|
if event.channel:
|
|
@@ -212,6 +217,12 @@ class IRCd:
|
|
|
212
217
|
async def emit_event(self, event: Event) -> None:
|
|
213
218
|
origin_tag = event.data.get("_origin")
|
|
214
219
|
attrs = self._build_event_span_attrs(event, origin_tag)
|
|
220
|
+
event_type_str = attrs[_ATTR_EVENT_TYPE]
|
|
221
|
+
origin_str = "federated" if origin_tag else "local"
|
|
222
|
+
|
|
223
|
+
self.metrics.events_emitted.add(1, {_ATTR_EVENT_TYPE: event_type_str, "origin": origin_str})
|
|
224
|
+
render_started = time.perf_counter()
|
|
225
|
+
|
|
215
226
|
# Per-call get_tracer: the `tracing_exporter` test fixture swaps the
|
|
216
227
|
# global provider between tests; a cached Tracer would bind to the
|
|
217
228
|
# first test's provider and stop delivering to later ones.
|
|
@@ -226,6 +237,9 @@ class IRCd:
|
|
|
226
237
|
await self._dispatch_to_bots(event)
|
|
227
238
|
await self._surface_event_privmsg(event)
|
|
228
239
|
|
|
240
|
+
render_ms = (time.perf_counter() - render_started) * 1000.0
|
|
241
|
+
self.metrics.events_render_duration.record(render_ms, {_ATTR_EVENT_TYPE: event_type_str})
|
|
242
|
+
|
|
229
243
|
_NO_SURFACE_TYPES = NO_SURFACE_EVENT_TYPES
|
|
230
244
|
|
|
231
245
|
@staticmethod
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import time
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
11
|
from opentelemetry import trace as otel_trace
|
|
@@ -137,8 +138,11 @@ class ServerLink:
|
|
|
137
138
|
except Exception: # noqa: BLE001 - telemetry must never break the link
|
|
138
139
|
logger.debug("traceparent injection failed; sending untagged", exc_info=True)
|
|
139
140
|
try:
|
|
140
|
-
|
|
141
|
+
wire = f"{line}\r\n".encode("utf-8")
|
|
142
|
+
self.writer.write(wire)
|
|
141
143
|
await self.writer.drain()
|
|
144
|
+
if self.server is not None:
|
|
145
|
+
self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2s"})
|
|
142
146
|
except OSError:
|
|
143
147
|
pass
|
|
144
148
|
|
|
@@ -155,6 +159,13 @@ class ServerLink:
|
|
|
155
159
|
line, buffer = buffer.split("\n", 1)
|
|
156
160
|
if line.strip():
|
|
157
161
|
msg = Message.parse(line)
|
|
162
|
+
if self.server is not None:
|
|
163
|
+
# +2 accounts for the \r\n that was stripped during line-split.
|
|
164
|
+
line_bytes = len(line.encode("utf-8")) + 2
|
|
165
|
+
self.server.metrics.irc_bytes_received.add(line_bytes, {"direction": "s2s"})
|
|
166
|
+
self.server.metrics.irc_message_size.record(
|
|
167
|
+
line_bytes, {"verb": msg.command, "direction": "s2s"}
|
|
168
|
+
)
|
|
158
169
|
if msg.command:
|
|
159
170
|
await self._dispatch(msg)
|
|
160
171
|
return buffer
|
|
@@ -187,6 +198,14 @@ class ServerLink:
|
|
|
187
198
|
except (ConnectionError, asyncio.IncompleteReadError):
|
|
188
199
|
pass
|
|
189
200
|
finally:
|
|
201
|
+
if self._authenticated:
|
|
202
|
+
direction = "outbound" if self.initiator else "inbound"
|
|
203
|
+
self.server.metrics.s2s_links_active.add(
|
|
204
|
+
-1, {"peer": self.peer_name or "", "direction": direction}
|
|
205
|
+
)
|
|
206
|
+
self.server.metrics.s2s_link_events.add(
|
|
207
|
+
1, {"peer": self.peer_name or "", "event": "disconnect"}
|
|
208
|
+
)
|
|
190
209
|
await self.server._remove_link(self, squit=self._squit_received)
|
|
191
210
|
self.writer.close()
|
|
192
211
|
try:
|
|
@@ -203,11 +222,20 @@ class ServerLink:
|
|
|
203
222
|
handler = getattr(self, f"_handle_{msg.command.lower()}", None)
|
|
204
223
|
|
|
205
224
|
extracted = extract_traceparent_from_tags(msg, peer=self.peer_name)
|
|
225
|
+
self.server.metrics.trace_inbound.add(
|
|
226
|
+
1, {"result": extracted.status, "peer": self.peer_name or ""}
|
|
227
|
+
)
|
|
206
228
|
if extracted.status == "valid":
|
|
207
229
|
parent_ctx: _OtelContext | None = context_from_traceparent(extracted.traceparent)
|
|
208
230
|
else:
|
|
209
231
|
parent_ctx = _OtelContext() # force root: detach from session span
|
|
210
232
|
|
|
233
|
+
if self.server is not None:
|
|
234
|
+
self.server.metrics.s2s_messages.add(
|
|
235
|
+
1,
|
|
236
|
+
{"verb": verb, "direction": "inbound", "peer": self.peer_name or ""},
|
|
237
|
+
)
|
|
238
|
+
|
|
211
239
|
attrs = {
|
|
212
240
|
"irc.command": verb,
|
|
213
241
|
"culture.trace.origin": "remote",
|
|
@@ -266,17 +294,26 @@ class ServerLink:
|
|
|
266
294
|
"""
|
|
267
295
|
if self._peer_pass != self.password:
|
|
268
296
|
logger.warning("Bad password from peer %s", self.peer_name)
|
|
297
|
+
self.server.metrics.s2s_link_events.add(
|
|
298
|
+
1, {"peer": self.peer_name or "", "event": "auth_fail"}
|
|
299
|
+
)
|
|
269
300
|
await self.send_raw("ERROR :Bad password")
|
|
270
301
|
raise ConnectionError("Bad S2S password")
|
|
271
302
|
|
|
272
303
|
# Check for duplicate server name
|
|
273
304
|
if self.peer_name in self.server.links:
|
|
274
305
|
logger.warning("Duplicate server name %s", self.peer_name)
|
|
306
|
+
self.server.metrics.s2s_link_events.add(
|
|
307
|
+
1, {"peer": self.peer_name or "", "event": "auth_fail"}
|
|
308
|
+
)
|
|
275
309
|
await self.send_raw(f"ERROR :Server name {self.peer_name} already linked")
|
|
276
310
|
raise ConnectionError("Duplicate server name")
|
|
277
311
|
|
|
278
312
|
if self.peer_name == self.server.config.name:
|
|
279
313
|
logger.warning("Peer has same name as us: %s", self.peer_name)
|
|
314
|
+
self.server.metrics.s2s_link_events.add(
|
|
315
|
+
1, {"peer": self.peer_name or "", "event": "auth_fail"}
|
|
316
|
+
)
|
|
280
317
|
await self.send_raw("ERROR :Cannot link to self")
|
|
281
318
|
raise ConnectionError("Cannot link to self")
|
|
282
319
|
|
|
@@ -291,6 +328,11 @@ class ServerLink:
|
|
|
291
328
|
await self._validate_peer_credentials()
|
|
292
329
|
|
|
293
330
|
self._authenticated = True
|
|
331
|
+
direction = "outbound" if self.initiator else "inbound"
|
|
332
|
+
self.server.metrics.s2s_links_active.add(
|
|
333
|
+
1, {"peer": self.peer_name, "direction": direction}
|
|
334
|
+
)
|
|
335
|
+
self.server.metrics.s2s_link_events.add(1, {"peer": self.peer_name, "event": "connect"})
|
|
294
336
|
if self._session_span is not None:
|
|
295
337
|
self._session_span.set_attribute("s2s.peer", self.peer_name)
|
|
296
338
|
self.server.links[self.peer_name] = self
|
|
@@ -900,6 +942,10 @@ class ServerLink:
|
|
|
900
942
|
except ValueError:
|
|
901
943
|
return
|
|
902
944
|
|
|
945
|
+
self.server.metrics.s2s_link_events.add(
|
|
946
|
+
1, {"peer": self.peer_name or "", "event": "backfill_start"}
|
|
947
|
+
)
|
|
948
|
+
|
|
903
949
|
# Use the higher of: what peer claims, or what we know they acked
|
|
904
950
|
# (during real-time relay, peer saw everything up to our _seq at link drop)
|
|
905
951
|
acked = self.server._peer_acked_seq.get(self.peer_name, 0)
|
|
@@ -910,6 +956,9 @@ class ServerLink:
|
|
|
910
956
|
await self._replay_event(seq, event)
|
|
911
957
|
|
|
912
958
|
await self.send_raw(f":{self.server.config.name} BACKFILLEND {self.server._seq}")
|
|
959
|
+
self.server.metrics.s2s_link_events.add(
|
|
960
|
+
1, {"peer": self.peer_name or "", "event": "backfill_complete"}
|
|
961
|
+
)
|
|
913
962
|
|
|
914
963
|
def _handle_backfillend(self, msg: Message) -> None:
|
|
915
964
|
"""Peer finished backfilling."""
|
|
@@ -946,6 +995,8 @@ class ServerLink:
|
|
|
946
995
|
"event.type": event_type_str,
|
|
947
996
|
"s2s.peer": self.peer_name or "",
|
|
948
997
|
}
|
|
998
|
+
relay_started = time.perf_counter()
|
|
999
|
+
relayed = False
|
|
949
1000
|
# Single span name (no verb suffix): the wire verb is decided downstream
|
|
950
1001
|
# by _RELAY_DISPATCH or the SEVENT fallback, after this span opens.
|
|
951
1002
|
with otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
@@ -955,19 +1006,29 @@ class ServerLink:
|
|
|
955
1006
|
handler = self._RELAY_DISPATCH.get(event.type)
|
|
956
1007
|
if handler:
|
|
957
1008
|
await maybe_await(handler(self, event, origin))
|
|
958
|
-
return
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1009
|
+
# v1: typed _relay_* handlers may early-return on internal trust checks
|
|
1010
|
+
# without signaling. We optimistically count typed dispatch as relayed.
|
|
1011
|
+
# Future: refactor _relay_* to return bool so latency only fires on
|
|
1012
|
+
# genuine sends.
|
|
1013
|
+
relayed = True
|
|
1014
|
+
else:
|
|
1015
|
+
# If no typed relay exists, fall back to generic SEVENT.
|
|
1016
|
+
# v1 assumes all peers support SEVENT; cap negotiation is deferred — see plan task 12.
|
|
1017
|
+
payload = self.server._build_event_payload(event)
|
|
1018
|
+
encoded = self.server._encode_event_data(payload, event_type_str)
|
|
1019
|
+
target = event.channel or "*"
|
|
1020
|
+
# Egress trust check: channel-scoped events respect should_relay; global events always relay
|
|
1021
|
+
if event.channel is None or self.should_relay(event.channel):
|
|
1022
|
+
seq = self.server._seq # current local seq; peer stores but doesn't re-sequence
|
|
1023
|
+
await self.send_raw(
|
|
1024
|
+
f":{origin} SEVENT {origin} {seq} {event_type_str} {target} :{encoded}"
|
|
1025
|
+
)
|
|
1026
|
+
relayed = True
|
|
1027
|
+
|
|
1028
|
+
if relayed:
|
|
1029
|
+
self.server.metrics.s2s_relay_latency.record(
|
|
1030
|
+
(time.perf_counter() - relay_started) * 1000.0,
|
|
1031
|
+
{"event.type": event_type_str, "peer": self.peer_name or ""},
|
|
971
1032
|
)
|
|
972
1033
|
|
|
973
1034
|
async def _relay_message(self, event: Event, origin: str) -> None:
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
1. Tag absent → start a new root span. Attribute: `culture.trace.origin=local`.
|
|
13
13
|
2. Tag present and valid → start a child span linked to the extracted context. Attributes: `culture.trace.origin=remote`, `culture.federation.peer=<peer>`.
|
|
14
|
-
3. Tag present but malformed or over the length cap → drop the tag, start a new root span. Attributes: `culture.trace.origin=remote`, `culture.trace.dropped_reason=malformed|too_long`, `culture.federation.peer=<peer>`. Log a rate-limited warning. Increment `culture.trace.inbound{result=malformed|too_long, peer=<peer>}
|
|
14
|
+
3. Tag present but malformed or over the length cap → drop the tag, start a new root span. Attributes: `culture.trace.origin=remote`, `culture.trace.dropped_reason=malformed|too_long`, `culture.federation.peer=<peer>`. Log a rate-limited warning. Increment `culture.trace.inbound{result=malformed|too_long, peer=<peer>}` (shipped in culture 8.4.0; on every inbound regardless of result, so `result=valid` and `result=missing` are also recorded).
|
|
15
15
|
|
|
16
16
|
**Length caps** (hard-coded, not configurable):
|
|
17
17
|
|
|
@@ -12,15 +12,18 @@ from culture.telemetry.context import (
|
|
|
12
12
|
extract_traceparent_from_tags,
|
|
13
13
|
inject_traceparent,
|
|
14
14
|
)
|
|
15
|
+
from culture.telemetry.metrics import MetricsRegistry, init_metrics
|
|
15
16
|
from culture.telemetry.tracing import init_telemetry
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"ExtractResult",
|
|
20
|
+
"MetricsRegistry",
|
|
19
21
|
"TRACEPARENT_TAG",
|
|
20
22
|
"TRACESTATE_TAG",
|
|
21
23
|
"context_from_traceparent",
|
|
22
24
|
"current_traceparent",
|
|
23
25
|
"extract_traceparent_from_tags",
|
|
26
|
+
"init_metrics",
|
|
24
27
|
"init_telemetry",
|
|
25
28
|
"inject_traceparent",
|
|
26
29
|
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""OpenTelemetry MeterProvider bootstrap for Culture.
|
|
2
|
+
|
|
3
|
+
`init_metrics(config)` is idempotent — safe to call from multiple places.
|
|
4
|
+
When `config.telemetry.enabled` or `metrics_enabled` is False, returns a
|
|
5
|
+
MetricsRegistry whose instruments are bound to OTEL's proxy meter that
|
|
6
|
+
becomes a real meter only if a provider is later installed. In production
|
|
7
|
+
this is effectively no-op (no provider is installed). In tests, callers
|
|
8
|
+
MUST `reset_for_tests()` between disabled-init and the `metrics_reader`
|
|
9
|
+
fixture, otherwise the cached proxy instruments would forward to the
|
|
10
|
+
freshly-installed test provider.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import asdict, dataclass
|
|
17
|
+
|
|
18
|
+
from opentelemetry import metrics
|
|
19
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
|
20
|
+
from opentelemetry.metrics import Counter, Histogram, Meter, UpDownCounter
|
|
21
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
22
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
23
|
+
from opentelemetry.sdk.resources import Resource
|
|
24
|
+
|
|
25
|
+
from culture.agentirc.config import ServerConfig
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
_CULTURE_METER_NAME = "culture.agentirc"
|
|
30
|
+
_initialized_for: dict | None = None
|
|
31
|
+
_meter_provider: "MeterProvider | None" = None
|
|
32
|
+
_registry: "MetricsRegistry | None" = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class MetricsRegistry:
|
|
37
|
+
"""All Plan-3 server-side instruments, registered once during init_metrics.
|
|
38
|
+
|
|
39
|
+
Plan 4/5/6 will extend by adding fields for audit / harness / bots.
|
|
40
|
+
Keep this a single dataclass and grow it — don't spawn parallel
|
|
41
|
+
registries per category.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Message flow
|
|
45
|
+
irc_bytes_sent: Counter
|
|
46
|
+
irc_bytes_received: Counter
|
|
47
|
+
irc_message_size: Histogram
|
|
48
|
+
privmsg_delivered: Counter
|
|
49
|
+
# Events
|
|
50
|
+
events_emitted: Counter
|
|
51
|
+
events_render_duration: Histogram
|
|
52
|
+
# Federation
|
|
53
|
+
s2s_messages: Counter
|
|
54
|
+
s2s_relay_latency: Histogram
|
|
55
|
+
s2s_links_active: UpDownCounter
|
|
56
|
+
s2s_link_events: Counter
|
|
57
|
+
# Clients & sessions
|
|
58
|
+
clients_connected: UpDownCounter
|
|
59
|
+
client_session_duration: Histogram
|
|
60
|
+
client_command_duration: Histogram
|
|
61
|
+
# Trace-context hygiene
|
|
62
|
+
trace_inbound: Counter
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def reset_for_tests() -> None:
|
|
66
|
+
"""Reset module state so each test gets a fresh provider. Test-only."""
|
|
67
|
+
global _initialized_for, _meter_provider, _registry
|
|
68
|
+
if _meter_provider is not None:
|
|
69
|
+
try:
|
|
70
|
+
_meter_provider.shutdown()
|
|
71
|
+
except Exception: # noqa: BLE001
|
|
72
|
+
pass
|
|
73
|
+
_meter_provider = None
|
|
74
|
+
_initialized_for = None
|
|
75
|
+
_registry = None
|
|
76
|
+
# _METER_PROVIDER and Once live on the _internal sub-package, not the
|
|
77
|
+
# top-level metrics module (unlike the trace API which exposes them directly).
|
|
78
|
+
import opentelemetry.metrics._internal as _mi # type: ignore[attr-defined]
|
|
79
|
+
|
|
80
|
+
_mi._METER_PROVIDER = None
|
|
81
|
+
_mi._METER_PROVIDER_SET_ONCE = _mi.Once()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_registry(meter: Meter) -> MetricsRegistry:
|
|
85
|
+
"""Single source of truth for instrument names / units / descriptions.
|
|
86
|
+
|
|
87
|
+
Names and units must match docs/superpowers/specs/2026-04-24-otel-observability-design.md
|
|
88
|
+
Metrics catalog section.
|
|
89
|
+
"""
|
|
90
|
+
return MetricsRegistry(
|
|
91
|
+
# Message flow
|
|
92
|
+
irc_bytes_sent=meter.create_counter(
|
|
93
|
+
"culture.irc.bytes_sent",
|
|
94
|
+
unit="By",
|
|
95
|
+
description="Bytes written to client/peer sockets",
|
|
96
|
+
),
|
|
97
|
+
irc_bytes_received=meter.create_counter(
|
|
98
|
+
"culture.irc.bytes_received",
|
|
99
|
+
unit="By",
|
|
100
|
+
description="Bytes read from client/peer sockets",
|
|
101
|
+
),
|
|
102
|
+
irc_message_size=meter.create_histogram(
|
|
103
|
+
"culture.irc.message.size",
|
|
104
|
+
unit="By",
|
|
105
|
+
description="Per-message byte size at parse time",
|
|
106
|
+
),
|
|
107
|
+
privmsg_delivered=meter.create_counter(
|
|
108
|
+
"culture.privmsg.delivered",
|
|
109
|
+
description="Per-PRIVMSG delivery count, labeled by kind=dm|channel",
|
|
110
|
+
),
|
|
111
|
+
# Events
|
|
112
|
+
events_emitted=meter.create_counter(
|
|
113
|
+
"culture.events.emitted",
|
|
114
|
+
description="Events through IRCd.emit_event, labeled by type and origin",
|
|
115
|
+
),
|
|
116
|
+
events_render_duration=meter.create_histogram(
|
|
117
|
+
"culture.events.render.duration",
|
|
118
|
+
unit="ms",
|
|
119
|
+
description="Time spent in skill hooks + bot dispatch + surfacing",
|
|
120
|
+
),
|
|
121
|
+
# Federation
|
|
122
|
+
s2s_messages=meter.create_counter(
|
|
123
|
+
"culture.s2s.messages",
|
|
124
|
+
description="Inbound S2S messages by verb and peer (outbound deferred)",
|
|
125
|
+
),
|
|
126
|
+
s2s_relay_latency=meter.create_histogram(
|
|
127
|
+
"culture.s2s.relay_latency",
|
|
128
|
+
unit="ms",
|
|
129
|
+
description="Per-event relay duration in ServerLink.relay_event",
|
|
130
|
+
),
|
|
131
|
+
s2s_links_active=meter.create_up_down_counter(
|
|
132
|
+
"culture.s2s.links_active",
|
|
133
|
+
description="Currently active federation links",
|
|
134
|
+
),
|
|
135
|
+
s2s_link_events=meter.create_counter(
|
|
136
|
+
"culture.s2s.link_events",
|
|
137
|
+
description="Federation lifecycle events: connect/disconnect/auth_fail/backfill_*",
|
|
138
|
+
),
|
|
139
|
+
# Clients & sessions
|
|
140
|
+
clients_connected=meter.create_up_down_counter(
|
|
141
|
+
"culture.clients.connected",
|
|
142
|
+
description="Currently connected clients by kind=human|bot|harness",
|
|
143
|
+
),
|
|
144
|
+
client_session_duration=meter.create_histogram(
|
|
145
|
+
"culture.client.session.duration",
|
|
146
|
+
unit="s",
|
|
147
|
+
description="Per-client connection lifetime",
|
|
148
|
+
),
|
|
149
|
+
client_command_duration=meter.create_histogram(
|
|
150
|
+
"culture.client.command.duration",
|
|
151
|
+
unit="ms",
|
|
152
|
+
description="Per-command dispatch duration by verb",
|
|
153
|
+
),
|
|
154
|
+
# Trace-context hygiene
|
|
155
|
+
trace_inbound=meter.create_counter(
|
|
156
|
+
"culture.trace.inbound",
|
|
157
|
+
description="Inbound traceparent extraction outcome by result and peer",
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def init_metrics(config: ServerConfig) -> MetricsRegistry:
|
|
163
|
+
"""Initialize MeterProvider + register instruments. Idempotent.
|
|
164
|
+
|
|
165
|
+
Returns a MetricsRegistry. When telemetry is disabled or
|
|
166
|
+
metrics_enabled is False, instruments are bound to OTEL's proxy meter
|
|
167
|
+
— call sites can `instrument.add(...)` / `.record(...)` unconditionally
|
|
168
|
+
without guards. Production never installs a provider in this case.
|
|
169
|
+
"""
|
|
170
|
+
global _initialized_for, _meter_provider, _registry
|
|
171
|
+
|
|
172
|
+
tcfg = config.telemetry
|
|
173
|
+
# Include config.name so two IRCd instances with identical TelemetryConfig
|
|
174
|
+
# but different names each get their own registry and correct
|
|
175
|
+
# service.instance.id resource attribute.
|
|
176
|
+
snapshot = {"telemetry": asdict(tcfg), "instance": config.name}
|
|
177
|
+
if _initialized_for == snapshot and _registry is not None:
|
|
178
|
+
return _registry
|
|
179
|
+
|
|
180
|
+
# Tear down the previous SDK provider before installing a new one.
|
|
181
|
+
# PeriodicExportingMetricReader spawns a background thread that needs an
|
|
182
|
+
# explicit shutdown call; otherwise tests leak workers and may double-export.
|
|
183
|
+
if _meter_provider is not None:
|
|
184
|
+
try:
|
|
185
|
+
_meter_provider.shutdown()
|
|
186
|
+
except Exception: # noqa: BLE001 - shutdown errors must not crash init
|
|
187
|
+
logger.debug("MeterProvider shutdown failed", exc_info=True)
|
|
188
|
+
_meter_provider = None
|
|
189
|
+
|
|
190
|
+
if not tcfg.enabled or not tcfg.metrics_enabled:
|
|
191
|
+
meter = metrics.get_meter(_CULTURE_METER_NAME)
|
|
192
|
+
_registry = _build_registry(meter)
|
|
193
|
+
_initialized_for = snapshot
|
|
194
|
+
return _registry
|
|
195
|
+
|
|
196
|
+
resource = Resource.create(
|
|
197
|
+
{
|
|
198
|
+
"service.name": tcfg.service_name,
|
|
199
|
+
"service.instance.id": config.name,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
exporter = OTLPMetricExporter(
|
|
203
|
+
endpoint=tcfg.otlp_endpoint,
|
|
204
|
+
timeout=tcfg.otlp_timeout_ms / 1000.0,
|
|
205
|
+
compression=(None if tcfg.otlp_compression == "none" else tcfg.otlp_compression),
|
|
206
|
+
)
|
|
207
|
+
reader = PeriodicExportingMetricReader(
|
|
208
|
+
exporter=exporter,
|
|
209
|
+
export_interval_millis=tcfg.metrics_export_interval_ms,
|
|
210
|
+
)
|
|
211
|
+
provider = MeterProvider(resource=resource, metric_readers=[reader])
|
|
212
|
+
metrics.set_meter_provider(provider)
|
|
213
|
+
_meter_provider = provider
|
|
214
|
+
|
|
215
|
+
meter = metrics.get_meter(_CULTURE_METER_NAME)
|
|
216
|
+
_registry = _build_registry(meter)
|
|
217
|
+
_initialized_for = snapshot
|
|
218
|
+
logger.info(
|
|
219
|
+
"OTEL metrics initialized: service=%s instance=%s endpoint=%s interval=%dms",
|
|
220
|
+
tcfg.service_name,
|
|
221
|
+
config.name,
|
|
222
|
+
tcfg.otlp_endpoint,
|
|
223
|
+
tcfg.metrics_export_interval_ms,
|
|
224
|
+
)
|
|
225
|
+
return _registry
|
|
@@ -78,7 +78,10 @@ def init_telemetry(config: ServerConfig) -> Tracer:
|
|
|
78
78
|
tcfg = config.telemetry
|
|
79
79
|
# Compare against an immutable snapshot so in-place mutation of the
|
|
80
80
|
# caller's TelemetryConfig is detected (the dataclass is not frozen).
|
|
81
|
-
|
|
81
|
+
# Include config.name so two IRCd instances with identical TelemetryConfig
|
|
82
|
+
# but different names each get their own tracer and correct
|
|
83
|
+
# service.instance.id resource attribute. Mirrors metrics.py for parity.
|
|
84
|
+
snapshot = {"telemetry": asdict(tcfg), "instance": config.name}
|
|
82
85
|
if _initialized_for == snapshot and _tracer is not None:
|
|
83
86
|
return _tracer
|
|
84
87
|
|