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.
Files changed (431) hide show
  1. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/CHANGELOG.md +21 -0
  2. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/PKG-INFO +1 -1
  3. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/client.py +107 -109
  4. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/server_link.py +161 -42
  5. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/tracing.md +6 -0
  6. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/telemetry/__init__.py +4 -0
  7. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/telemetry/context.py +37 -0
  8. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/telemetry.md +24 -4
  9. agentirc_cli-8.3.0/docs/superpowers/plans/2026-04-25-otel-federation.md +229 -0
  10. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/pyproject.toml +1 -1
  11. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_context.py +42 -0
  12. agentirc_cli-8.3.0/tests/telemetry/test_federation_propagation.py +141 -0
  13. agentirc_cli-8.3.0/tests/telemetry/test_s2s_dispatch_span.py +156 -0
  14. agentirc_cli-8.3.0/tests/telemetry/test_s2s_relay_span.py +114 -0
  15. agentirc_cli-8.3.0/tests/telemetry/test_s2s_session_span.py +64 -0
  16. agentirc_cli-8.3.0/tests/telemetry/test_server_link_inject.py +124 -0
  17. agentirc_cli-8.3.0/tests/telemetry/test_session_span.py +105 -0
  18. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_federation.py +47 -0
  19. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/uv.lock +1 -1
  20. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/agents/doc-test-alignment.md +0 -0
  21. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/skills/pr-review/SKILL.md +0 -0
  22. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/skills/run-tests/SKILL.md +0 -0
  23. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  24. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.flake8 +0 -0
  25. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/docs-check.yml +0 -0
  26. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/publish.yml +0 -0
  27. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/security-checks.yml +0 -0
  28. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.github/workflows/tests.yml +0 -0
  29. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.gitignore +0 -0
  30. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.markdownlint-cli2.yaml +0 -0
  31. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.pr_agent.toml +0 -0
  32. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.pre-commit-config.yaml +0 -0
  33. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/.pylintrc +0 -0
  34. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/CLAUDE.md +0 -0
  35. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/Gemfile +0 -0
  36. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/Gemfile.lock +0 -0
  37. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/LICENSE +0 -0
  38. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/README.md +0 -0
  39. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/SECURITY.md +0 -0
  40. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_config.base.yml +0 -0
  41. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_config.culture.yml +0 -0
  42. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_data/sites.yml +0 -0
  43. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_includes/head_custom.html +0 -0
  44. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_sass/color_schemes/anthropic.scss +0 -0
  45. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_sass/color_schemes/dark-terminal.scss +0 -0
  46. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/_sass/custom/custom.scss +0 -0
  47. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/IMG_3183.png +0 -0
  48. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/apple-touch-icon.png +0 -0
  49. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/favicon-16x16.png +0 -0
  50. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/favicon-32x32.png +0 -0
  51. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/favicon.ico +0 -0
  52. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/og-agentirc.png +0 -0
  53. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/assets/images/og-culture.png +0 -0
  54. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/__init__.py +0 -0
  55. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/__main__.py +0 -0
  56. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/CLAUDE.md +0 -0
  57. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/__init__.py +0 -0
  58. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/__main__.py +0 -0
  59. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/channel.py +0 -0
  60. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/config.py +0 -0
  61. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc-architecture.md +0 -0
  62. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc-features.md +0 -0
  63. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc-skill.md +0 -0
  64. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/docs/agentirc.md +0 -0
  65. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/events.py +0 -0
  66. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/history_store.py +0 -0
  67. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/ircd.py +0 -0
  68. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/remote_client.py +0 -0
  69. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/room_store.py +0 -0
  70. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/rooms_util.py +0 -0
  71. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skill.py +0 -0
  72. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/__init__.py +0 -0
  73. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/history.py +0 -0
  74. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/icon.py +0 -0
  75. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/rooms.py +0 -0
  76. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/skills/threads.py +0 -0
  77. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/agentirc/thread_store.py +0 -0
  78. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/aio.py +0 -0
  79. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/__init__.py +0 -0
  80. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/bot.py +0 -0
  81. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/bot_manager.py +0 -0
  82. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/config.py +0 -0
  83. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/filter_dsl.py +0 -0
  84. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/http_listener.py +0 -0
  85. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/__init__.py +0 -0
  86. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/welcome/__init__.py +0 -0
  87. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/welcome/bot.yaml +0 -0
  88. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/system/welcome/handler.py +0 -0
  89. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/template_engine.py +0 -0
  90. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/bots/virtual_client.py +0 -0
  91. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/__init__.py +0 -0
  92. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/_passthrough.py +0 -0
  93. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/afi.py +0 -0
  94. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/agent.py +0 -0
  95. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/bot.py +0 -0
  96. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/channel.py +0 -0
  97. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/devex.py +0 -0
  98. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/introspect.py +0 -0
  99. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/mesh.py +0 -0
  100. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/server.py +0 -0
  101. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/__init__.py +0 -0
  102. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/constants.py +0 -0
  103. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/display.py +0 -0
  104. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/formatting.py +0 -0
  105. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/ipc.py +0 -0
  106. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/mesh.py +0 -0
  107. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/shared/process.py +0 -0
  108. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/cli/skills.py +0 -0
  109. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/__init__.py +0 -0
  110. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/__init__.py +0 -0
  111. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/agent_runner.py +0 -0
  112. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/config.py +0 -0
  113. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/culture.yaml +0 -0
  114. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/daemon.py +0 -0
  115. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/ipc.py +0 -0
  116. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/irc_transport.py +0 -0
  117. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/message_buffer.py +0 -0
  118. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/skill/SKILL.md +0 -0
  119. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/skill/__init__.py +0 -0
  120. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/skill/irc_client.py +0 -0
  121. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/socket_server.py +0 -0
  122. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/supervisor.py +0 -0
  123. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/acp/webhook.py +0 -0
  124. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/__init__.py +0 -0
  125. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/__main__.py +0 -0
  126. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/agent_runner.py +0 -0
  127. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/config.py +0 -0
  128. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/culture.yaml +0 -0
  129. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/daemon.py +0 -0
  130. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/ipc.py +0 -0
  131. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/irc_transport.py +0 -0
  132. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/message_buffer.py +0 -0
  133. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/skill/SKILL.md +0 -0
  134. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/skill/__init__.py +0 -0
  135. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/skill/irc_client.py +0 -0
  136. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/socket_server.py +0 -0
  137. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/supervisor.py +0 -0
  138. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/claude/webhook.py +0 -0
  139. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/__init__.py +0 -0
  140. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/agent_runner.py +0 -0
  141. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/config.py +0 -0
  142. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/culture.yaml +0 -0
  143. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/daemon.py +0 -0
  144. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/ipc.py +0 -0
  145. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/irc_transport.py +0 -0
  146. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/message_buffer.py +0 -0
  147. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/skill/SKILL.md +0 -0
  148. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/skill/__init__.py +0 -0
  149. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/skill/irc_client.py +0 -0
  150. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/socket_server.py +0 -0
  151. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/supervisor.py +0 -0
  152. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/codex/webhook.py +0 -0
  153. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/__init__.py +0 -0
  154. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/agent_runner.py +0 -0
  155. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/config.py +0 -0
  156. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/culture.yaml +0 -0
  157. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/daemon.py +0 -0
  158. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/ipc.py +0 -0
  159. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/irc_transport.py +0 -0
  160. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/message_buffer.py +0 -0
  161. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/skill/SKILL.md +0 -0
  162. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/skill/__init__.py +0 -0
  163. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/skill/irc_client.py +0 -0
  164. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/socket_server.py +0 -0
  165. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/supervisor.py +0 -0
  166. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/clients/copilot/webhook.py +0 -0
  167. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/config.py +0 -0
  168. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/__init__.py +0 -0
  169. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/app.py +0 -0
  170. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/client.py +0 -0
  171. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/commands.py +0 -0
  172. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/status.py +0 -0
  173. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/__init__.py +0 -0
  174. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/chat.py +0 -0
  175. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/info_panel.py +0 -0
  176. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/console/widgets/sidebar.py +0 -0
  177. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/constants.py +0 -0
  178. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/credentials.py +0 -0
  179. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/formatting.py +0 -0
  180. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/learn_prompt.py +0 -0
  181. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/mesh_config.py +0 -0
  182. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/observer.py +0 -0
  183. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/__init__.py +0 -0
  184. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/collector.py +0 -0
  185. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/model.py +0 -0
  186. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/renderer_text.py +0 -0
  187. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/renderer_web.py +0 -0
  188. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/overview/web/style.css +0 -0
  189. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/persistence.py +0 -0
  190. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/pidfile.py +0 -0
  191. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/__init__.py +0 -0
  192. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/commands.py +0 -0
  193. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/events.md +0 -0
  194. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/federation.md +0 -0
  195. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/history.md +0 -0
  196. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/icons.md +0 -0
  197. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/rooms.md +0 -0
  198. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/tags.md +0 -0
  199. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/extensions/threads.md +0 -0
  200. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/message.py +0 -0
  201. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/protocol-index.md +0 -0
  202. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/protocol/replies.py +0 -0
  203. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/skills/culture/SKILL.md +0 -0
  204. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/culture/telemetry/tracing.py +0 -0
  205. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/README.md +0 -0
  206. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/architecture-overview.md +0 -0
  207. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/bots.md +0 -0
  208. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/events.md +0 -0
  209. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/index.md +0 -0
  210. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/otelcol-template.yaml +0 -0
  211. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/agentirc/why-agentirc.md +0 -0
  212. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/agent-lifecycle.md +0 -0
  213. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/choose-a-harness.md +0 -0
  214. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/features.md +0 -0
  215. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/index.md +0 -0
  216. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/mental-model.md +0 -0
  217. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/operate.md +0 -0
  218. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/patterns.md +0 -0
  219. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/quickstart.md +0 -0
  220. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/reflective-development.md +0 -0
  221. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/vision-patterns-index.md +0 -0
  222. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/vision.md +0 -0
  223. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/culture/what-is-culture.md +0 -0
  224. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/agent-harness-spec.md +0 -0
  225. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/index.md +0 -0
  226. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/layers.md +0 -0
  227. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/subsites.md +0 -0
  228. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/architecture/threads.md +0 -0
  229. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/afi.md +0 -0
  230. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/commands.md +0 -0
  231. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/devex.md +0 -0
  232. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/cli/index.md +0 -0
  233. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/console.md +0 -0
  234. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/acp.md +0 -0
  235. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/claude.md +0 -0
  236. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/codex.md +0 -0
  237. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/copilot.md +0 -0
  238. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/harnesses/index.md +0 -0
  239. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/index.md +0 -0
  240. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/architecture.md +0 -0
  241. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/config.md +0 -0
  242. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/deployment.md +0 -0
  243. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/index.md +0 -0
  244. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/reference/server/security.md +0 -0
  245. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  246. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/resources/positioning.md +0 -0
  247. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/federation.md +0 -0
  248. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/harnesses.md +0 -0
  249. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/humans-and-agents.md +0 -0
  250. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/index.md +0 -0
  251. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/persistence.md +0 -0
  252. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/concepts/rooms.md +0 -0
  253. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/demos/magic-demo.md +0 -0
  254. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/first-session.md +0 -0
  255. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/index.md +0 -0
  256. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/join-as-human.md +0 -0
  257. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/local-setup.md +0 -0
  258. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/guides/multi-machine.md +0 -0
  259. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/01-pair-programming.md +0 -0
  260. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/02-code-review-ensemble.md +0 -0
  261. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/03-cross-server-delegation.md +0 -0
  262. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/04-knowledge-propagation.md +0 -0
  263. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/05-the-observer.md +0 -0
  264. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/06-cross-server-ops.md +0 -0
  265. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/07-supervisor-intervention.md +0 -0
  266. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/08-apps-as-agents.md +0 -0
  267. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/09-research-swarm.md +0 -0
  268. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases/10-agent-lifecycle.md +0 -0
  269. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/shared/use-cases-index.md +0 -0
  270. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  271. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  272. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
  273. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
  274. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
  275. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
  276. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
  277. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
  278. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
  279. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
  280. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-12-console-enhancements.md +0 -0
  281. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-15-mesh-events.md +0 -0
  282. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-18-culture-dev-positioning.md +0 -0
  283. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-22-agex-integration.md +0 -0
  284. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/plans/2026-04-24-otel-foundation.md +0 -0
  285. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  286. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  287. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
  288. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
  289. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
  290. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
  291. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
  292. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
  293. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
  294. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
  295. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
  296. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
  297. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
  298. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
  299. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
  300. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
  301. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-12-console-enhancements-design.md +0 -0
  302. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-15-mesh-events-design.md +0 -0
  303. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-17-sites-repositioning-design.md +0 -0
  304. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-18-culture-dev-positioning-design.md +0 -0
  305. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-22-agex-integration-design.md +0 -0
  306. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/docs/superpowers/specs/2026-04-24-otel-observability-design.md +0 -0
  307. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/favicon.ico +0 -0
  308. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/README.md +0 -0
  309. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/config.py +0 -0
  310. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/culture.yaml +0 -0
  311. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/daemon.py +0 -0
  312. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/ipc.py +0 -0
  313. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/irc_transport.py +0 -0
  314. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/message_buffer.py +0 -0
  315. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/skill/SKILL.md +0 -0
  316. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/skill/irc_client.py +0 -0
  317. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/socket_server.py +0 -0
  318. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/packages/agent-harness/webhook.py +0 -0
  319. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  320. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
  321. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  322. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
  323. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/robots.txt +0 -0
  324. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sitemap-agentirc.html +0 -0
  325. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sitemap-main.html +0 -0
  326. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sitemap.html +0 -0
  327. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/sonar-project.properties +0 -0
  328. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/__init__.py +0 -0
  329. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/conftest.py +0 -0
  330. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/__init__.py +0 -0
  331. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/_fakes.py +0 -0
  332. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_config.py +0 -0
  333. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_config_load.py +0 -0
  334. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_dispatch_span.py +0 -0
  335. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_emit_event_span.py +0 -0
  336. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_outbound_inject.py +0 -0
  337. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_parse_error.py +0 -0
  338. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_privmsg_span.py +0 -0
  339. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_server_init.py +0 -0
  340. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/telemetry/test_tracing.py +0 -0
  341. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_acp_daemon.py +0 -0
  342. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_agent_runner.py +0 -0
  343. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_archive.py +0 -0
  344. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot.py +0 -0
  345. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot_config.py +0 -0
  346. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot_config_fires_event_toplevel.py +0 -0
  347. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bot_manager.py +0 -0
  348. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_bots_integration.py +0 -0
  349. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_channel.py +0 -0
  350. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_channel_cli.py +0 -0
  351. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_afi.py +0 -0
  352. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_devex.py +0 -0
  353. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_introspect.py +0 -0
  354. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_cli_passthrough.py +0 -0
  355. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_codex_daemon.py +0 -0
  356. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_connection.py +0 -0
  357. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_chat_markdown.py +0 -0
  358. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_client.py +0 -0
  359. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_commands.py +0 -0
  360. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_connection.py +0 -0
  361. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_fixes_224_227.py +0 -0
  362. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_icons.py +0 -0
  363. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_integration.py +0 -0
  364. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_console_status.py +0 -0
  365. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_copilot_daemon.py +0 -0
  366. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_credentials.py +0 -0
  367. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_culture_config.py +0 -0
  368. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_daemon.py +0 -0
  369. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_daemon_config.py +0 -0
  370. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_daemon_ipc.py +0 -0
  371. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_discovery.py +0 -0
  372. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_display.py +0 -0
  373. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_basic.py +0 -0
  374. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_bot_chain.py +0 -0
  375. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_bot_trigger.py +0 -0
  376. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_cap_fallback.py +0 -0
  377. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_catalog.py +0 -0
  378. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_federation.py +0 -0
  379. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_history.py +0 -0
  380. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_lifecycle.py +0 -0
  381. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_events_reserved_nick.py +0 -0
  382. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_filter_dsl.py +0 -0
  383. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_history.py +0 -0
  384. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_http_listener.py +0 -0
  385. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_integration_layer5.py +0 -0
  386. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_ipc.py +0 -0
  387. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_irc_transport.py +0 -0
  388. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_irc_transport_tags.py +0 -0
  389. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_learn_prompt.py +0 -0
  390. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_link_reconnect.py +0 -0
  391. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_manifest_config.py +0 -0
  392. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mention_alias.py +0 -0
  393. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mention_target_cleanup.py +0 -0
  394. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mention_warning.py +0 -0
  395. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mentions.py +0 -0
  396. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mesh_config.py +0 -0
  397. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_mesh_readiness.py +0 -0
  398. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_message.py +0 -0
  399. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_message_buffer.py +0 -0
  400. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_message_tags.py +0 -0
  401. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_messaging.py +0 -0
  402. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_migrate_cli.py +0 -0
  403. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_modes.py +0 -0
  404. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_cli.py +0 -0
  405. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_collector.py +0 -0
  406. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_model.py +0 -0
  407. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_renderer.py +0 -0
  408. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_overview_web.py +0 -0
  409. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_persistence.py +0 -0
  410. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_persistence_timeout.py +0 -0
  411. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_pidfile.py +0 -0
  412. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_poll_loop.py +0 -0
  413. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_register_cli.py +0 -0
  414. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_room_persistence.py +0 -0
  415. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_rooms.py +0 -0
  416. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_rooms_federation.py +0 -0
  417. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_rooms_integration.py +0 -0
  418. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_server_icon_skill.py +0 -0
  419. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_setup_update_cli.py +0 -0
  420. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_skill_client.py +0 -0
  421. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_skill_docs.py +0 -0
  422. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_skills.py +0 -0
  423. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_socket_server.py +0 -0
  424. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_supervisor.py +0 -0
  425. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_template_engine.py +0 -0
  426. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_thread_buffer.py +0 -0
  427. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_threads.py +0 -0
  428. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_virtual_client.py +0 -0
  429. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_wait_for_port.py +0 -0
  430. {agentirc_cli-8.2.0 → agentirc_cli-8.3.0}/tests/test_webhook.py +0 -0
  431. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 8.2.0
