bazaar-compute-node 0.1.23__tar.gz → 0.1.25__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 (115) hide show
  1. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/command.py +228 -39
  5. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/system_service.py +42 -8
  6. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/bcc.py +34 -152
  7. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/outbound.py +5 -1
  8. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/codec.py +26 -114
  9. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/migrations.py +2 -0
  10. bazaar_compute_node-0.1.25/src/bazaar_compute_node/contrib/sqlite/outbound_draft_migration.py +112 -0
  11. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/repository.py +59 -20
  12. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +32 -4
  13. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/command.py +43 -2
  14. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/instruction.py +5 -1
  15. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/models/__init__.py +0 -2
  16. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/models/entities.py +7 -58
  17. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/models/states.py +0 -20
  18. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/command.py +151 -208
  19. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/storage.py +12 -4
  20. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/LICENSE +0 -0
  21. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/README.md +0 -0
  22. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/__init__.py +0 -0
  23. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/__init__.py +0 -0
  24. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/agent.py +0 -0
  25. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/agent_management.py +0 -0
  26. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/application.py +0 -0
  27. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/attachments.py +0 -0
  28. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/config.py +0 -0
  29. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/registry.py +0 -0
  30. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/resource_dispatch.py +0 -0
  31. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/transport.py +0 -0
  32. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  33. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/app/wrapper.py +0 -0
  34. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/cli.py +0 -0
  35. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  36. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  37. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  38. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  39. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  40. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  41. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  42. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  43. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  44. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/__init__.py +0 -0
  45. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/api.py +0 -0
  46. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/approval.py +0 -0
  47. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/attachments.py +0 -0
  48. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/channel.py +0 -0
  49. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/frame.py +0 -0
  50. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/identity.py +0 -0
  51. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/plugin.py +0 -0
  52. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/lark/transport.py +0 -0
  53. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  54. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  55. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  56. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  57. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +0 -0
  58. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  59. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/handoff_codec.py +0 -0
  60. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/handoff_migration.py +0 -0
  61. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/handoff_repository.py +0 -0
  62. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/inbox_migration.py +0 -0
  63. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/migration.py +0 -0
  64. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  65. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  66. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
  67. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +0 -0
  68. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/sqlite/storage.py +0 -0
  69. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  70. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  71. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  72. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  73. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/channel.py +0 -0
  74. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  75. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  76. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  77. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  78. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  79. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  80. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  81. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  82. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  83. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/__init__.py +0 -0
  84. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/approval.py +0 -0
  85. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/audit.py +0 -0
  86. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/channel.py +0 -0
  87. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/client.py +0 -0
  88. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/concurrency.py +0 -0
  89. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/correlation.py +0 -0
  90. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/handoff.py +0 -0
  91. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/inbox.py +0 -0
  92. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  93. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/models/handoff.py +0 -0
  94. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  95. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  96. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/observability.py +0 -0
  97. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  98. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  99. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  100. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/handoff_command.py +0 -0
  101. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  102. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  103. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  104. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/session.py +0 -0
  105. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  106. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/outcomes.py +0 -0
  107. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/paths.py +0 -0
  108. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/reminder.py +0 -0
  109. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  110. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/runtime.py +0 -0
  111. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  112. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  113. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  114. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/src/bazaar_compute_node/i18n/english.py +0 -0
  115. {bazaar_compute_node-0.1.23 → bazaar_compute_node-0.1.25}/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.23
3
+ Version: 0.1.25
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.23"
3
+ version = "0.1.25"
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.23"
3
+ version = "0.1.25"
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,21 +1,31 @@
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 InboundMessage, InboxTargetSummary, OutboundDeliveryState
19
29
 
20
30
 
21
31
  def serialize_inbound(message: InboundMessage) -> dict[str, object]:
@@ -56,39 +66,6 @@ def serialize_inbound(message: InboundMessage) -> dict[str, object]:
56
66
  }
57
67
 
58
68
 
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,
89
- }
90
-
91
-
92
69
  def serialize_inbox_target(summary: InboxTargetSummary) -> dict[str, object]:
93
70
  sender = summary.latest_sender
94
71
  latest_time_ms = (
@@ -172,6 +149,7 @@ class _MessageSendRequest(_CommandRequest):
172
149
  command_id: NonEmptyText
173
150
  attachment_paths: list[NonEmptyText] = Field(default_factory=list)
174
151
  reply_to_message_id: NonEmptyText | None = None
152
+ send_draft: StrictBool = False
175
153
  created_at_ms: NonNegativeInt = Field(
176
154
  default_factory=lambda: time_ns() // 1_000_000
177
155
  )
@@ -480,17 +458,57 @@ class CommandDispatcher:
480
458
  created_at_ms=request.created_at_ms,
481
459
  attachment_paths=tuple(request.attachment_paths),
482
460
  reply_to_message_id=request.reply_to_message_id,
461
+ send_draft=request.send_draft,
483
462
  )
