bazaar-compute-node 0.1.27__tar.gz → 0.1.29__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 (144) hide show
  1. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/PKG-INFO +2 -1
  2. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/pyproject.toml +2 -1
  3. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/pyproject.toml.orig +2 -1
  4. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/agent.py +1 -0
  5. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/application.py +56 -2
  6. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/command.py +50 -20
  7. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/system_service.py +57 -161
  8. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/transport.py +7 -0
  9. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/windows_pipe.py +14 -2
  10. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/bcc.py +9 -0
  11. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/api.py +12 -0
  12. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/approval.py +7 -3
  13. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/channel.py +146 -2
  14. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/codec.py +8 -0
  15. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/database.py +4 -0
  16. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/executor.py +41 -5
  17. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/registry.py +2 -0
  18. bazaar_compute_node-0.1.29/src/bazaar_compute_node/contrib/sqlite/migrations/v22_readable_targets.py +20 -0
  19. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/repository/facade.py +19 -17
  20. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/repository/messages.py +56 -14
  21. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/repository/sessions.py +13 -3
  22. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/storage.py +1 -1
  23. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/approval.py +12 -9
  24. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/channel.py +108 -0
  25. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/command.py +27 -4
  26. bazaar_compute_node-0.1.29/src/bazaar_compute_node/core/instruction.py +52 -0
  27. bazaar_compute_node-0.1.29/src/bazaar_compute_node/core/lifecycle.py +100 -0
  28. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/models/__init__.py +2 -0
  29. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/models/entities.py +51 -0
  30. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/command.py +33 -30
  31. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/reminder.py +29 -2
  32. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/services.py +24 -12
  33. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/session.py +88 -3
  34. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/storage.py +94 -19
  35. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/timerwheel.py +9 -0
  36. bazaar_compute_node-0.1.29/src/bazaar_compute_node/i18n/catalog.py +79 -0
  37. bazaar_compute_node-0.1.29/src/bazaar_compute_node/rendering.py +86 -0
  38. bazaar_compute_node-0.1.27/src/bazaar_compute_node/core/instruction.py → bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/developer_instructions.md +11 -68
  39. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/locales/en.toml +49 -0
  40. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/locales/zh-CN.toml +49 -0
  41. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/system_service/launchd.plist +37 -0
  42. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/system_service/launchd.sh +11 -0
  43. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/system_service/systemd.service +19 -0
  44. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/system_service/windows.ps1 +98 -0
  45. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/system_service/windows.vbs +11 -0
  46. bazaar_compute_node-0.1.29/src/bazaar_compute_node/resources/system_service/windows.xml +39 -0
  47. bazaar_compute_node-0.1.27/src/bazaar_compute_node/core/lifecycle.py +0 -48
  48. bazaar_compute_node-0.1.27/src/bazaar_compute_node/i18n/catalog.py +0 -46
  49. bazaar_compute_node-0.1.27/src/bazaar_compute_node/i18n/english.py +0 -87
  50. bazaar_compute_node-0.1.27/src/bazaar_compute_node/i18n/schinese.py +0 -73
  51. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/LICENSE +0 -0
  52. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/README.md +0 -0
  53. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/__init__.py +0 -0
  54. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/__init__.py +0 -0
  55. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/agent_management.py +0 -0
  56. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/attachments.py +0 -0
  57. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/config.py +0 -0
  58. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/registry.py +0 -0
  59. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/resource_dispatch.py +0 -0
  60. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/app/wrapper.py +0 -0
  61. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/cli.py +0 -0
  62. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  63. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  64. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  65. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  66. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  67. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  68. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  69. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  70. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  71. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/__init__.py +0 -0
  72. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/attachments.py +0 -0
  73. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/frame.py +0 -0
  74. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/identity.py +0 -0
  75. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/outbound.py +0 -0
  76. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/plugin.py +0 -0
  77. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/lark/transport.py +0 -0
  78. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  79. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  80. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  81. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  82. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/__init__.py +0 -0
  83. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/model.py +0 -0
  84. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v01_initial_node_schema.py +0 -0
  85. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v02_session_mapping_indexes.py +0 -0
  86. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v03_message_log_indexes.py +0 -0
  87. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v04_runtime_attempt_facts.py +0 -0
  88. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v05_inbound_message_references.py +0 -0
  89. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v06_inbound_message_reference_integrity.py +0 -0
  90. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v07_inbound_provider_identity.py +0 -0
  91. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v08_transient_stream_events.py +0 -0
  92. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v09_remove_runtime_events.py +0 -0
  93. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v10_add_outbound_attachments.py +0 -0
  94. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v11_remove_runtime_session_mapping.py +0 -0
  95. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v12_add_reminders.py +0 -0
  96. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v13_add_agent_ownership.py +0 -0
  97. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v14_add_inbox_discovery_indexes.py +0 -0
  98. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v15_add_handoffs.py +0 -0
  99. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v16_remove_outbound_drafts.py +0 -0
  100. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v17_remove_agent_identity_triggers.py +0 -0
  101. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v18_unify_messages.py +0 -0
  102. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v19_reminder_system_messages.py +0 -0
  103. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v20_remove_freshness_persistence.py +0 -0
  104. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/migrations/v21_remove_handoffs.py +0 -0
  105. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  106. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  107. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/repository/__init__.py +0 -0
  108. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/repository/base.py +0 -0
  109. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/sqlite/repository/reminders.py +0 -0
  110. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  111. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  112. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  113. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  114. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  115. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  116. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  117. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  118. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  119. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  120. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  121. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  122. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/__init__.py +0 -0
  123. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/approval.py +0 -0
  124. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/audit.py +0 -0
  125. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/channel.py +0 -0
  126. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/client.py +0 -0
  127. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/concurrency.py +0 -0
  128. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/correlation.py +0 -0
  129. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/inbox.py +0 -0
  130. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  131. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  132. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/models/states.py +0 -0
  133. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/observability.py +0 -0
  134. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  135. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  136. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  137. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  138. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  139. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/outcomes.py +0 -0
  140. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/paths.py +0 -0
  141. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/reminder.py +0 -0
  142. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  143. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/core/runtime.py +0 -0
  144. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.29}/src/bazaar_compute_node/i18n/__init__.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.27
