agentirc-cli 8.2.0__tar.gz → 8.3.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.2.0 → agentirc_cli-8.3.0}/CHANGELOG.md +21 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/PKG-INFO +1 -1
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/client.py +107 -109
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/server_link.py +161 -42
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/tracing.md +6 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/telemetry/__init__.py +4 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/telemetry/context.py +37 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/telemetry.md +24 -4
- agentirc_cli-8.3.0/docs/superpowers/plans/2026-04-25-otel-federation.md +229 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/pyproject.toml +1 -1
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_context.py +42 -0
- agentirc_cli-8.3.0/tests/telemetry/test_federation_propagation.py +141 -0
- agentirc_cli-8.3.0/tests/telemetry/test_s2s_dispatch_span.py +156 -0
- agentirc_cli-8.3.0/tests/telemetry/test_s2s_relay_span.py +114 -0
- agentirc_cli-8.3.0/tests/telemetry/test_s2s_session_span.py +64 -0
- agentirc_cli-8.3.0/tests/telemetry/test_server_link_inject.py +124 -0
- agentirc_cli-8.3.0/tests/telemetry/test_session_span.py +105 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_federation.py +47 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/uv.lock +1 -1
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/agents/doc-test-alignment.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.flake8 +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/docs-check.yml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/security-checks.yml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.gitignore +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.pr_agent.toml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.pylintrc +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/CLAUDE.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/Gemfile +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/Gemfile.lock +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/LICENSE +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/README.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/SECURITY.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_config.base.yml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_config.culture.yml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_data/sites.yml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_includes/head_custom.html +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_sass/color_schemes/dark-terminal.scss +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/IMG_3183.png +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/apple-touch-icon.png +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/favicon-16x16.png +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/favicon-32x32.png +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/favicon.ico +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/og-agentirc.png +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/og-culture.png +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/__main__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/CLAUDE.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/__main__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/channel.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc-architecture.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc-features.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc-skill.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/events.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/history_store.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/ircd.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/remote_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/room_store.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/rooms_util.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skill.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/history.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/icon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/rooms.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/threads.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/thread_store.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/aio.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/bot.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/bot_manager.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/filter_dsl.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/http_listener.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/welcome/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/welcome/bot.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/welcome/handler.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/template_engine.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/virtual_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/_passthrough.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/afi.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/agent.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/bot.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/channel.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/devex.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/introspect.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/mesh.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/server.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/constants.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/display.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/formatting.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/mesh.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/process.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/skills.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/culture.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/irc_transport.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/message_buffer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/socket_server.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/supervisor.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/webhook.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/__main__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/culture.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/socket_server.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/supervisor.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/webhook.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/culture.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/socket_server.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/supervisor.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/webhook.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/culture.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/webhook.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/app.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/commands.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/status.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/chat.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/info_panel.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/sidebar.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/constants.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/credentials.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/formatting.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/learn_prompt.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/mesh_config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/observer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/collector.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/model.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/renderer_text.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/renderer_web.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/web/style.css +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/persistence.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/pidfile.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/commands.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/events.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/federation.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/history.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/icons.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/tags.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/threads.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/message.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/protocol-index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/replies.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/skills/culture/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/telemetry/tracing.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/README.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/architecture-overview.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/bots.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/events.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/otelcol-template.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/why-agentirc.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/agent-lifecycle.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/choose-a-harness.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/features.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/mental-model.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/operate.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/patterns.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/quickstart.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/reflective-development.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/vision-patterns-index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/vision.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/what-is-culture.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/agent-harness-spec.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/layers.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/subsites.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/threads.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/afi.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/commands.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/devex.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/console.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/acp.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/claude.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/codex.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/copilot.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/architecture.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/config.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/deployment.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/security.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/resources/positioning.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/federation.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/harnesses.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/humans-and-agents.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/persistence.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/rooms.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/demos/magic-demo.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/first-session.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/join-as-human.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/local-setup.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/multi-machine.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/10-agent-lifecycle.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases-index.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-12-console-enhancements.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-15-mesh-events.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-18-culture-dev-positioning.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-22-agex-integration.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-24-otel-foundation.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-12-console-enhancements-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-15-mesh-events-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-17-sites-repositioning-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-18-culture-dev-positioning-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-22-agex-integration-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-24-otel-observability-design.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/favicon.ico +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/culture.yaml +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/robots.txt +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sitemap-agentirc.html +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sitemap-main.html +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sitemap.html +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sonar-project.properties +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/conftest.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/__init__.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/_fakes.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_config_load.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_dispatch_span.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_emit_event_span.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_outbound_inject.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_parse_error.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_privmsg_span.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_server_init.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_tracing.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_archive.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot_config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot_config_fires_event_toplevel.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot_manager.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bots_integration.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_channel.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_channel_cli.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_afi.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_devex.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_introspect.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_passthrough.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_connection.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_chat_markdown.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_commands.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_connection.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_fixes_224_227.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_icons.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_integration.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_status.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_credentials.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_culture_config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_display.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_basic.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_bot_chain.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_bot_trigger.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_cap_fallback.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_catalog.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_federation.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_history.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_lifecycle.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_reserved_nick.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_filter_dsl.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_history.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_http_listener.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_irc_transport_tags.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_learn_prompt.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_manifest_config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mention_alias.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mention_target_cleanup.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mention_warning.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mesh_readiness.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_message.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_message_tags.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_migrate_cli.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_modes.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_model.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_web.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_persistence.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_persistence_timeout.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_pidfile.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_poll_loop.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_register_cli.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_rooms.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_skill_docs.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_skills.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_template_engine.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_threads.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_virtual_client.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_wait_for_port.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_webhook.py +0 -0
- {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_welcome_bot.py +0 -0
|
@@ -4,6 +4,27 @@ 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.3.0] - 2026-04-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `irc.s2s.session` span over ServerLink connection lifetime.
|
|
12
|
+
- `irc.s2s.<VERB>` per-verb spans on inbound federation messages with traceparent extraction and the inbound mitigation rules from `culture/protocol/extensions/tracing.md`.
|
|
13
|
+
- `irc.s2s.relay` span on outbound relay enforcing the re-sign-per-hop rule.
|
|
14
|
+
- `irc.client.session` span over Client connection lifetime (#290).
|
|
15
|
+
- `irc.join` and `irc.part` spans (#290).
|
|
16
|
+
- Public `culture.telemetry.context_from_traceparent` and `culture.telemetry.current_traceparent` helpers.
|
|
17
|
+
- Single traceparent injection choke point at `ServerLink.send_raw`.
|
|
18
|
+
- End-to-end propagation tests proving one `trace_id` spans federated client → server → relay → server hops.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `Client._dispatch` span name and `irc.command` attribute now uppercase, matching `ServerLink._dispatch` convention.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `_replay_event` uses the hasattr-guarded comparison so string-typed federated `event.type` no longer skips the typed fast path. (#291)
|
|
27
|
+
|
|
7
28
|
## [8.2.0] - 2026-04-24
|
|
8
29
|
|
|
9
30
|
### Added
|
|
@@ -7,8 +7,8 @@ import re
|
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
9
|
from opentelemetry import trace as _otel_trace
|
|
10
|
-
from opentelemetry.
|
|
11
|
-
from opentelemetry.trace
|
|
10
|
+
from opentelemetry.context import Context as _OtelContext
|
|
11
|
+
from opentelemetry.trace import Span as _OtelSpan
|
|
12
12
|
|
|
13
13
|
from culture.agentirc.channel import Channel
|
|
14
14
|
from culture.agentirc.skill import Event, EventType
|
|
@@ -18,6 +18,8 @@ from culture.protocol import replies
|
|
|
18
18
|
from culture.protocol.message import Message
|
|
19
19
|
from culture.telemetry.context import TRACEPARENT_TAG as _TP_TAG_NAME
|
|
20
20
|
from culture.telemetry.context import (
|
|
21
|
+
context_from_traceparent,
|
|
22
|
+
current_traceparent,
|
|
21
23
|
extract_traceparent_from_tags,
|
|
22
24
|
)
|
|
23
25
|
from culture.telemetry.context import inject_traceparent as _inject_traceparent
|
|
@@ -25,39 +27,12 @@ from culture.telemetry.context import inject_traceparent as _inject_traceparent
|
|
|
25
27
|
# OTEL instrumentation name (must match `_CULTURE_TRACER_NAME` in
|
|
26
28
|
# culture/telemetry/tracing.py so trace consumers see one consistent value).
|
|
27
29
|
_TRACER_NAME = "culture.agentirc"
|
|
28
|
-
# Span attribute keys
|
|
29
|
-
#
|
|
30
|
+
# Span attribute keys, defined once so a future rename / sanitization layer
|
|
31
|
+
# has one edit point.
|
|
30
32
|
_ATTR_BODY = "irc.message.body"
|
|
31
33
|
_ATTR_SIZE = "irc.message.size"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _context_from_traceparent(tp: str):
|
|
35
|
-
"""Build an OTEL context whose current span is a NonRecordingSpan
|
|
36
|
-
synthesized from a W3C traceparent string. The `_dispatch` span we
|
|
37
|
-
start next will be a child of this context."""
|
|
38
|
-
# Format: 00-<trace-id>-<parent-id>-<flags>
|
|
39
|
-
_, trace_hex, parent_hex, flags_hex = tp.split("-")
|
|
40
|
-
span_ctx = SpanContext(
|
|
41
|
-
trace_id=int(trace_hex, 16),
|
|
42
|
-
span_id=int(parent_hex, 16),
|
|
43
|
-
is_remote=True,
|
|
44
|
-
trace_flags=TraceFlags(int(flags_hex, 16)),
|
|
45
|
-
)
|
|
46
|
-
return set_span_in_context(NonRecordingSpan(span_ctx))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _current_traceparent() -> str | None:
|
|
50
|
-
"""Return the W3C traceparent for the currently-active span, or None
|
|
51
|
-
if no span is recording (no-op tracer / sampler dropped).
|
|
52
|
-
"""
|
|
53
|
-
span = _otel_trace.get_current_span()
|
|
54
|
-
ctx = span.get_span_context()
|
|
55
|
-
if not ctx.is_valid:
|
|
56
|
-
return None
|
|
57
|
-
return (
|
|
58
|
-
f"00-{format(ctx.trace_id, '032x')}-{format(ctx.span_id, '016x')}"
|
|
59
|
-
f"-{format(int(ctx.trace_flags), '02x')}"
|
|
60
|
-
)
|
|
34
|
+
_ATTR_NICK = "irc.client.nick"
|
|
35
|
+
_ATTR_CHANNEL = "irc.channel"
|
|
61
36
|
|
|
62
37
|
|
|
63
38
|
if TYPE_CHECKING:
|
|
@@ -86,6 +61,7 @@ class Client:
|
|
|
86
61
|
self.caps: set[str] = set()
|
|
87
62
|
self.modes: set[str] = set()
|
|
88
63
|
self.icon: str | None = None
|
|
64
|
+
self._session_span: _OtelSpan | None = None
|
|
89
65
|
|
|
90
66
|
@property
|
|
91
67
|
def prefix(self) -> str:
|
|
@@ -97,7 +73,7 @@ class Client:
|
|
|
97
73
|
# block and `send_tagged`'s tag-stripping for non-capable clients
|
|
98
74
|
# would be undone here.
|
|
99
75
|
if "message-tags" in self.caps:
|
|
100
|
-
tp =
|
|
76
|
+
tp = current_traceparent()
|
|
101
77
|
if tp is not None:
|
|
102
78
|
_inject_traceparent(message, traceparent=tp, tracestate=None)
|
|
103
79
|
try:
|
|
@@ -114,7 +90,7 @@ class Client:
|
|
|
114
90
|
AND the client negotiated the `message-tags` capability.
|
|
115
91
|
"""
|
|
116
92
|
if "message-tags" in self.caps:
|
|
117
|
-
tp =
|
|
93
|
+
tp = current_traceparent()
|
|
118
94
|
if tp is not None:
|
|
119
95
|
# send_raw takes a pre-formatted line without an existing tag
|
|
120
96
|
# block; prefix a fresh @tag.
|
|
@@ -171,30 +147,42 @@ class Client:
|
|
|
171
147
|
return buffer
|
|
172
148
|
|
|
173
149
|
async def handle(self, initial_msg: str | None = None) -> None:
|
|
174
|
-
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
150
|
+
peer_info = self.writer.get_extra_info("peername")
|
|
151
|
+
remote_addr = f"{peer_info[0]}:{peer_info[1]}" if peer_info else ""
|
|
152
|
+
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
153
|
+
"irc.client.session",
|
|
154
|
+
attributes={"irc.client.remote_addr": remote_addr},
|
|
155
|
+
) as span:
|
|
156
|
+
self._session_span = span
|
|
157
|
+
try:
|
|
158
|
+
buffer = ""
|
|
159
|
+
if initial_msg:
|
|
160
|
+
buffer = initial_msg.replace("\r\n", "\n").replace("\r", "\n")
|
|
161
|
+
buffer = await self._process_buffer(buffer)
|
|
162
|
+
while True:
|
|
163
|
+
data = await self.reader.read(4096)
|
|
164
|
+
if not data:
|
|
165
|
+
break
|
|
166
|
+
buffer += data.decode("utf-8", errors="replace")
|
|
167
|
+
# Cap buffer to prevent unbounded memory growth (512 bytes per RFC 2812)
|
|
168
|
+
if len(buffer) > 8192:
|
|
169
|
+
buffer = buffer[-4096:]
|
|
170
|
+
# Normalize all line endings to \n for simpler parsing
|
|
171
|
+
buffer = buffer.replace("\r\n", "\n").replace("\r", "\n")
|
|
172
|
+
buffer = await self._process_buffer(buffer)
|
|
173
|
+
except (ConnectionError, asyncio.IncompleteReadError):
|
|
174
|
+
pass
|
|
189
175
|
|
|
190
176
|
async def _dispatch(self, msg: Message) -> None:
|
|
191
177
|
extract = extract_traceparent_from_tags(msg, peer=None)
|
|
192
|
-
parent_ctx = None
|
|
193
178
|
if extract.status == "valid":
|
|
194
|
-
parent_ctx =
|
|
179
|
+
parent_ctx: _OtelContext | None = context_from_traceparent(extract.traceparent)
|
|
180
|
+
else:
|
|
181
|
+
parent_ctx = _OtelContext() # force root: detach from session span
|
|
195
182
|
|
|
183
|
+
verb = msg.command.upper()
|
|
196
184
|
attrs = {
|
|
197
|
-
"irc.command":
|
|
185
|
+
"irc.command": verb,
|
|
198
186
|
"irc.prefix_nick": (msg.prefix.split("!")[0] if msg.prefix else ""),
|
|
199
187
|
"culture.trace.origin": "local" if extract.status == "missing" else "remote",
|
|
200
188
|
}
|
|
@@ -203,7 +191,7 @@ class Client:
|
|
|
203
191
|
|
|
204
192
|
# Per-call get_tracer: test fixture swaps provider between tests.
|
|
205
193
|
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
206
|
-
f"irc.command.{
|
|
194
|
+
f"irc.command.{verb}",
|
|
207
195
|
context=parent_ctx,
|
|
208
196
|
attributes=attrs,
|
|
209
197
|
):
|
|
@@ -304,6 +292,8 @@ class Client:
|
|
|
304
292
|
|
|
305
293
|
self.nick = nick
|
|
306
294
|
self.server.clients[nick] = self
|
|
295
|
+
if self._session_span is not None:
|
|
296
|
+
self._session_span.set_attribute(_ATTR_NICK, nick)
|
|
307
297
|
await self._try_register()
|
|
308
298
|
|
|
309
299
|
async def _handle_user(self, msg: Message) -> None:
|
|
@@ -355,45 +345,49 @@ class Client:
|
|
|
355
345
|
return
|
|
356
346
|
|
|
357
347
|
channel_name = msg.params[0]
|
|
358
|
-
|
|
359
|
-
|
|
348
|
+
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
349
|
+
"irc.join",
|
|
350
|
+
attributes={_ATTR_CHANNEL: channel_name, _ATTR_NICK: self.nick or ""},
|
|
351
|
+
):
|
|
352
|
+
if not channel_name.startswith("#"):
|
|
353
|
+
return
|
|
360
354
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
355
|
+
# Block joins to archived rooms
|
|
356
|
+
existing = self.server.channels.get(channel_name)
|
|
357
|
+
if existing and existing.archived:
|
|
358
|
+
await self.send(
|
|
359
|
+
Message(
|
|
360
|
+
prefix=self.server.config.name,
|
|
361
|
+
command="NOTICE",
|
|
362
|
+
params=[self.nick, f"{channel_name} is archived and cannot be joined"],
|
|
363
|
+
)
|
|
369
364
|
)
|
|
370
|
-
|
|
371
|
-
return
|
|
365
|
+
return
|
|
372
366
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
367
|
+
channel = self.server.get_or_create_channel(channel_name)
|
|
368
|
+
if self in channel.members:
|
|
369
|
+
return
|
|
376
370
|
|
|
377
|
-
|
|
378
|
-
|
|
371
|
+
channel.add(self)
|
|
372
|
+
self.channels.add(channel)
|
|
379
373
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
374
|
+
# Notify all channel members (including self)
|
|
375
|
+
join_msg = Message(prefix=self.prefix, command="JOIN", params=[channel_name])
|
|
376
|
+
for member in list(channel.members):
|
|
377
|
+
await member.send(join_msg)
|
|
384
378
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
379
|
+
# Send topic if set
|
|
380
|
+
if channel.topic:
|
|
381
|
+
await self.send_numeric(replies.RPL_TOPIC, channel_name, channel.topic)
|
|
388
382
|
|
|
389
|
-
|
|
390
|
-
|
|
383
|
+
# Send names list
|
|
384
|
+
await self._send_names(channel)
|
|
391
385
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
386
|
+
# Emit event AFTER delivering all join-related numerics (topic, NAMES)
|
|
387
|
+
# so that the event PRIVMSG doesn't interleave with 353/366 in client buffers.
|
|
388
|
+
await self.server.emit_event(
|
|
389
|
+
Event(type=EventType.JOIN, channel=channel_name, nick=self.nick)
|
|
390
|
+
)
|
|
397
391
|
|
|
398
392
|
async def _handle_part(self, msg: Message) -> None:
|
|
399
393
|
if not msg.params:
|
|
@@ -401,36 +395,40 @@ class Client:
|
|
|
401
395
|
return
|
|
402
396
|
|
|
403
397
|
channel_name = msg.params[0]
|
|
404
|
-
|
|
398
|
+
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
399
|
+
"irc.part",
|
|
400
|
+
attributes={_ATTR_CHANNEL: channel_name, _ATTR_NICK: self.nick or ""},
|
|
401
|
+
):
|
|
402
|
+
reason = msg.params[1] if len(msg.params) > 1 else ""
|
|
405
403
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
404
|
+
channel = self.server.channels.get(channel_name)
|
|
405
|
+
if not channel or self not in channel.members:
|
|
406
|
+
await self.send_numeric(
|
|
407
|
+
replies.ERR_NOTONCHANNEL,
|
|
408
|
+
channel_name,
|
|
409
|
+
replies.MSG_NOTONCHANNEL,
|
|
410
|
+
)
|
|
411
|
+
return
|
|
414
412
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
part_params = [channel_name, reason] if reason else [channel_name]
|
|
414
|
+
part_msg = Message(prefix=self.prefix, command="PART", params=part_params)
|
|
415
|
+
for member in list(channel.members):
|
|
416
|
+
await member.send(part_msg)
|
|
419
417
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
418
|
+
await self.server.emit_event(
|
|
419
|
+
Event(
|
|
420
|
+
type=EventType.PART,
|
|
421
|
+
channel=channel_name,
|
|
422
|
+
nick=self.nick,
|
|
423
|
+
data={"reason": reason},
|
|
424
|
+
)
|
|
426
425
|
)
|
|
427
|
-
)
|
|
428
426
|
|
|
429
|
-
|
|
430
|
-
|
|
427
|
+
channel.remove(self)
|
|
428
|
+
self.channels.discard(channel)
|
|
431
429
|
|
|
432
|
-
|
|
433
|
-
|
|
430
|
+
if not channel.members and not channel.persistent:
|
|
431
|
+
del self.server.channels[channel_name]
|
|
434
432
|
|
|
435
433
|
async def _handle_topic(self, msg: Message) -> None:
|
|
436
434
|
if not msg.params:
|
|
@@ -7,12 +7,21 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
|
+
from opentelemetry import trace as otel_trace
|
|
11
|
+
from opentelemetry.context import Context as _OtelContext
|
|
12
|
+
|
|
10
13
|
from culture.agentirc.remote_client import RemoteClient
|
|
11
14
|
from culture.agentirc.skill import Event, EventType
|
|
12
15
|
from culture.aio import maybe_await
|
|
13
16
|
from culture.bots.virtual_client import VirtualClient
|
|
14
17
|
from culture.constants import SYSTEM_USER_PREFIX
|
|
15
18
|
from culture.protocol.message import Message
|
|
19
|
+
from culture.telemetry import context_from_traceparent, current_traceparent
|
|
20
|
+
from culture.telemetry.context import TRACEPARENT_TAG, extract_traceparent_from_tags
|
|
21
|
+
|
|
22
|
+
# OTEL instrumentation name (must match `_CULTURE_TRACER_NAME` in
|
|
23
|
+
# culture/telemetry/tracing.py so all spans go through one tracer instance).
|
|
24
|
+
_TRACER_NAME = "culture.agentirc"
|
|
16
25
|
|
|
17
26
|
if TYPE_CHECKING:
|
|
18
27
|
from culture.agentirc.ircd import IRCd
|
|
@@ -20,6 +29,64 @@ if TYPE_CHECKING:
|
|
|
20
29
|
logger = logging.getLogger(__name__)
|
|
21
30
|
|
|
22
31
|
|
|
32
|
+
def _prepend_trace_tags(line: str, tp: str) -> str:
|
|
33
|
+
"""Inject *tp* as the ``culture.dev/traceparent`` IRCv3 tag on *line*.
|
|
34
|
+
|
|
35
|
+
- Empty line → returned unchanged (defensive no-op).
|
|
36
|
+
- Line with no existing tag block (does not start with ``@``) → prepend
|
|
37
|
+
``@culture.dev/traceparent=<tp> `` before the rest of the line.
|
|
38
|
+
- Line that already has a tag block (starts with ``@``) → merge the tag
|
|
39
|
+
into the existing block. If the block already contains
|
|
40
|
+
``culture.dev/traceparent``, its value is replaced with *tp*; otherwise
|
|
41
|
+
the new tag is appended with a ``;`` separator.
|
|
42
|
+
|
|
43
|
+
The helper is intentionally lenient: if the tag block is somehow
|
|
44
|
+
ill-formed the existing text is preserved and the new tag is appended —
|
|
45
|
+
tagging is best-effort, never load-bearing.
|
|
46
|
+
|
|
47
|
+
.. note:: Parsing limitation
|
|
48
|
+
|
|
49
|
+
This helper does not fully parse or unescape IRCv3 tag values; it
|
|
50
|
+
simply splits the tag block on ``;`` and separates each tag's key from
|
|
51
|
+
its value on the first ``=``. This is safe for current ``send_raw``
|
|
52
|
+
callers, which today emit no tags of their own. Revisit if a caller
|
|
53
|
+
begins authoring tags whose values may contain IRCv3-escaped
|
|
54
|
+
semicolons.
|
|
55
|
+
"""
|
|
56
|
+
if not line:
|
|
57
|
+
return line
|
|
58
|
+
|
|
59
|
+
if not line.startswith("@"):
|
|
60
|
+
return f"@{TRACEPARENT_TAG}={tp} {line}"
|
|
61
|
+
|
|
62
|
+
# Split off the tag block: everything up to the first space, then the rest.
|
|
63
|
+
space_idx = line.find(" ")
|
|
64
|
+
if space_idx == -1:
|
|
65
|
+
# Malformed: entire line is a tag block with no message body.
|
|
66
|
+
# Append the tag and move on.
|
|
67
|
+
return f"{line};{TRACEPARENT_TAG}={tp}"
|
|
68
|
+
|
|
69
|
+
tag_block = line[1:space_idx] # strip leading '@'
|
|
70
|
+
rest = line[space_idx + 1 :]
|
|
71
|
+
|
|
72
|
+
# Split existing tags on ';', drop empty entries (e.g. from an empty tag
|
|
73
|
+
# block like "@ :rest"), then replace or append the traceparent tag.
|
|
74
|
+
tags = [t for t in tag_block.split(";") if t]
|
|
75
|
+
replaced = False
|
|
76
|
+
new_tags = []
|
|
77
|
+
for tag in tags:
|
|
78
|
+
key = tag.split("=", 1)[0]
|
|
79
|
+
if key == TRACEPARENT_TAG:
|
|
80
|
+
new_tags.append(f"{TRACEPARENT_TAG}={tp}")
|
|
81
|
+
replaced = True
|
|
82
|
+
else:
|
|
83
|
+
new_tags.append(tag)
|
|
84
|
+
if not replaced:
|
|
85
|
+
new_tags.append(f"{TRACEPARENT_TAG}={tp}")
|
|
86
|
+
|
|
87
|
+
return f"@{';'.join(new_tags)} {rest}"
|
|
88
|
+
|
|
89
|
+
|
|
23
90
|
class ServerLink:
|
|
24
91
|
"""A server-to-server link to a peer IRCd."""
|
|
25
92
|
|
|
@@ -47,6 +114,7 @@ class ServerLink:
|
|
|
47
114
|
self._peer_pass: str | None = None
|
|
48
115
|
self.last_seen_seq: int = 0
|
|
49
116
|
self._squit_received: bool = False
|
|
117
|
+
self._session_span: otel_trace.Span | None = None
|
|
50
118
|
|
|
51
119
|
def should_relay(self, channel_name: str) -> bool:
|
|
52
120
|
"""Check if a channel event should be relayed over this link."""
|
|
@@ -62,6 +130,12 @@ class ServerLink:
|
|
|
62
130
|
return False
|
|
63
131
|
|
|
64
132
|
async def send_raw(self, line: str) -> None:
|
|
133
|
+
tp = current_traceparent()
|
|
134
|
+
if tp:
|
|
135
|
+
try:
|
|
136
|
+
line = _prepend_trace_tags(line, tp)
|
|
137
|
+
except Exception: # noqa: BLE001 - telemetry must never break the link
|
|
138
|
+
logger.debug("traceparent injection failed; sending untagged", exc_info=True)
|
|
65
139
|
try:
|
|
66
140
|
self.writer.write(f"{line}\r\n".encode("utf-8"))
|
|
67
141
|
await self.writer.drain()
|
|
@@ -87,40 +161,68 @@ class ServerLink:
|
|
|
87
161
|
|
|
88
162
|
async def handle(self, initial_msg: str | None = None) -> None:
|
|
89
163
|
"""Main S2S connection loop."""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
buffer = await self._process_buffer(buffer)
|
|
98
|
-
|
|
99
|
-
while True:
|
|
100
|
-
data = await self.reader.read(4096)
|
|
101
|
-
if not data:
|
|
102
|
-
break
|
|
103
|
-
buffer += data.decode("utf-8", errors="replace")
|
|
104
|
-
buffer = buffer.replace("\r\n", "\n").replace("\r", "\n")
|
|
105
|
-
buffer = await self._process_buffer(buffer)
|
|
106
|
-
except (ConnectionError, asyncio.IncompleteReadError):
|
|
107
|
-
pass
|
|
108
|
-
finally:
|
|
109
|
-
await self.server._remove_link(self, squit=self._squit_received)
|
|
110
|
-
self.writer.close()
|
|
164
|
+
tracer = otel_trace.get_tracer(_TRACER_NAME)
|
|
165
|
+
direction = "outbound" if self.initiator else "inbound"
|
|
166
|
+
with tracer.start_as_current_span(
|
|
167
|
+
"irc.s2s.session",
|
|
168
|
+
attributes={"s2s.direction": direction},
|
|
169
|
+
) as span:
|
|
170
|
+
self._session_span = span
|
|
111
171
|
try:
|
|
112
|
-
|
|
113
|
-
|
|
172
|
+
if self.initiator:
|
|
173
|
+
await self._send_handshake()
|
|
174
|
+
|
|
175
|
+
buffer = ""
|
|
176
|
+
if initial_msg:
|
|
177
|
+
buffer = initial_msg + "\n"
|
|
178
|
+
buffer = await self._process_buffer(buffer)
|
|
179
|
+
|
|
180
|
+
while True:
|
|
181
|
+
data = await self.reader.read(4096)
|
|
182
|
+
if not data:
|
|
183
|
+
break
|
|
184
|
+
buffer += data.decode("utf-8", errors="replace")
|
|
185
|
+
buffer = buffer.replace("\r\n", "\n").replace("\r", "\n")
|
|
186
|
+
buffer = await self._process_buffer(buffer)
|
|
187
|
+
except (ConnectionError, asyncio.IncompleteReadError):
|
|
114
188
|
pass
|
|
189
|
+
finally:
|
|
190
|
+
await self.server._remove_link(self, squit=self._squit_received)
|
|
191
|
+
self.writer.close()
|
|
192
|
+
try:
|
|
193
|
+
await self.writer.wait_closed()
|
|
194
|
+
except ConnectionError:
|
|
195
|
+
pass
|
|
115
196
|
|
|
116
197
|
async def _send_handshake(self) -> None:
|
|
117
198
|
await self.send_raw(f"PASS {self.password}")
|
|
118
199
|
await self.send_raw(f"SERVER {self.server.config.name} 1 :{self.server.config.name} IRC")
|
|
119
200
|
|
|
120
201
|
async def _dispatch(self, msg: Message) -> None:
|
|
202
|
+
verb = msg.command.upper()
|
|
121
203
|
handler = getattr(self, f"_handle_{msg.command.lower()}", None)
|
|
122
|
-
|
|
123
|
-
|
|
204
|
+
|
|
205
|
+
extracted = extract_traceparent_from_tags(msg, peer=self.peer_name)
|
|
206
|
+
if extracted.status == "valid":
|
|
207
|
+
parent_ctx: _OtelContext | None = context_from_traceparent(extracted.traceparent)
|
|
208
|
+
else:
|
|
209
|
+
parent_ctx = _OtelContext() # force root: detach from session span
|
|
210
|
+
|
|
211
|
+
attrs = {
|
|
212
|
+
"irc.command": verb,
|
|
213
|
+
"culture.trace.origin": "remote",
|
|
214
|
+
"culture.federation.peer": self.peer_name or "",
|
|
215
|
+
}
|
|
216
|
+
if extracted.status in ("malformed", "too_long"):
|
|
217
|
+
attrs["culture.trace.dropped_reason"] = extracted.status
|
|
218
|
+
|
|
219
|
+
with otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
220
|
+
f"irc.s2s.{verb}",
|
|
221
|
+
context=parent_ctx,
|
|
222
|
+
attributes=attrs,
|
|
223
|
+
):
|
|
224
|
+
if handler:
|
|
225
|
+
await maybe_await(handler(msg))
|
|
124
226
|
|
|
125
227
|
# --- Handshake handlers ---
|
|
126
228
|
|
|
@@ -189,6 +291,8 @@ class ServerLink:
|
|
|
189
291
|
await self._validate_peer_credentials()
|
|
190
292
|
|
|
191
293
|
self._authenticated = True
|
|
294
|
+
if self._session_span is not None:
|
|
295
|
+
self._session_span.set_attribute("s2s.peer", self.peer_name)
|
|
192
296
|
self.server.links[self.peer_name] = self
|
|
193
297
|
self.server.cancel_link_retry(self.peer_name)
|
|
194
298
|
|
|
@@ -818,7 +922,11 @@ class ServerLink:
|
|
|
818
922
|
async def _replay_event(self, seq: int, event: Event) -> None:
|
|
819
923
|
"""Replay a single event to the peer as S2S wire format."""
|
|
820
924
|
origin = self.server.config.name
|
|
821
|
-
|
|
925
|
+
# Federated events arrive with event.type as either an EventType enum
|
|
926
|
+
# or a plain string (when _parse_event_type sees an unknown wire type).
|
|
927
|
+
# Compare against the .value to handle both shapes — see #291.
|
|
928
|
+
event_type_str = event.type.value if hasattr(event.type, "value") else str(event.type)
|
|
929
|
+
if event_type_str == EventType.MESSAGE.value:
|
|
822
930
|
target = event.channel or event.data.get("target", "")
|
|
823
931
|
text = event.data.get("text", "")
|
|
824
932
|
# Filter channel messages through trust check
|
|
@@ -833,23 +941,34 @@ class ServerLink:
|
|
|
833
941
|
|
|
834
942
|
async def relay_event(self, event: Event) -> None:
|
|
835
943
|
"""Relay a local event to the peer in S2S wire format."""
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
944
|
+
event_type_str = event.type.value if hasattr(event.type, "value") else str(event.type)
|
|
945
|
+
attrs = {
|
|
946
|
+
"event.type": event_type_str,
|
|
947
|
+
"s2s.peer": self.peer_name or "",
|
|
948
|
+
}
|
|
949
|
+
# Single span name (no verb suffix): the wire verb is decided downstream
|
|
950
|
+
# by _RELAY_DISPATCH or the SEVENT fallback, after this span opens.
|
|
951
|
+
with otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
952
|
+
"irc.s2s.relay", attributes=attrs
|
|
953
|
+
):
|
|
954
|
+
origin = self.server.config.name
|
|
955
|
+
handler = self._RELAY_DISPATCH.get(event.type)
|
|
956
|
+
if handler:
|
|
957
|
+
await maybe_await(handler(self, event, origin))
|
|
958
|
+
return
|
|
841
959
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
960
|
+
# If no typed relay exists, fall back to generic SEVENT.
|
|
961
|
+
# v1 assumes all peers support SEVENT; cap negotiation is deferred — see plan task 12.
|
|
962
|
+
payload = self.server._build_event_payload(event)
|
|
963
|
+
encoded = self.server._encode_event_data(payload, event_type_str)
|
|
964
|
+
target = event.channel or "*"
|
|
965
|
+
# Egress trust check: channel-scoped events respect should_relay; global events always relay
|
|
966
|
+
if event.channel is not None and not self.should_relay(event.channel):
|
|
967
|
+
return
|
|
968
|
+
seq = self.server._seq # current local seq; peer stores but doesn't re-sequence
|
|
969
|
+
await self.send_raw(
|
|
970
|
+
f":{origin} SEVENT {origin} {seq} {event_type_str} {target} :{encoded}"
|
|
971
|
+
)
|
|
853
972
|
|
|
854
973
|
async def _relay_message(self, event: Event, origin: str) -> None:
|
|
855
974
|
target = event.channel or event.data.get("target", "")
|
|
@@ -29,3 +29,9 @@ User on `spark` sends:
|
|
|
29
29
|
@culture.dev/traceparent=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 PRIVMSG #general :hi
|
|
30
30
|
|
|
31
31
|
Server `spark` starts span `irc.command.PRIVMSG` as a child of the extracted context, relays the event via SEVENT to `thor`, re-injecting its own span's traceparent. Server `thor` sees trace ID `4bf92f35…4736` and starts another child span — the collector stitches both spans into one trace.
|
|
32
|
+
|
|
33
|
+
**Re-sign per hop on the wire.** Concretely, the SEVENT line `spark` sends to `thor` looks like:
|
|
34
|
+
|
|
35
|
+
@culture.dev/traceparent=00-4bf92f3577b34da6a3ce929d0e0e4736-aaaaaaaaaaaaaaaa-01 :spark SEVENT spark 42 message #general :<base64>
|
|
36
|
+
|
|
37
|
+
The trace-id (`4bf92f35…4736`) is preserved across the hop. The parent-id changes (`00f067aa…02b7` → `aaaaaaaa…aaaa`) because `spark` re-injects its own relay span's id, not the inbound parent-id verbatim. This produces a parent-per-hop span tree that mirrors the federation topology rather than collapsing every hop into one flat span.
|
|
@@ -7,6 +7,8 @@ from culture.telemetry.context import (
|
|
|
7
7
|
TRACEPARENT_TAG,
|
|
8
8
|
TRACESTATE_TAG,
|
|
9
9
|
ExtractResult,
|
|
10
|
+
context_from_traceparent,
|
|
11
|
+
current_traceparent,
|
|
10
12
|
extract_traceparent_from_tags,
|
|
11
13
|
inject_traceparent,
|
|
12
14
|
)
|
|
@@ -16,6 +18,8 @@ __all__ = [
|
|
|
16
18
|
"ExtractResult",
|
|
17
19
|
"TRACEPARENT_TAG",
|
|
18
20
|
"TRACESTATE_TAG",
|
|
21
|
+
"context_from_traceparent",
|
|
22
|
+
"current_traceparent",
|
|
19
23
|
"extract_traceparent_from_tags",
|
|
20
24
|
"init_telemetry",
|
|
21
25
|
"inject_traceparent",
|