3
+ Version: 8.3.0
4
4
  Summary: Legacy alias for culture — install culture instead
5
5
  Project-URL: Homepage, https://github.com/OriNachum/culture
6
6
  Author: Ori Nachum
@@ -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.trace import NonRecordingSpan, SpanContext, TraceFlags
11
- from opentelemetry.trace.propagation import set_span_in_context
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 for PRIVMSG body capture, defined once so a future
29
- # rename / sanitization layer has one edit point.
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 = _current_traceparent()
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 = _current_traceparent()
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
- buffer = ""
175
- if initial_msg:
176
- buffer = initial_msg.replace("\r\n", "\n").replace("\r", "\n")
177
- buffer = await self._process_buffer(buffer)
178
- while True:
179
- data = await self.reader.read(4096)
180
- if not data:
181
- break
182
- buffer += data.decode("utf-8", errors="replace")
183
- # Cap buffer to prevent unbounded memory growth (512 bytes per RFC 2812)
184
- if len(buffer) > 8192:
185
- buffer = buffer[-4096:]
186
- # Normalize all line endings to \n for simpler parsing
187
- buffer = buffer.replace("\r\n", "\n").replace("\r", "\n")
188
- buffer = await self._process_buffer(buffer)
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 = _context_from_traceparent(extract.traceparent)
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": msg.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.{msg.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
- if not channel_name.startswith("#"):
359
- return
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
- # Block joins to archived rooms
362
- existing = self.server.channels.get(channel_name)
363
- if existing and existing.archived:
364
- await self.send(
365
- Message(
366
- prefix=self.server.config.name,
367
- command="NOTICE",
368
- params=[self.nick, f"{channel_name} is archived and cannot be joined"],
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
- channel = self.server.get_or_create_channel(channel_name)
374
- if self in channel.members:
375
- return
367
+ channel = self.server.get_or_create_channel(channel_name)
368
+ if self in channel.members:
369
+ return
376
370
 
377
- channel.add(self)
378
- self.channels.add(channel)
371
+ channel.add(self)
372
+ self.channels.add(channel)
379
373
 
380
- # Notify all channel members (including self)
381
- join_msg = Message(prefix=self.prefix, command="JOIN", params=[channel_name])
382
- for member in list(channel.members):
383
- await member.send(join_msg)
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
- # Send topic if set
386
- if channel.topic:
387
- await self.send_numeric(replies.RPL_TOPIC, channel_name, channel.topic)
379
+ # Send topic if set
380
+ if channel.topic:
381
+ await self.send_numeric(replies.RPL_TOPIC, channel_name, channel.topic)
388
382
 
389
- # Send names list
390
- await self._send_names(channel)
383
+ # Send names list
384
+ await self._send_names(channel)
391
385
 
392
- # Emit event AFTER delivering all join-related numerics (topic, NAMES)
393
- # so that the event PRIVMSG doesn't interleave with 353/366 in client buffers.
394
- await self.server.emit_event(
395
- Event(type=EventType.JOIN, channel=channel_name, nick=self.nick)
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
- reason = msg.params[1] if len(msg.params) > 1 else ""
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
- channel = self.server.channels.get(channel_name)
407
- if not channel or self not in channel.members:
408
- await self.send_numeric(
409
- replies.ERR_NOTONCHANNEL,
410
- channel_name,
411
- replies.MSG_NOTONCHANNEL,
412
- )
413
- return
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
- part_params = [channel_name, reason] if reason else [channel_name]
416
- part_msg = Message(prefix=self.prefix, command="PART", params=part_params)
417
- for member in list(channel.members):
418
- await member.send(part_msg)
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
- await self.server.emit_event(
421
- Event(
422
- type=EventType.PART,
423
- channel=channel_name,
424
- nick=self.nick,
425
- data={"reason": reason},
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
- channel.remove(self)
430
- self.channels.discard(channel)
427
+ channel.remove(self)
428
+ self.channels.discard(channel)
431
429
 
432
- if not channel.members and not channel.persistent:
433
- del self.server.channels[channel_name]
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
- try:
91
- if self.initiator:
92
- await self._send_handshake()
93
-
94
- buffer = ""
95
- if initial_msg:
96
- buffer = initial_msg + "\n"
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
- await self.writer.wait_closed()
113
- except ConnectionError:
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
- if handler:
123
- await maybe_await(handler(msg))
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
- if event.type == EventType.MESSAGE:
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
- origin = self.server.config.name
837
- handler = self._RELAY_DISPATCH.get(event.type)
838
- if handler:
839
- await maybe_await(handler(self, event, origin))
840
- return
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
- # If no typed relay exists, fall back to generic SEVENT.
843
- # v1 assumes all peers support SEVENT; cap negotiation is deferred — see plan task 12.
844
- type_str = event.type.value if hasattr(event.type, "value") else str(event.type)
845
- payload = self.server._build_event_payload(event)
846
- encoded = self.server._encode_event_data(payload, type_str)
847
- target = event.channel or "*"
848
- # Egress trust check: channel-scoped events respect should_relay; global events always relay
849
- if event.channel is not None and not self.should_relay(event.channel):
850
- return
851
- seq = self.server._seq # current local seq; peer stores but doesn't re-sequence
852
- await self.send_raw(f":{origin} SEVENT {origin} {seq} {type_str} {target} :{encoded}")
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",