3
+ Version: 0.1.29
4
4
  Summary: Runtime-agnostic computer node daemon for agents and channels.
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
7
7
  Requires-Dist: aiohttp>=3.12.0
8
8
  Requires-Dist: aiosqlite>=0.20.0
9
9
  Requires-Dist: cryptography>=45.0.0
10
+ Requires-Dist: jinja2>=3.1.6
10
11
  Requires-Dist: pydantic>=2.0.0
11
12
  Requires-Dist: protobug==1.0.0
12
13
  Requires-Dist: tzdata>=2024.1 ; sys_platform == 'win32'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.27"
3
+ version = "0.1.29"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "aiohttp>=3.12.0",
11
11
  "aiosqlite>=0.20.0",
12
12
  "cryptography>=45.0.0",
13
+ "jinja2>=3.1.6",
13
14
  "pydantic>=2.0.0",
14
15
  "protobug==1.0.0",
15
16
  "tzdata>=2024.1; sys_platform == 'win32'",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.27"
3
+ version = "0.1.29"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "aiohttp>=3.12.0",
11
11
  "aiosqlite>=0.20.0",
12
12
  "cryptography>=45.0.0",
13
+ "jinja2>=3.1.6",
13
14
  "pydantic>=2.0.0",
14
15
  "protobug==1.0.0",
15
16
  "tzdata>=2024.1; sys_platform == 'win32'",
@@ -277,6 +277,7 @@ class AgentApplication:
277
277
  "channel": self.channel.name,
278
278
  "runtime": self.runtime.name,
279
279
  "channel_health": dict(self.channel.health),
280
+ "orchestrator_health": self.orchestrator.health,
280
281
  }
281
282
 
282
283
  async def _cleanup_partial_start(self) -> None:
@@ -10,7 +10,7 @@ from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
 
12
12
  from ..core.concurrency import SessionLockRegistry
13
- from ..core.lifecycle import TimeoutBudget
13
+ from ..core.lifecycle import ITaskFailureSource, TimeoutBudget
14
14
  from ..core.models import InboundAttachment, Message
15
15
  from ..core.observability import IAudit
16
16
  from ..core.orchestration import ReminderScheduler
@@ -245,7 +245,61 @@ class NodeApplication:
245
245
  loop.add_signal_handler(signum, self._stopped.set)
246
246
  except NotImplementedError, RuntimeError:
247
247
  pass