463
+ if isinstance(result, MessageSendFreshnessHold):
464
+ return {
465
+ "ok": True,
466
+ "result": {"text": format_freshness_hold(result)},
467
+ }
484
468
  if isinstance(result, MessageSendHandoffRequired):
485
469
  return {
486
470
  "ok": True,
487
- "result": {
488
- "handoff_required": {"target": result.target},
489
- },
471
+ "result": {"text": format_cross_session_hold(result.target)},
490
472
  }
473
+ if result.state is OutboundDeliveryState.SENT:
474
+ text = (
475
+ f"Message sent to {result.target}. "
476
+ f"Message ID: {result.outbound_message_id}"
477
+ )
478
+ elif result.state is OutboundDeliveryState.QUEUED:
479
+ text = (
480
+ f"Message queued to {result.target}. "
481
+ f"Message ID: {result.outbound_message_id}"
482
+ )
483
+ elif result.state is OutboundDeliveryState.PARTIAL:
484
+ raise CommandDispatchError(
485
+ "SEND_PARTIAL",
486
+ result.error_message
487
+ or "Message delivery was only partially confirmed.",
488
+ next_action=(
489
+ "Do not retry the complete message automatically; reconcile "
490
+ "confirmed delivery first."
491
+ ),
492
+ )
493
+ elif result.state is OutboundDeliveryState.UNKNOWN:
494
+ raise CommandDispatchError(
495
+ "SEND_UNKNOWN",
496
+ result.error_message or "Message delivery outcome is unknown.",
497
+ next_action="Reconcile channel delivery before retrying.",
498
+ )
499
+ elif result.state is OutboundDeliveryState.FAILED:
500
+ raise CommandDispatchError(
501
+ "SEND_FAILED",
502
+ result.error_message or "Message delivery failed.",
503
+ next_action="Fix the provider error before retrying.",
504
+ )
505
+ else:
506
+ raise AssertionError(
507
+ f"message send returned unsupported state: {result.state.value}"
508
+ )
491
509
  return {
492
510
  "ok": True,
493
- "result": {"outbound": serialize_outbound(result)},
511
+ "result": {"text": text},
494
512
  }
495
513
 
496
514
  if resource == "thread" and command == "unfollow":
@@ -507,3 +525,174 @@ def format_message_time(timestamp_ms: int) -> str:
507
525
  .astimezone()
508
526
  .strftime("%Y-%m-%d %H:%M:%S")
509
527
  )
