bazaar-compute-node 0.1.24__tar.gz → 0.1.26__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 (139) hide show
  1. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/agent.py +7 -10
  5. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/command.py +287 -56
  6. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/system_service.py +42 -8
  7. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/bcc.py +34 -152
  8. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/channel.py +10 -8
  9. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/sqlite/codec.py +152 -215
  10. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/database.py +288 -0
  11. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/executor.py +544 -0
  12. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/__init__.py +57 -0
  13. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/registry.py +164 -0
  14. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/migrations.py → bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v01_initial_node_schema.py +2 -339
  15. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v02_session_mapping_indexes.py +31 -0
  16. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v03_message_log_indexes.py +23 -0
  17. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v04_runtime_attempt_facts.py +46 -0
  18. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v05_inbound_message_references.py +33 -0
  19. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v06_inbound_message_reference_integrity.py +25 -0
  20. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v07_inbound_provider_identity.py +24 -0
  21. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v08_transient_stream_events.py +29 -0
  22. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v09_remove_runtime_events.py +20 -0
  23. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v10_add_outbound_attachments.py +17 -0
  24. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v11_remove_runtime_session_mapping.py +19 -0
  25. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py → bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v12_add_reminders.py +1 -1
  26. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/agent_migration.py → bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v13_add_agent_ownership.py +1 -1
  27. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/inbox_migration.py → bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v14_add_inbox_discovery_indexes.py +1 -1
  28. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/handoff_migration.py → bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v15_add_handoffs.py +1 -1
  29. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v16_remove_outbound_drafts.py +112 -0
  30. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v17_remove_agent_identity_triggers.py +20 -0
  31. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/v18_unify_messages.py +400 -0
  32. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/sqlite/plugin.py +2 -1
  33. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/repository/__init__.py +5 -0
  34. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/repository/base.py +168 -0
  35. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/repository/facade.py +143 -0
  36. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/handoff_repository.py → bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/repository/handoffs.py +8 -34
  37. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/repository/messages.py +813 -0
  38. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py → bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/repository/reminders.py +221 -233
  39. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/repository/sessions.py +272 -0
  40. bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/storage.py +154 -0
  41. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/channel.py +11 -8
  42. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/wecom/channel.py +13 -10
  43. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/channel.py +4 -4
  44. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/command.py +51 -6
  45. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/instruction.py +5 -1
  46. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/models/__init__.py +4 -6
  47. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/models/entities.py +116 -113
  48. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/models/states.py +5 -20
  49. bazaar_compute_node-0.1.26/src/bazaar_compute_node/core/orchestration/command.py +475 -0
  50. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/error_feedback.py +4 -3
  51. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/handoff_command.py +49 -105
  52. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/reminder.py +9 -15
  53. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/reminder_command.py +36 -96
  54. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/services.py +3 -4
  55. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/session.py +100 -221
  56. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/turn.py +8 -8
  57. bazaar_compute_node-0.1.26/src/bazaar_compute_node/core/storage.py +753 -0
  58. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/database.py +0 -220
  59. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -767
  60. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +0 -903
  61. bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/storage.py +0 -74
  62. bazaar_compute_node-0.1.24/src/bazaar_compute_node/core/orchestration/command.py +0 -707
  63. bazaar_compute_node-0.1.24/src/bazaar_compute_node/core/storage.py +0 -220
  64. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/LICENSE +0 -0
  65. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/README.md +0 -0
  66. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/__init__.py +0 -0
  67. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/__init__.py +0 -0
  68. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/agent_management.py +0 -0
  69. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/application.py +0 -0
  70. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/attachments.py +0 -0
  71. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/config.py +0 -0
  72. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/registry.py +0 -0
  73. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/resource_dispatch.py +0 -0
  74. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/transport.py +0 -0
  75. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  76. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/app/wrapper.py +0 -0
  77. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/cli.py +0 -0
  78. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  79. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  80. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  81. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  82. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  83. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  84. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  85. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  86. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  87. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/__init__.py +0 -0
  88. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/api.py +0 -0
  89. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/approval.py +0 -0
  90. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/attachments.py +0 -0
  91. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/frame.py +0 -0
  92. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/identity.py +0 -0
  93. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/outbound.py +0 -0
  94. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/plugin.py +0 -0
  95. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/lark/transport.py +0 -0
  96. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  97. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  98. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  99. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  100. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/sqlite/handoff_codec.py +0 -0
  101. /bazaar_compute_node-0.1.24/src/bazaar_compute_node/contrib/sqlite/migration.py → /bazaar_compute_node-0.1.26/src/bazaar_compute_node/contrib/sqlite/migrations/model.py +0 -0
  102. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  103. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  104. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  105. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  106. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  107. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  108. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  109. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  110. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  111. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  112. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  113. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  114. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  115. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/__init__.py +0 -0
  116. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/approval.py +0 -0
  117. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/audit.py +0 -0
  118. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/client.py +0 -0
  119. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/concurrency.py +0 -0
  120. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/correlation.py +0 -0
  121. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/handoff.py +0 -0
  122. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/inbox.py +0 -0
  123. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  124. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/models/handoff.py +0 -0
  125. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  126. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  127. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/observability.py +0 -0
  128. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  129. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  130. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/outcomes.py +0 -0
  131. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/paths.py +0 -0
  132. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/reminder.py +0 -0
  133. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  134. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/runtime.py +0 -0
  135. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  136. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  137. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  138. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/i18n/english.py +0 -0
  139. {bazaar_compute_node-0.1.24 → bazaar_compute_node-0.1.26}/src/bazaar_compute_node/i18n/schinese.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.24