248
- await self._stopped.wait()
248
+ stop_task = asyncio.create_task(self._stopped.wait(), name="bcn-stop-signal")
249
+ sources: list[tuple[str, ITaskFailureSource]] = [
250
+ ("timer_wheel", self.timer_wheel),
251
+ ("reminder_scheduler", self.reminder_scheduler),
252
+ ("command_server", self.command_server),
253
+ ]
254
+ if isinstance(self.storage, ITaskFailureSource):
255
+ sources.append(("storage", self.storage))
256
+ failure_tasks = {
257
+ asyncio.create_task(
258
+ source.wait_failure(),
259
+ name=f"bcn-critical-{name}",
260
+ ): name
261
+ for name, source in sources
262
+ }
263
+ tasks = {stop_task, *failure_tasks}
264
+ try:
265
+ done, _ = await asyncio.wait(
266
+ tasks,
267
+ return_when=asyncio.FIRST_COMPLETED,
268
+ )
269
+ failure: tuple[str, BaseException] | None = None
270
+ for task in done:
271
+ if task is stop_task:
272
+ continue
273
+ try:
274
+ task.result()
275
+ except asyncio.CancelledError:
276
+ caught: BaseException = RuntimeError(
277
+ f"{failure_tasks[task]} failure monitor stopped unexpectedly"
278
+ )
279
+ except BaseException as error: # noqa: BLE001
280
+ caught = error
281
+ else:
282
+ caught = RuntimeError(
283
+ f"{failure_tasks[task]} failure monitor returned unexpectedly"
284
+ )
285
+ if failure is None:
286
+ failure = failure_tasks[task], caught
287
+ if failure is not None:
288
+ component, error = failure
289
+ self._ready = False
290
+ self._accepting = False
291
+ self._log(
292
+ "bcn.critical.failed",
293
+ component=component,
294
+ error_type=type(error).__name__,
295
+ error=_safe_error(error),
296
+ )
297
+ raise error
298
+ finally:
299
+ for task in tasks:
300
+ if not task.done():
301
+ task.cancel()
302
+ await asyncio.gather(*tasks, return_exceptions=True)
249
303
 
