agentirc-cli 8.3.0__tar.gz → 8.4.0__tar.gz

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