3
+ Version: 0.1.26
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.24"
3
+ version = "0.1.26"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.24"
3
+ version = "0.1.26"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -279,8 +279,7 @@ class AgentApplication:
279
279
  async def has_session(self, session_id: str) -> bool:
280
280
  if not isinstance(session_id, str) or not session_id:
281
281
  return False
282
- async with self.storage.transaction() as transaction:
283
- return await transaction.get_bcn_session(session_id) is not None
282
+ return await self.storage.get_bcn_session(session_id) is not None
284
283
 
285
284
  async def publish_reminder_wake(self, session_id: str) -> None:
286
285
  if not self._started:
@@ -323,8 +322,7 @@ class AgentApplication:
323
322
  self._wrapper_path = None
324
323
 
325
324
  async def _referenced_attachment_paths(self) -> set[str]:
326
- async with self.storage.transaction() as transaction:
327
- return set(await transaction.list_ready_attachment_paths())
325
+ return set(await self.storage.list_ready_attachment_paths())
328
326
 
329
327
  def _adapter_context(self) -> Mapping[str, object]:
330
328
  return {
@@ -370,12 +368,11 @@ class AgentApplication:
370
368
  )
371
369
  runtime_session_id = request.get("runtime_session_id")
372
370
  session_capability = request.get("session_capability")
373
- async with self.storage.transaction() as transaction:
374
- if await transaction.get_bcn_session(session_id) is None:
375
- raise CommandDispatchError(
376
- "SESSION_NOT_FOUND",
377
- f"unknown bcn session: {session_id}",
378
- )
371
+ if await self.storage.get_bcn_session(session_id) is None:
372
+ raise CommandDispatchError(
373
+ "SESSION_NOT_FOUND",
374
+ f"unknown bcn session: {session_id}",
375
+ )
379
376
  runtime_session = self.orchestrator.runtime_session(session_id)
380
377
  if runtime_session is None or runtime_session_id != runtime_session.id:
381
378
  raise CommandDispatchError(
@@ -1,31 +1,78 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import json
4
5
  from collections.abc import Awaitable, Callable, Mapping
5
6
  from datetime import UTC, datetime
6
7
  from time import time_ns
7
8
  from typing import Annotated, Literal, cast
8
9
 
9
- from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, ValidationError
10
+ from pydantic import (
11
+ BaseModel,
12
+ ConfigDict,
13
+ Field,
14
+ StrictBool,
15
+ StrictInt,
16
+ StrictStr,
17
+ ValidationError,
18
+ )
10
19
 
11
20
  from ..core.command import (
12
21
  ICommandService,
13
22
  InboxListResult,
23
+ MessageSendFreshnessHold,
14
24
  MessageSendHandoffRequired,
15
25
  SessionNotFoundError,
16
26
  )
17
27
  from ..core.lifecycle import TimeoutBudget
18
- from ..core.models import InboundMessage, InboxTargetSummary, OutboundMessage
28
+ from ..core.models import (
29
+ InboundAttachment,
30
+ InboxTargetSummary,
31
+ Message,
32
+ MessageDirection,
33
+ OutboundAttachment,
34
+ OutboundDeliveryState,
35
+ )
36
+
37
+
38
+ def _serialize_attachment(
39
+ attachment: InboundAttachment | OutboundAttachment,
40
+ ) -> dict[str, object]:
41
+ if isinstance(attachment, InboundAttachment):
42
+ return {
43
+ "attachment_id": attachment.attachment_id,
44
+ "name": attachment.name,
45
+ "kind": attachment.kind,
46
+ "state": attachment.state,
47
+ "media_type": attachment.media_type,
48
+ "relative_path": attachment.relative_path,
49
+ "size_bytes": attachment.size_bytes,
50
+ "error": attachment.error,
51
+ "sha256": None,
52
+ }
53
+ return {
54
+ "attachment_id": None,
55
+ "name": attachment.name,
56
+ "kind": "file",
57
+ "state": "ready",
58
+ "media_type": attachment.media_type,
59
+ "relative_path": attachment.relative_path,
60
+ "size_bytes": attachment.size_bytes,
61
+ "error": None,
62
+ "sha256": attachment.sha256,
63
+ }
19
64
 
20
65
 
21
- def serialize_inbound(message: InboundMessage) -> dict[str, object]:
66
+ def serialize_message(message: Message) -> dict[str, object]:
22
67
  return {
23
68
  "seq": message.seq,
24
69
  "message_id": message.message_id,
70
+ "direction": message.direction.value,
25
71
  "session_id": message.session_id,
26
72
  "channel_session_id": message.channel_session_id,
27
73
  "channel": message.channel,
28
74
  "received_at_ms": message.received_at_ms,
75
+ "created_at_ms": message.created_at_ms,
29
76
  "provider_time_ms": message.provider_time_ms,
30
77
  "sender": (
31
78
  None
@@ -34,58 +81,21 @@ def serialize_inbound(message: InboundMessage) -> dict[str, object]:
34
81
  ),
35
82
  "sender_kind": message.sender_kind.value,
36
83
  "message_type": message.message_type,
37
- "canonical_target": message.canonical_target,
84
+ "canonical_target": message.target,
38
85
  "target_kind": message.target_kind.value,
39
86
  "mentions_agent": message.mentions_agent,
40
87
  "notifies_runtime": message.notifies_runtime,
41
88
  "attachments": [
42
- {
43
- "attachment_id": attachment.attachment_id,
44
- "name": attachment.name,
45
- "kind": attachment.kind,
46
- "state": attachment.state,
47
- "media_type": attachment.media_type,
48
- "relative_path": attachment.relative_path,
49
- "size_bytes": attachment.size_bytes,
50
- "error": attachment.error,
51
- }
52
- for attachment in message.attachments
89
+ _serialize_attachment(attachment) for attachment in message.attachments
53
90
  ],
54
91
  "body": message.body,
55
92
  "reply_to_message_id": message.reply_to_message_id,
56
- }
57
-
58
-
59
- def serialize_outbound(message: OutboundMessage) -> dict[str, object]:
60
- return {
61
- "outbound_message_id": message.outbound_message_id,
62
- "command_id": message.command_id,
63
- "session_id": message.session_id,
64
- "channel_session_id": message.channel_session_id,
65
- "target": message.target,
66
- "reply_to_message_id": message.reply_to_message_id,
67
- "body": message.body,
68
- "attachments": [
69
- {
70
- "name": attachment.name,
71
- "relative_path": attachment.relative_path,
72
- "media_type": attachment.media_type,
73
- "size_bytes": attachment.size_bytes,
74
- "sha256": attachment.sha256,
75
- }
76
- for attachment in message.attachments
77
- ],
78
- "state": message.state.value,
79
- "fresh_check_state": message.fresh_check_state.value,
80
- "created_at_ms": message.created_at_ms,
81
- "snapshot_seq": message.snapshot_seq,
82
- "current_inbound_seq": message.current_inbound_seq,
83
- "provider_attempted_at_ms": message.provider_attempted_at_ms,
84
- "completed_at_ms": message.completed_at_ms,
85
- "draft_saved_at_ms": message.draft_saved_at_ms,
86
- "error_kind": message.error_kind,
87
- "error_message": message.error_message,
88
- "next_action": message.next_action,
93
+ "delivery_state": (
94
+ message.delivery_state.value
95
+ if message.direction is MessageDirection.OUTBOUND
96
+ and message.delivery_state is not None
97
+ else None
98
+ ),
89
99
  }
90
100
 
91
101
 
@@ -172,6 +182,7 @@ class _MessageSendRequest(_CommandRequest):
172
182
  command_id: NonEmptyText
173
183
  attachment_paths: list[NonEmptyText] = Field(default_factory=list)
174
184
  reply_to_message_id: NonEmptyText | None = None
185
+ send_draft: StrictBool = False
175
186
  created_at_ms: NonNegativeInt = Field(
176
187
  default_factory=lambda: time_ns() // 1_000_000
177
188
  )
@@ -426,10 +437,10 @@ class CommandDispatcher:
426
437
  "ok": True,
427
438
  "result": {
428
439
  "messages": [
429
- serialize_inbound(message) for message in result.messages
440
+ serialize_message(message) for message in result.messages
430
441
  ],
431
442
  "referenced_messages": [
432
- serialize_inbound(message)
443
+ serialize_message(message)
433
444
  for message in result.referenced_messages
434
445
  ],
435
446
  "snapshot_seq": result.snapshot_seq,
@@ -458,10 +469,10 @@ class CommandDispatcher:
458
469
  "ok": True,
459
470
  "result": {
460
471
  "messages": [
461
- serialize_inbound(message) for message in result.messages
472
+ serialize_message(message) for message in result.messages
462
473
  ],
463
474
  "referenced_messages": [
464
- serialize_inbound(message)
475
+ serialize_message(message)
465
476
  for message in result.referenced_messages
466
477
  ],
467
478
  "snapshot_seq": result.snapshot_seq,
@@ -480,17 +491,59 @@ class CommandDispatcher:
480
491
  created_at_ms=request.created_at_ms,
481
492
  attachment_paths=tuple(request.attachment_paths),
482
493
  reply_to_message_id=request.reply_to_message_id,
494
+ send_draft=request.send_draft,
483
495
  )
496
+ if isinstance(result, MessageSendFreshnessHold):
497
+ return {
498
+ "ok": True,
499
+ "result": {"text": format_freshness_hold(result)},
500
+ }
484
501
  if isinstance(result, MessageSendHandoffRequired):
485
502
  return {
486
503
  "ok": True,
487
- "result": {
488
- "handoff_required": {"target": result.target},
489
- },
504
+ "result": {"text": format_cross_session_hold(result.target)},
490
505
  }
506
+ delivery_state = result.delivery_state
507
+ if delivery_state is None:
508
+ raise RuntimeError("outbound message has no delivery state")
509
+ if delivery_state is OutboundDeliveryState.SENT:
510
+ text = (
511
+ f"Message sent to {result.target}. Message ID: {result.message_id}"
512
+ )
513
+ elif delivery_state is OutboundDeliveryState.QUEUED:
514
+ text = (
515
+ f"Message queued to {result.target}. "
516
+ f"Message ID: {result.message_id}"
517
+ )
518
+ elif delivery_state is OutboundDeliveryState.PARTIAL:
519
+ raise CommandDispatchError(
520
+ "SEND_PARTIAL",
521
+ result.error_message
522
+ or "Message delivery was only partially confirmed.",
523
+ next_action=(
524
+ "Do not retry the complete message automatically; reconcile "
525
+ "confirmed delivery first."
526
+ ),
527
+ )
528
+ elif delivery_state is OutboundDeliveryState.UNKNOWN:
529
+ raise CommandDispatchError(
530
+ "SEND_UNKNOWN",
531
+ result.error_message or "Message delivery outcome is unknown.",
532
+ next_action="Reconcile channel delivery before retrying.",
533
+ )
534
+ elif delivery_state is OutboundDeliveryState.FAILED:
535
+ raise CommandDispatchError(
536
+ "SEND_FAILED",
537
+ result.error_message or "Message delivery failed.",
538
+ next_action="Fix the provider error before retrying.",
539
+ )
540
+ else:
541
+ raise AssertionError(
542
+ f"message send returned unsupported state: {delivery_state.value}"
543
+ )
491
544
  return {
492
545
  "ok": True,
493
- "result": {"outbound": serialize_outbound(result)},
546
+ "result": {"text": text},
494
547
  }
495
548
 
496
549
  if resource == "thread" and command == "unfollow":
@@ -507,3 +560,181 @@ def format_message_time(timestamp_ms: int) -> str:
507
560
  .astimezone()
508
561
  .strftime("%Y-%m-%d %H:%M:%S")
509
562
  )
563
+
564
+
565
+ def _message_timestamp(message: Mapping[str, object]) -> int:
566
+ for field_name in ("provider_time_ms", "received_at_ms", "created_at_ms"):
567
+ timestamp = message.get(field_name)
568
+ if timestamp is not None:
569
+ return cast(int, timestamp)
570
+ raise ValueError("message has no display timestamp")
571
+
572
+
573
+ def _message_header_fields(
574
+ message: Mapping[str, object],
575
+ ) -> tuple[str, str, str, str, str | None, str]:
576
+ target = cast(str, message["canonical_target"])
577
+ message_id = cast(str, message["message_id"])
578
+ sender_kind = cast(str, message["sender_kind"])
579
+ sender_value = message["sender"]
580
+ sender: str | None = None
581
+ if sender_value is not None:
582
+ sender_mapping = cast(Mapping[str, object], sender_value)
583
+ sender_id = cast(str | None, sender_mapping.get("id"))
584
+ sender_name = cast(str | None, sender_mapping.get("name"))
585
+ if sender_id is not None and sender_name is not None:
586
+ sender = f"@{sender_id}({sender_name})"
587
+ elif sender_name is not None:
588
+ sender = f"@{sender_name}"
589
+ elif sender_id is not None:
590
+ sender = f"@{sender_id}"
591
+ return (
592
+ target,
593
+ message_id,
594
+ format_message_time(_message_timestamp(message)),
595
+ sender_kind,
596
+ sender,
597
+ cast(str, message["body"]),
598
+ )
599
+
600
+
601
+ def _attachment_suffix(message: Mapping[str, object]) -> str:
602
+ attachments = cast(list[Mapping[str, object]], message["attachments"])
603
+ if not attachments:
604
+ return ""
605
+ rendered: list[str] = []
606
+ for attachment in attachments:
607
+ name = cast(str, attachment["name"])
608
+ attachment_id = cast(str | None, attachment.get("attachment_id"))
609
+ state = cast(str, attachment["state"])
610
+ identity = f"id:{attachment_id}, " if attachment_id is not None else ""
611
+ if state == "ready":
612
+ path = cast(str, attachment["relative_path"])
613
+ rendered.append(f"{name} ({identity}path:{path})")
614
+ else:
615
+ error = cast(str, attachment["error"])
616
+ rendered.append(f"{name} ({identity}state:failed, error:{error})")
617
+ label = "attachment" if len(rendered) == 1 else "attachments"
618
+ return f" [{len(rendered)} {label}: {', '.join(rendered)}]"
619
+
620
+
621
+ def format_check_message(message: Mapping[str, object]) -> str:
622
+ target, message_id, timestamp, sender_kind, sender, body = _message_header_fields(
623
+ message
624
+ )
625
+ line = (
626
+ f"[target={target} msg={message_id} time={timestamp} "
627
+ f"type={sender_kind} mentioned={str(message['mentions_agent']).lower()}"
628
+ )
629
+ reply_to_message_id = message["reply_to_message_id"]
630
+ if reply_to_message_id is not None:
631
+ line += f" reply_to={cast(str, reply_to_message_id)}"
632
+ line += "] "
633
+ if sender is not None:
634
+ line += f"{sender} "
635
+ return line + body + _attachment_suffix(message)
636
+
637
+
638
+ def format_read_message(
639
+ message: Mapping[str, object],
640
+ *,
641
+ index: int,
642
+ count: int,
643
+ ) -> str:
644
+ target, message_id, timestamp, sender_kind, sender, body = _message_header_fields(
645
+ message
646
+ )
647
+ fields = [
648
+ f"seq={cast(int, message['seq'])}",
649
+ f"msg={message_id}",
650
+ f"time={timestamp}",
651
+ f"type={sender_kind}",
652
+ f"replyTarget={target}",
653
+ f"mentioned={str(message['mentions_agent']).lower()}",
654
+ ]
655
+ reply_to_message_id = message["reply_to_message_id"]
656
+ if reply_to_message_id is not None:
657
+ fields.append(f"replyTo={cast(str, reply_to_message_id)}")
658
+ line = f"[{index}/{count} {' '.join(fields)}] "
659
+ if sender is not None:
660
+ line += f"{sender} "
661
+ return line + body + _attachment_suffix(message)
662
+
663
+
664
+ def format_freshness_hold(result: MessageSendFreshnessHold) -> str:
665
+ messages = [serialize_message(message) for message in result.messages]
666
+ referenced_messages = [
667
+ serialize_message(message) for message in result.referenced_messages
668
+ ]
669
+ shown = len(messages)
670
+ total = result.newer_message_total
671
+ message_label = "message" if total == 1 else "messages"
672
+ if messages:
673
+ bounds = f"{messages[0]['seq']}-{messages[-1]['seq']}"
674
+ else:
675
+ bounds = "none-none"
676
+ older_bound = (
677
+ "Older unreviewed messages are omitted."
678
+ if total > shown
679
+ else "No older unreviewed messages."
680
+ )
681
+ newer_bound = "No newer unreviewed messages."
682
+ lines = [
683
+ f"Unreviewed synced context for this target: {total} {message_label}.",
684
+ (
685
+ "Your message has been saved as a draft. Review this target's "
686
+ "synced context before sending."
687
+ ),
688
+ "",
689
+ (
690
+ f"Read window: {shown} returned, seq {bounds}, oldest to newest. "
691
+ f"{older_bound} {newer_bound}"
692
+ ),
693
+ "",
694
+ ]
695
+ if referenced_messages:
696
+ lines.append(f"Referenced messages: {len(referenced_messages)}")
697
+ lines.extend(
698
+ format_read_message(message, index=index, count=len(referenced_messages))
699
+ for index, message in enumerate(referenced_messages, start=1)
700
+ )
701
+ lines.append("Window messages:")
702
+ lines.extend(
703
+ format_read_message(message, index=index, count=shown)
704
+ for index, message in enumerate(messages, start=1)
705
+ )
706
+ lines.extend(
707
+ (
708
+ "",
709
+ f"End of window: {shown}/{total} shown.",
710
+ "",
711
+ "To update the draft, send revised content normally:",
712
+ f" bcc message send --target {json.dumps(result.target)} <<'BCCMSG'",
713
+ " revised message",
714
+ " BCCMSG",
715
+ "To send the current draft unchanged:",
716
+ f" bcc message send --send-draft --target {json.dumps(result.target)}",
717
+ "You can also choose not to send anything.",
718
+ )
719
+ )
720
+ return "\n".join(lines)
721
+
722
+
723
+ def format_cross_session_hold(target: str) -> str:
724
+ quoted_target = json.dumps(target)
725
+ return "\n".join(
726
+ (
727
+ "Your message was not sent because the target belongs to another conversation.",
728
+ "",
729
+ (
730
+ "To continue this work in the target conversation, send a "
731
+ "self-contained handoff:"
732
+ ),
733
+ f" bcc handoff send --target {quoted_target} <<'BCCMSG'",
734
+ " enough context to understand the background, goal, and next action",
735
+ " BCCMSG",
736
+ "This creates a handoff notice that wakes you in that conversation.",
737
+ "",
738
+ "You can also choose not to send anything.",
739
+ )
740
+ )
@@ -271,6 +271,29 @@ exit $exitCode
271
271
  """
272
272
 
273
273
 
274
+ def _vbs_literal(value: Path | str) -> str:
275
+ return '"' + str(value).replace('"', '""') + '"'
276
+
277
+
278
+ def _render_windows_launcher(wrapper_path: Path) -> str:
279
+ command = _vbs_literal(
280
+ "powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden "
281
+ f'-ExecutionPolicy Bypass -File "{wrapper_path}"'
282
+ )
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
+
274
297
  def _add_task_element(
275
298
  parent: ElementTree.Element,
276
299
  name: str,
@@ -284,7 +307,7 @@ def _add_task_element(
284
307
 
285
308
  def _render_windows_task(
286
309
  context: SystemServiceContext,
287
- wrapper_path: Path,
310
+ launcher_path: Path,
288
311
  ) -> bytes:
289
312
  ElementTree.register_namespace("", TASK_NAMESPACE)
290
313
  task = ElementTree.Element(
@@ -323,11 +346,11 @@ def _render_windows_task(
323
346
  {"Context": "Author"},
324
347
  )
325
348
  action = _add_task_element(actions, "Exec")
326
- _add_task_element(action, "Command", "powershell.exe")
349
+ _add_task_element(action, "Command", "wscript.exe")
327
350
  _add_task_element(
328
351
  action,
329
352
  "Arguments",
330
- f'-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{wrapper_path}"',
353
+ f'//B //Nologo "{launcher_path}"',
331
354
  )
332
355
  _add_task_element(action, "WorkingDirectory", str(context.data_dir))
333
356
  return ElementTree.tostring(task, encoding="utf-16", xml_declaration=True)
@@ -423,10 +446,11 @@ def _launchd_paths() -> tuple[Path, Path]:
423
446
  )
424
447
 
425
448
 
426
- def _windows_paths(context: SystemServiceContext) -> tuple[Path, Path]:
449
+ def _windows_paths(context: SystemServiceContext) -> tuple[Path, Path, Path]:
427
450
  return (
428
451
  context.data_dir / "bcn-system-service.xml",
429
452
  context.data_dir / "bcn-system-service.ps1",
453
+ context.data_dir / "bcn-system-service.vbs",
430
454
  )
431
455
 
432
456
 
@@ -462,16 +486,21 @@ def _install_macos(context: SystemServiceContext) -> None:
462
486
 
463
487
 
464
488
  def _install_windows(context: SystemServiceContext) -> None:
465
- xml_path, wrapper_path = _windows_paths(context)
489
+ xml_path, wrapper_path, launcher_path = _windows_paths(context)
466
490
  context.data_dir.mkdir(parents=True, exist_ok=True)
467
491
  _write_managed_file(
468
492
  wrapper_path,
469
493
  _render_windows_wrapper(context),
470
494
  mode=0o600,
471
495
  )
496
+ _write_managed_file(
497
+ launcher_path,
498
+ _render_windows_launcher(wrapper_path),
499
+ mode=0o600,
500
+ )
472
501
  _write_managed_file(
473
502
  xml_path,
474
- _render_windows_task(context, wrapper_path),
503
+ _render_windows_task(context, launcher_path),
475
504
  mode=0o600,
476
505
  )
477
506
  _run_native_command(
@@ -686,15 +715,20 @@ def _uninstall_macos() -> None:
686
715
 
687
716
 
688
717
  def _uninstall_windows(context: SystemServiceContext) -> None:
689
- xml_path, wrapper_path = _windows_paths(context)
718
+ xml_path, wrapper_path, launcher_path = _windows_paths(context)
690
719
  _stop_windows(context)
691
720
  _run_native_command(
692
721
  ["schtasks", "/Delete", "/TN", WINDOWS_TASK_NAME, "/F"],
693
722
  check=False,
694
723
  )
695
724
  _remove_managed_file(xml_path)
725
+ _remove_managed_file(launcher_path)
696
726
  _remove_managed_file(wrapper_path)
697
- if not xml_path.exists() and not wrapper_path.exists():
727
+ if (
728
+ not xml_path.exists()
729
+ and not launcher_path.exists()
730
+ and not wrapper_path.exists()
731
+ ):
698
732
  print("system service uninstalled platform=windows", flush=True)
699
733
 
700
734