250
304
  async def _publish_inbox_wake(
251
305
  self,
@@ -21,7 +21,9 @@ from ..core.command import (
21
21
  ICommandService,
22
22
  InboxListResult,
23
23
  MessageSendFreshnessHold,
24
+ MessageSendSuccess,
24
25
  SessionNotFoundError,
26
+ TargetProjection,
25
27
  )
26
28
  from ..core.lifecycle import TimeoutBudget
27
29
  from ..core.models import (
@@ -62,7 +64,14 @@ def _serialize_attachment(
62
64
  }
63
65
 
64
66
 
65
- def serialize_message(message: Message) -> dict[str, object]:
67
+ def serialize_message(
68
+ message: Message,
69
+ target_projections: tuple[TargetProjection, ...] = (),
70
+ ) -> dict[str, object]:
71
+ targets = {
72
+ projection.canonical_target: projection.display_target
73
+ for projection in target_projections
74
+ }
66
75
  return {
67
76
  "seq": message.seq,
68
77
  "message_id": message.message_id,
@@ -84,13 +93,19 @@ def serialize_message(message: Message) -> dict[str, object]:
84
93
  if message.system_message_kind is None
85
94
  else message.system_message_kind.value
86
95
  ),
87
- "system_message_source_target": message.metadata.get(
88
- "system_message_source_target"
96
+ "system_message_source_target": (
97
+ targets.get(source_target, source_target)
98
+ if isinstance(
99
+ source_target := message.metadata.get("system_message_source_target"),
100
+ str,
101
+ )
102
+ else None
89
103
  ),
90
104
  "system_message_source_message_id": message.metadata.get(
91
105
  "system_message_source_message_id"
92
106
  ),
93
107
  "message_type": message.message_type,
108
+ "target": targets.get(message.target, message.target),
94
109
  "canonical_target": message.target,
95
110
  "target_kind": message.target_kind.value,
96
111
  "mentions_agent": message.mentions_agent,
@@ -447,10 +462,11 @@ class CommandDispatcher:
447
462
  "ok": True,
448
463
  "result": {
449
464
  "messages": [
450
- serialize_message(message) for message in result.messages
465
+ serialize_message(message, result.target_projections)
466
+ for message in result.messages
451
467
  ],
452
468
  "referenced_messages": [
453
- serialize_message(message)
469
+ serialize_message(message, result.target_projections)
454
470
  for message in result.referenced_messages
455
471
  ],
456
472
  "snapshot_seq": result.snapshot_seq,
@@ -471,7 +487,7 @@ class CommandDispatcher:
471
487
  request = cast(_MessageReadRequest, request)
472
488
  result = await self._service.read(
473
489
  session_id,
474
- target=request.target,
490
+ raw_target=request.target,
475
491
  around_message_id=request.around_message_id,
476
492
  limit=request.limit,
477
493
  )
@@ -479,10 +495,11 @@ class CommandDispatcher:
479
495
  "ok": True,
480
496
  "result": {
481
497
  "messages": [
482
- serialize_message(message) for message in result.messages
498
+ serialize_message(message, result.target_projections)
499
+ for message in result.messages
483
500
  ],
484
501
  "referenced_messages": [
485
- serialize_message(message)
502
+ serialize_message(message, result.target_projections)
486
503
  for message in result.referenced_messages
487
504
  ],
488
505
  "snapshot_seq": result.snapshot_seq,
@@ -496,7 +513,7 @@ class CommandDispatcher:
496
513
  result = await self._service.send(
497
514
  session_id=session_id,
498
515
  command_id=request.command_id,
499
- target=request.target,
516
+ raw_target=request.target,
500
517
  body=request.body,
501
518
  created_at_ms=request.created_at_ms,
502
519
  attachment_paths=tuple(request.attachment_paths),
@@ -508,22 +525,25 @@ class CommandDispatcher:
508
525
  "ok": True,
509
526
  "result": {"text": format_freshness_hold(result)},
510
527
  }
511
- delivery_state = result.delivery_state
528
+ if not isinstance(result, MessageSendSuccess):
529
+ raise AssertionError("message send returned an unsupported result")
530
+ message = result.message
531
+ delivery_state = message.delivery_state
512
532
  if delivery_state is None:
513
533
  raise RuntimeError("outbound message has no delivery state")
514
534
  if delivery_state is OutboundDeliveryState.SENT:
515
535
  text = (
516
- f"Message sent to {result.target}. Message ID: {result.message_id}"
536
+ f"Message sent to {result.target}. Message ID: {message.message_id}"
517
537
  )
518
538
  elif delivery_state is OutboundDeliveryState.QUEUED:
519
539
  text = (
520
540
  f"Message queued to {result.target}. "
521
- f"Message ID: {result.message_id}"
541
+ f"Message ID: {message.message_id}"
522
542
  )
523
543
  elif delivery_state is OutboundDeliveryState.PARTIAL:
524
544
  raise CommandDispatchError(
525
545
  "SEND_PARTIAL",
526
- result.error_message
546
+ message.error_message
527
547
  or "Message delivery was only partially confirmed.",
528
548
  next_action=(
529
549
  "Do not retry the complete message automatically; reconcile "
@@ -533,13 +553,13 @@ class CommandDispatcher:
533
553
  elif delivery_state is OutboundDeliveryState.UNKNOWN:
534
554
  raise CommandDispatchError(
535
555
  "SEND_UNKNOWN",
536
- result.error_message or "Message delivery outcome is unknown.",
556
+ message.error_message or "Message delivery outcome is unknown.",
537
557
  next_action="Reconcile channel delivery before retrying.",
538
558
  )
539
559
  elif delivery_state is OutboundDeliveryState.FAILED:
540
560
  raise CommandDispatchError(
541
561
  "SEND_FAILED",
542
- result.error_message or "Message delivery failed.",
562
+ message.error_message or "Message delivery failed.",
543
563
  next_action="Fix the provider error before retrying.",
544
564
  )
545
565
  else:
@@ -553,8 +573,14 @@ class CommandDispatcher:
553
573
 
554
574
  if resource == "thread" and command == "unfollow":
555
575
  request = cast(_ThreadUnfollowRequest, request)
556
- changed = await self._service.unfollow(session_id, target=request.target)
557
- return {"ok": True, "result": {"changed": changed}}
576
+ result = await self._service.unfollow(
577
+ session_id,
578
+ raw_target=request.target,
579
+ )
580
+ return {
581
+ "ok": True,
582
+ "result": {"target": result.target, "changed": result.changed},
583
+ }
558
584
 
559
585
  raise AssertionError("validated command route has no handler")
560
586
 
@@ -578,7 +604,7 @@ def _message_timestamp(message: Mapping[str, object]) -> int:
578
604
  def _message_header_fields(
579
605
  message: Mapping[str, object],
580
606
  ) -> tuple[str, str, str, str, str | None, str]:
581
- target = cast(str, message["canonical_target"])
607
+ target = cast(str, message["target"])
582
608
  message_id = cast(str, message["message_id"])
583
609
  sender_kind = cast(str, message["sender_kind"])
584
610
  sender_value = message["sender"]
@@ -694,9 +720,13 @@ def format_read_message(
694
720
 
695
721
 
696
722
  def format_freshness_hold(result: MessageSendFreshnessHold) -> str:
697
- messages = [serialize_message(message) for message in result.messages]
723
+ messages = [
724
+ serialize_message(message, result.target_projections)
725
+ for message in result.messages
726
+ ]
698
727
  referenced_messages = [
699
- serialize_message(message) for message in result.referenced_messages
728
+ serialize_message(message, result.target_projections)
729
+ for message in result.referenced_messages
700
730
  ]
701
731
  shown = len(messages)
702
732
  total = result.newer_message_total
@@ -6,7 +6,6 @@ import getpass
6
6
  import locale
7
7
  import os
8
8
  import platform
9
- import plistlib
10
9
  import shlex
11
10
  import shutil
12
11
  import subprocess
@@ -15,10 +14,10 @@ import tempfile
15
14
  import time
16
15
  from dataclasses import dataclass
17
16
  from pathlib import Path
18
- from xml.etree import ElementTree
19
17
 
20
18
  from ..core.paths import resolve_data_dir
21
19
  from ..i18n import Translator, create_translator
20
+ from ..rendering import TextTemplate
22
21
  from .config import ConfigurationError, load_control_configuration, resolve_config_path
23
22
  from .transport import LocalCommandClient, local_endpoint_for_path
24
23
 
@@ -26,10 +25,16 @@ SYSTEMD_UNIT_NAME = "bcn.service"
26
25
  LAUNCHD_LABEL = "io.github.yuchanns.bazaar-compute-node"
27
26
  WINDOWS_TASK_NAME = r"\BazaarComputeNode"
28
27
  MANAGED_MARKER = "Managed by bazaar-compute-node."
29
- TASK_NAMESPACE = "http://schemas.microsoft.com/windows/2004/02/mit/task"
30
28
  WINDOWS_STOP_ATTEMPTS = 100
31
29
  WINDOWS_STOP_INTERVAL = 0.05
32
30
 
31
+ _SYSTEMD_UNIT_TEMPLATE = TextTemplate.from_resource("system_service/systemd.service")
32
+ _LAUNCHD_PLIST_TEMPLATE = TextTemplate.from_resource("system_service/launchd.plist")
33
+ _LAUNCHD_WRAPPER_TEMPLATE = TextTemplate.from_resource("system_service/launchd.sh")
34
+ _WINDOWS_TASK_TEMPLATE = TextTemplate.from_resource("system_service/windows.xml")
35
+ _WINDOWS_LAUNCHER_TEMPLATE = TextTemplate.from_resource("system_service/windows.vbs")
36
+ _WINDOWS_WRAPPER_TEMPLATE = TextTemplate.from_resource("system_service/windows.ps1")
37
+
33
38
 
34
39
  @dataclass(frozen=True, slots=True)
35
40
  class SystemServiceContext:
@@ -166,109 +171,53 @@ def _powershell_literal(path: Path | str | None) -> str:
166
171
 
167
172
 
168
173
  def _render_systemd_unit(context: SystemServiceContext) -> str:
169
- executable = _require_executable(context)
170
- lines = [
171
- f"# {MANAGED_MARKER}",
172
- "[Unit]",
173
- "Description=Bazaar Compute Node",
174
- "",
175
- "[Service]",
176
- "Type=simple",
177
- f"ExecStart={_systemd_quote(executable)} run --config {_systemd_quote(context.config_path)}",
178
- f"WorkingDirectory={_systemd_quote(context.data_dir)}",
179
- ]
180
- if context.env_file is not None:
181
- lines.append(f"EnvironmentFile=-{_systemd_quote(context.env_file)}")
182
- lines.extend(
183
- (
184
- "Restart=on-failure",
185
- "RestartSec=5",
186
- "KillSignal=SIGTERM",
187
- "TimeoutStopSec=15",
188
- "UMask=0077",
189
- "StandardOutput=journal",
190
- "StandardError=journal",
191
- "",
192
- "[Install]",
193
- "WantedBy=default.target",
194
- "",
195
- )
174
+ return _SYSTEMD_UNIT_TEMPLATE.render(
175
+ {
176
+ "managed_marker": MANAGED_MARKER,
177
+ "executable": _systemd_quote(_require_executable(context)),
178
+ "config_path": _systemd_quote(context.config_path),
179
+ "data_dir": _systemd_quote(context.data_dir),
180
+ "environment_file": (
181
+ "" if context.env_file is None else _systemd_quote(context.env_file)
182
+ ),
183
+ }
196
184
  )
197
- return "\n".join(lines)
198
185
 
199
186
 
200
187
  def _render_launchd_wrapper() -> str:
201
- return f"""#!/bin/sh
202
- # {MANAGED_MARKER}
203
- set -eu
204
-
205
- if [ -n "${{BCN_ENV_FILE:-}}" ] && [ -f "$BCN_ENV_FILE" ]; then
206
- set -a
207
- . "$BCN_ENV_FILE"
208
- set +a
209
- fi
210
-
211
- exec "$BCN_EXECUTABLE" run --config "$BCN_CONFIG"
212
- """
188
+ return _LAUNCHD_WRAPPER_TEMPLATE.render({"managed_marker": MANAGED_MARKER})
213
189
 
214
190
 
215
191
  def _render_launchd_plist(
216
192
  context: SystemServiceContext,
217
193
  wrapper_path: Path,
218
194
  ) -> bytes:
219
- payload = {
220
- "Label": LAUNCHD_LABEL,
221
- "Comment": MANAGED_MARKER,
222
- "ProgramArguments": [str(wrapper_path)],
223
- "WorkingDirectory": str(context.data_dir),
224
- "EnvironmentVariables": {
225
- "BCN_CONFIG": str(context.config_path),
226
- "BCN_ENV_FILE": "" if context.env_file is None else str(context.env_file),
227
- "BCN_EXECUTABLE": str(_require_executable(context)),
228
- },
229
- "RunAtLoad": True,
230
- "KeepAlive": True,
231
- "ProcessType": "Background",
232
- "ThrottleInterval": 5,
233
- "StandardOutPath": str(context.log_path),
234
- "StandardErrorPath": str(context.log_path),
235
- }
236
- return plistlib.dumps(payload, fmt=plistlib.FMT_XML, sort_keys=False)
195
+ return _LAUNCHD_PLIST_TEMPLATE.render(
196
+ {
197
+ "label": LAUNCHD_LABEL,
198
+ "managed_marker": MANAGED_MARKER,
199
+ "wrapper_path": str(wrapper_path),
200
+ "data_dir": str(context.data_dir),
201
+ "config_path": str(context.config_path),
202
+ "environment_file": (
203
+ "" if context.env_file is None else str(context.env_file)
204
+ ),
205
+ "executable": str(_require_executable(context)),
206
+ "log_path": str(context.log_path),
207
+ }
208
+ ).encode("utf-8")
237
209
 
238
210
 
239
211
  def _render_windows_wrapper(context: SystemServiceContext) -> str:
240
- executable = _powershell_literal(_require_executable(context))
241
- config_path = _powershell_literal(context.config_path)
242
- environment_script = _powershell_literal(context.env_file)
243
- log_path = _powershell_literal(context.log_path)
244
- return f"""# {MANAGED_MARKER}
245
- $ErrorActionPreference = 'Stop'
246
-
247
- $executable = {executable}
248
- $configPath = {config_path}
249
- $environmentScript = {environment_script}
250
- $logPath = {log_path}
251
-
252
- if ($environmentScript -and (Test-Path -LiteralPath $environmentScript)) {{
253
- try {{
254
- . $environmentScript
255
- }} catch {{
256
- $_ | Out-String | Add-Content -LiteralPath $logPath -Encoding utf8
257
- exit 1
258
- }}
259
- }}
260
-
261
- # Native stderr is routed to the service log and must not terminate the wrapper.
262
- $ErrorActionPreference = 'Continue'
263
- try {{
264
- & $executable run --config $configPath *>> $logPath
265
- $exitCode = $LASTEXITCODE
266
- }} catch {{
267
- $_ | Out-String | Add-Content -LiteralPath $logPath -Encoding utf8
268
- exit 1
269
- }}
270
- exit $exitCode
271
- """
212
+ return _WINDOWS_WRAPPER_TEMPLATE.render(
213
+ {
214
+ "managed_marker": MANAGED_MARKER,
215
+ "executable": _powershell_literal(_require_executable(context)),
216
+ "config_path": _powershell_literal(context.config_path),
217
+ "environment_script": _powershell_literal(context.env_file),
218
+ "log_path": _powershell_literal(context.log_path),
219
+ }
220
+ )
272
221
 
273
222
 
274
223
  def _vbs_literal(value: Path | str) -> str:
@@ -280,80 +229,27 @@ def _render_windows_launcher(wrapper_path: Path) -> str:
280
229
  "powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden "
281
230
  f'-ExecutionPolicy Bypass -File "{wrapper_path}"'
282
231
  )
283
- return f"""' {MANAGED_MARKER}
284
- Option Explicit
285
-
286
- Dim shell
287
- Dim command
288
- Dim exitCode
289
-
290
- Set shell = CreateObject("WScript.Shell")
291
- command = {command}
292
- exitCode = shell.Run(command, 0, True)
293
- WScript.Quit exitCode
294
- """
295
-
296
-
297
- def _add_task_element(
298
- parent: ElementTree.Element,
299
- name: str,
300
- text: str | None = None,
301
- ) -> ElementTree.Element:
302
- element = ElementTree.SubElement(parent, f"{{{TASK_NAMESPACE}}}{name}")
303
- if text is not None:
304
- element.text = text
305
- return element
232
+ return _WINDOWS_LAUNCHER_TEMPLATE.render(
233
+ {
234
+ "managed_marker": MANAGED_MARKER,
235
+ "command": command,
236
+ }
237
+ )
306
238
 
307
239
 
308
240
  def _render_windows_task(
309
241
  context: SystemServiceContext,
310
242
  launcher_path: Path,
311
243
  ) -> bytes:
312
- ElementTree.register_namespace("", TASK_NAMESPACE)
313
- task = ElementTree.Element(
314
- f"{{{TASK_NAMESPACE}}}Task",
315
- {"version": "1.4"},
316
- )
317
- registration = _add_task_element(task, "RegistrationInfo")
318
- _add_task_element(registration, "Author", context.user)
319
- _add_task_element(registration, "Description", MANAGED_MARKER)
320
- triggers = _add_task_element(task, "Triggers")
321
- logon_trigger = _add_task_element(triggers, "LogonTrigger")
322
- _add_task_element(logon_trigger, "Enabled", "true")
323
- _add_task_element(logon_trigger, "UserId", context.user)
324
- principals = _add_task_element(task, "Principals")
325
- principal = ElementTree.SubElement(
326
- principals,
327
- f"{{{TASK_NAMESPACE}}}Principal",
328
- {"id": "Author"},
329
- )
330
- _add_task_element(principal, "UserId", context.user)
331
- _add_task_element(principal, "LogonType", "InteractiveToken")
332
- _add_task_element(principal, "RunLevel", "LeastPrivilege")
333
- settings = _add_task_element(task, "Settings")
334
- _add_task_element(settings, "MultipleInstancesPolicy", "IgnoreNew")
335
- _add_task_element(settings, "DisallowStartIfOnBatteries", "false")
336
- _add_task_element(settings, "StopIfGoingOnBatteries", "false")
337
- _add_task_element(settings, "AllowHardTerminate", "true")
338
- _add_task_element(settings, "StartWhenAvailable", "true")
339
- _add_task_element(settings, "ExecutionTimeLimit", "PT0S")
340
- restart = _add_task_element(settings, "RestartOnFailure")
341
- _add_task_element(restart, "Interval", "PT5M")
342
- _add_task_element(restart, "Count", "3")
343
- actions = ElementTree.SubElement(
344
- task,
345
- f"{{{TASK_NAMESPACE}}}Actions",
346
- {"Context": "Author"},
347
- )
348
- action = _add_task_element(actions, "Exec")
349
- _add_task_element(action, "Command", "wscript.exe")
350
- _add_task_element(
351
- action,
352
- "Arguments",
353
- f'//B //Nologo "{launcher_path}"',
354
- )
355
- _add_task_element(action, "WorkingDirectory", str(context.data_dir))
356
- return ElementTree.tostring(task, encoding="utf-16", xml_declaration=True)
244
+ rendered = _WINDOWS_TASK_TEMPLATE.render(
245
+ {
246
+ "user": context.user,
247
+ "managed_marker": MANAGED_MARKER,
248
+ "arguments": f'//B //Nologo "{launcher_path}"',
249
+ "data_dir": str(context.data_dir),
250
+ }
251
+ )
252
+ return rendered.encode("utf-16")
357
253
 
358
254
 
359
255
  def _write_managed_file(path: Path, content: str | bytes, *, mode: int) -> None:
@@ -134,6 +134,13 @@ class LocalCommandServer:
134
134
  if (path_stat.st_dev, path_stat.st_ino) == identity:
135
135
  await asyncio.to_thread(path.unlink)
136
136
 
137
+ async def wait_failure(self) -> None:
138
+ windows_server = self._windows_server
139
+ if windows_server is not None:
140
+ await windows_server.wait_failure()
141
+ return
142
+ await asyncio.Future()
143
+
137
144
  async def _handle_client(
138
145
  self,
139
146
  reader: asyncio.StreamReader,
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
  from typing import Any
13
13
  from urllib.parse import urlsplit
14
14
 
15
+ from bazaar_compute_node.core.lifecycle import TaskFailureSignal
15
16
  from bazaar_compute_node.core.paths import resolve_data_dir
16
17
 
17
18
  RequestHandler = Callable[[Mapping[str, object]], Awaitable[Mapping[str, object]]]
@@ -301,6 +302,7 @@ class WindowsNamedPipeServer:
301
302
  self._stop = threading.Event()
302
303
  self._ready = threading.Event()
303
304
  self._startup_errors: list[BaseException] = []
305
+ self._failure = TaskFailureSignal()
304
306
  self._thread: threading.Thread | None = None
305
307
  self._workers: set[threading.Thread] = set()
306
308
  self._workers_lock = threading.Lock()
@@ -317,6 +319,7 @@ class WindowsNamedPipeServer:
317
319
  self._stop.clear()
318
320
  self._ready.clear()
319
321
  self._startup_errors.clear()
322
+ self._failure.reset()
320
323
  self._thread = threading.Thread(
321
324
  target=self._serve,
322
325
  name="bcn-windows-named-pipe",
@@ -332,6 +335,7 @@ class WindowsNamedPipeServer:
332
335
  raise TimeoutError("Windows named pipe server did not become ready")
333
336
 
334
337
  async def stop(self) -> None:
338
+ self._failure.disable()
335
339
  thread = self._thread
336
340
  if thread is None:
337
341
  _release_mutex(self._mutex)
@@ -352,8 +356,16 @@ class WindowsNamedPipeServer:
352
356
  try:
353
357
  self._serve_loop()
354
358
  except Exception as error: # noqa: BLE001
355
- self._startup_errors.append(error)
356
- self._ready.set()
359
+ if not self._ready.is_set():
360
+ self._startup_errors.append(error)
361
+ self._ready.set()
362
+ return
363
+ loop = self._loop
364
+ if loop is not None:
365
+ loop.call_soon_threadsafe(self._failure.fail, error)
366
+
367
+ async def wait_failure(self) -> None:
368
+ await self._failure.wait()
357
369
 
358
370
  def _serve_loop(self) -> None:
359
371
  initialized = False
@@ -578,6 +578,13 @@ def serialize_send(result: Mapping[str, object]) -> str:
578
578
  ) from error
579
579
 
580
580
 
581
+ def serialize_unfollow(result: Mapping[str, object]) -> str:
582
+ target = cast(str, result["target"])
583
+ if cast(bool, result["changed"]):
584
+ return f"Thread unfollowed: {target}"
585
+ return f"Thread was already unfollowed: {target}"
586
+
587
+
581
588
  def _reminder(result: Mapping[str, object]) -> Mapping[str, object]:
582
589
  return cast(Mapping[str, object], result["reminder"])
583
590
 
@@ -694,6 +701,8 @@ async def async_main(argv: Sequence[str] | None = None) -> int:
694
701
  "cancel": serialize_reminder_cancel,
695
702
  }[args.command]
696
703
  print(serializer(result))
704
+ elif args.resource == "thread" and args.command == "unfollow":
705
+ print(serialize_unfollow(result))
697
706
  return 0
698
707
 
699
708