528
+
529
+
530
+ def _message_timestamp(message: Mapping[str, object]) -> int:
531
+ timestamp = message["provider_time_ms"]
532
+ if timestamp is None:
533
+ timestamp = message["received_at_ms"]
534
+ return cast(int, timestamp)
535
+
536
+
537
+ def _message_header_fields(
538
+ message: Mapping[str, object],
539
+ ) -> tuple[str, str, str, str, str | None, str]:
540
+ target = cast(str, message["canonical_target"])
541
+ message_id = cast(str, message["message_id"])
542
+ sender_kind = cast(str, message["sender_kind"])
543
+ sender_value = message["sender"]
544
+ sender: str | None = None
545
+ if sender_value is not None:
546
+ sender_mapping = cast(Mapping[str, object], sender_value)
547
+ sender_id = cast(str | None, sender_mapping.get("id"))
548
+ sender_name = cast(str | None, sender_mapping.get("name"))
549
+ sender = f"@{sender_id}({sender_name})" if sender_name else f"@{sender_id}"
550
+ return (
551
+ target,
552
+ message_id,
553
+ format_message_time(_message_timestamp(message)),
554
+ sender_kind,
555
+ sender,
556
+ cast(str, message["body"]),
557
+ )
558
+
559
+
560
+ def _attachment_suffix(message: Mapping[str, object]) -> str:
561
+ attachments = cast(list[Mapping[str, object]], message["attachments"])
562
+ if not attachments:
563
+ return ""
564
+ rendered: list[str] = []
565
+ for attachment in attachments:
566
+ name = cast(str, attachment["name"])
567
+ attachment_id = cast(str, attachment["attachment_id"])
568
+ state = cast(str, attachment["state"])
569
+ if state == "ready":
570
+ path = cast(str, attachment["relative_path"])
571
+ rendered.append(f"{name} (id:{attachment_id}, path:{path})")
572
+ else:
573
+ error = cast(str, attachment["error"])
574
+ rendered.append(f"{name} (id:{attachment_id}, state:failed, error:{error})")
575
+ label = "attachment" if len(rendered) == 1 else "attachments"
576
+ return f" [{len(rendered)} {label}: {', '.join(rendered)}]"
577
+
578
+
579
+ def format_check_message(message: Mapping[str, object]) -> str:
580
+ target, message_id, timestamp, message_type, sender, body = _message_header_fields(
581
+ message
582
+ )
583
+ line = (
584
+ f"[target={target} msg={message_id} time={timestamp} "
585
+ f"type={message_type} mentioned={str(message['mentions_agent']).lower()}"
586
+ )
587
+ reply_to_message_id = message["reply_to_message_id"]
588
+ if reply_to_message_id is not None:
589
+ line += f" reply_to={cast(str, reply_to_message_id)}"
590
+ line += "] "
591
+ if sender is not None:
592
+ line += f"{sender} "
593
+ return line + body + _attachment_suffix(message)
594
+
595
+
596
+ def format_read_message(
597
+ message: Mapping[str, object],
598
+ *,
599
+ index: int,
600
+ count: int,
601
+ ) -> str:
602
+ target, message_id, timestamp, message_type, sender, body = _message_header_fields(
603
+ message
604
+ )
605
+ fields = [
606
+ f"seq={cast(int, message['seq'])}",
607
+ f"msg={message_id}",
608
+ f"time={timestamp}",
609
+ f"type={message_type}",
610
+ f"replyTarget={target}",
611
+ f"mentioned={str(message['mentions_agent']).lower()}",
612
+ ]
613
+ reply_to_message_id = message["reply_to_message_id"]
614
+ if reply_to_message_id is not None:
615
+ fields.append(f"replyTo={cast(str, reply_to_message_id)}")
616
+ line = f"[{index}/{count} {' '.join(fields)}] "
617
+ if sender is not None:
618
+ line += f"{sender} "
619
+ return line + body + _attachment_suffix(message)
620
+
621
+
622
+ def format_freshness_hold(result: MessageSendFreshnessHold) -> str:
623
+ messages = [serialize_inbound(message) for message in result.messages]
624
+ referenced_messages = [
625
+ serialize_inbound(message) for message in result.referenced_messages
626
+ ]
627
+ shown = len(messages)
628
+ total = result.newer_message_total
629
+ message_label = "message" if total == 1 else "messages"
630
+ if messages:
631
+ bounds = f"{messages[0]['seq']}-{messages[-1]['seq']}"
632
+ else:
633
+ bounds = "none-none"
634
+ older_bound = (
635
+ "Older unreviewed messages are omitted."
636
+ if total > shown
637
+ else "No older unreviewed messages."
638
+ )
639
+ newer_bound = "No newer unreviewed messages."
640
+ lines = [
641
+ f"Unreviewed synced context for this target: {total} {message_label}.",
642
+ (
643
+ "Your message has been saved as a draft. Review this target's "
644
+ "synced context before sending."
645
+ ),
646
+ "",
647
+ (
648
+ f"Read window: {shown} returned, seq {bounds}, oldest to newest. "
649
+ f"{older_bound} {newer_bound}"
650
+ ),
651
+ "",
652
+ ]
653
+ if referenced_messages:
654
+ lines.append(f"Referenced messages: {len(referenced_messages)}")
655
+ lines.extend(
656
+ format_read_message(message, index=index, count=len(referenced_messages))
657
+ for index, message in enumerate(referenced_messages, start=1)
658
+ )
659
+ lines.append("Window messages:")
660
+ lines.extend(
661
+ format_read_message(message, index=index, count=shown)
662
+ for index, message in enumerate(messages, start=1)
663
+ )
664
+ lines.extend(
665
+ (
666
+ "",
667
+ f"End of window: {shown}/{total} shown.",
668
+ "",
669
+ "To update the draft, send revised content normally:",
670
+ f" bcc message send --target {json.dumps(result.target)} <<'BCCMSG'",
671
+ " revised message",
672
+ " BCCMSG",
673
+ "To send the current draft unchanged:",
674
+ f" bcc message send --send-draft --target {json.dumps(result.target)}",
675
+ "You can also choose not to send anything.",
676
+ )
677
+ )
678
+ return "\n".join(lines)
679
+
680
+
681
+ def format_cross_session_hold(target: str) -> str:
682
+ quoted_target = json.dumps(target)
683
+ return "\n".join(
684
+ (
685
+ "Your message was not sent because the target belongs to another conversation.",
686
+ "",
687
+ (
688
+ "To continue this work in the target conversation, send a "
689
+ "self-contained handoff:"
690
+ ),
691
+ f" bcc handoff send --target {quoted_target} <<'BCCMSG'",
692
+ " enough context to understand the background, goal, and next action",
693
+ " BCCMSG",
694
+ "This creates a handoff notice that wakes you in that conversation.",
695
+ "",
696
+ "You can also choose not to send anything.",
697
+ )
698
+ )
@@ -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