bazaar-compute-node 0.1.7__tar.gz → 0.1.9__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 (87) hide show
  1. bazaar_compute_node-0.1.9/PKG-INFO +106 -0
  2. bazaar_compute_node-0.1.9/README.md +93 -0
  3. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/pyproject.toml +3 -2
  4. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/pyproject.toml.orig +3 -2
  5. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/application.py +43 -16
  6. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/command.py +33 -11
  7. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/config.py +14 -10
  8. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/registry.py +24 -7
  9. bazaar_compute_node-0.1.9/src/bazaar_compute_node/app/reminder_dispatch.py +411 -0
  10. bazaar_compute_node-0.1.9/src/bazaar_compute_node/bcc.py +913 -0
  11. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/cli.py +2 -5
  12. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/approval.py +50 -3
  13. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/sqlite/__init__.py +2 -1
  14. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/sqlite/plugin.py +1 -1
  15. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +63 -0
  16. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +21 -0
  17. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +85 -0
  18. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +420 -0
  19. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/__init__.py +1 -0
  20. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/api.py +356 -0
  21. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/approval.py +411 -0
  22. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/attachments.py +442 -0
  23. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/channel.py +846 -0
  24. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/identity.py +68 -0
  25. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/markdown.py +343 -0
  26. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/outbound.py +796 -0
  27. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/telegram/plugin.py +20 -0
  28. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/wecom/channel.py +10 -5
  29. bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/wecom/plugin.py +33 -0
  30. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/channel.py +50 -5
  31. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/command.py +54 -0
  32. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/instruction.py +46 -10
  33. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/models/__init__.py +7 -0
  34. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/models/entities.py +3 -0
  35. bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/models/reminder.py +282 -0
  36. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/models/states.py +13 -0
  37. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/orchestration/__init__.py +2 -1
  38. bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/orchestration/reminder.py +250 -0
  39. bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/orchestration/reminder_command.py +362 -0
  40. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/orchestration/session.py +182 -59
  41. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/orchestration/turn.py +40 -12
  42. bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/reminder.py +342 -0
  43. bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/reminder_schedule.py +330 -0
  44. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/storage.py +88 -0
  45. bazaar_compute_node-0.1.7/PKG-INFO +0 -184
  46. bazaar_compute_node-0.1.7/README.md +0 -171
  47. bazaar_compute_node-0.1.7/src/bazaar_compute_node/bcc.py +0 -529
  48. bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -29
  49. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/LICENSE +0 -0
  50. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/__init__.py +0 -0
  51. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/__init__.py +0 -0
  52. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/attachments.py +0 -0
  53. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/transport.py +0 -0
  54. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  55. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/app/wrapper.py +0 -0
  56. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  57. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  58. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  59. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  60. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  61. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  62. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  63. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  64. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  65. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  66. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  67. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  68. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  69. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  70. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  71. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  72. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  73. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  74. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/__init__.py +0 -0
  75. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/approval.py +0 -0
  76. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/audit.py +0 -0
  77. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/client.py +0 -0
  78. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/concurrency.py +0 -0
  79. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/correlation.py +0 -0
  80. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  81. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/observability.py +0 -0
  82. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  83. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  84. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/outcomes.py +0 -0
  85. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/paths.py +0 -0
  86. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/runtime.py +0 -0
  87. {bazaar_compute_node-0.1.7 → bazaar_compute_node-0.1.9}/src/bazaar_compute_node/core/timerwheel.py +0 -0
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: bazaar-compute-node
3
+ Version: 0.1.9
4
+ Summary: Runtime-agnostic computer node daemon for agents and channels.
5
+ License-Expression: AGPL-3.0-only
6
+ License-File: LICENSE
7
+ Requires-Dist: aiohttp>=3.12.0
8
+ Requires-Dist: aiosqlite>=0.20.0
9
+ Requires-Dist: cryptography>=45.0.0
10
+ Requires-Python: >=3.14
11
+ Project-URL: Repository, https://github.com/yuchanns/bazaar-compute-node
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Bazaar Compute Node
15
+
16
+ Collaborate with your Agents in the bazaar across compute nodes, through any interface, with any harness runtime.
17
+ 在集市里与你的 Agent 们合作,透过任意界面与任意 Harness。
18
+
19
+ <p align="left">
20
+ <a href="https://github.com/yuchanns/bazaar-compute-node/actions"><img
21
+ src="https://github.com/yuchanns/bazaar-compute-node/actions/workflows/release.yml/badge.svg"
22
+ alt="Release"
23
+ /></a>
24
+ <a href="https://pypi.org/project/bazaar-compute-node"><img
25
+ src="https://img.shields.io/pypi/v/bazaar-compute-node.svg"
26
+ alt="Python Package Index"
27
+ /></a>
28
+ </p>
29
+
30
+ **Documentation is coming soon...**
31
+
32
+ ## Quick Start / 快速开始
33
+
34
+ Python 3.14+ is required:
35
+
36
+ ```sh
37
+ # Install
38
+ uv tool install bazaar-compute-node
39
+ bcn --version
40
+
41
+ # Upgrade
42
+ uv tool upgrade bazaar-compute-node
43
+
44
+ # Start daemon
45
+ export BCN_WECOM_BOT_SECRET='your-bot-secret'
46
+
47
+ bcn start --channel wecom --runtime codex \
48
+ --model gpt-5.6-luna --effort max \
49
+ --sandbox-mode workspace-write \
50
+ --network-access true \
51
+ --idle-timeout 600
52
+
53
+ # Stop daemon
54
+ bcn stop
55
+ ```
56
+
57
+ ## Features / 功能
58
+
59
+ - ✅ Done / 已完成
60
+ - 🚧 Working in progress / 开发中
61
+
62
+ ### Facilities / 通用能力
63
+
64
+ | Status / 状态 | Ability / 能力 |
65
+ | --- | --- |
66
+ | ✅ | Reminders / 定时器 |
67
+ | ✅ | Attachments / 附件 |
68
+ | 🚧 | Multi-Agents / 多 Agent |
69
+ | 🚧 | Teams / 团队协作 |
70
+
71
+ ### Channels / 渠道
72
+
73
+ | Status / 状态 | Channel / 渠道 |
74
+ | --- | --- |
75
+ | ✅ | WeCom / 企业微信 |
76
+ | ✅ | Telegram |
77
+ | 🚧 | GitHub |
78
+ | 🚧 | GitLab |
79
+
80
+ ### Harnesses
81
+
82
+ | Status / 状态 | Runtime / 运行时 |
83
+ | --- | --- |
84
+ | ✅ | Codex |
85
+ | 🚧 | Claude Code |
86
+ | 🚧 | Pi |
87
+ | 🚧 | Hermes Agent |
88
+ | 🚧 | DeepSeek Harness |
89
+
90
+ ### Platforms / 平台
91
+
92
+ | Status / 状态 | Platform / 平台 |
93
+ | --- | --- |
94
+ | ✅ | Windows |
95
+ | ✅ | macOS |
96
+ | ✅ | Linux |
97
+
98
+ ## License / 许可证
99
+
100
+ This project is licensed under the [AGPLv3](./LICENSE).
101
+ 本项目采用 [AGPLv3](./LICENSE) 许可证。
102
+
103
+ ## Credits / 致谢
104
+
105
+ This project is inspired by [raft.build](https://raft.build).
106
+ 本项目的灵感来自 [raft.build](https://raft.build)。
@@ -0,0 +1,93 @@
1
+ # Bazaar Compute Node
2
+
3
+ Collaborate with your Agents in the bazaar across compute nodes, through any interface, with any harness runtime.
4
+ 在集市里与你的 Agent 们合作,透过任意界面与任意 Harness。
5
+
6
+ <p align="left">
7
+ <a href="https://github.com/yuchanns/bazaar-compute-node/actions"><img
8
+ src="https://github.com/yuchanns/bazaar-compute-node/actions/workflows/release.yml/badge.svg"
9
+ alt="Release"
10
+ /></a>
11
+ <a href="https://pypi.org/project/bazaar-compute-node"><img
12
+ src="https://img.shields.io/pypi/v/bazaar-compute-node.svg"
13
+ alt="Python Package Index"
14
+ /></a>
15
+ </p>
16
+
17
+ **Documentation is coming soon...**
18
+
19
+ ## Quick Start / 快速开始
20
+
21
+ Python 3.14+ is required:
22
+
23
+ ```sh
24
+ # Install
25
+ uv tool install bazaar-compute-node
26
+ bcn --version
27
+
28
+ # Upgrade
29
+ uv tool upgrade bazaar-compute-node
30
+
31
+ # Start daemon
32
+ export BCN_WECOM_BOT_SECRET='your-bot-secret'
33
+
34
+ bcn start --channel wecom --runtime codex \
35
+ --model gpt-5.6-luna --effort max \
36
+ --sandbox-mode workspace-write \
37
+ --network-access true \
38
+ --idle-timeout 600
39
+
40
+ # Stop daemon
41
+ bcn stop
42
+ ```
43
+
44
+ ## Features / 功能
45
+
46
+ - ✅ Done / 已完成
47
+ - 🚧 Working in progress / 开发中
48
+
49
+ ### Facilities / 通用能力
50
+
51
+ | Status / 状态 | Ability / 能力 |
52
+ | --- | --- |
53
+ | ✅ | Reminders / 定时器 |
54
+ | ✅ | Attachments / 附件 |
55
+ | 🚧 | Multi-Agents / 多 Agent |
56
+ | 🚧 | Teams / 团队协作 |
57
+
58
+ ### Channels / 渠道
59
+
60
+ | Status / 状态 | Channel / 渠道 |
61
+ | --- | --- |
62
+ | ✅ | WeCom / 企业微信 |
63
+ | ✅ | Telegram |
64
+ | 🚧 | GitHub |
65
+ | 🚧 | GitLab |
66
+
67
+ ### Harnesses
68
+
69
+ | Status / 状态 | Runtime / 运行时 |
70
+ | --- | --- |
71
+ | ✅ | Codex |
72
+ | 🚧 | Claude Code |
73
+ | 🚧 | Pi |
74
+ | 🚧 | Hermes Agent |
75
+ | 🚧 | DeepSeek Harness |
76
+
77
+ ### Platforms / 平台
78
+
79
+ | Status / 状态 | Platform / 平台 |
80
+ | --- | --- |
81
+ | ✅ | Windows |
82
+ | ✅ | macOS |
83
+ | ✅ | Linux |
84
+
85
+ ## License / 许可证
86
+
87
+ This project is licensed under the [AGPLv3](./LICENSE).
88
+ 本项目采用 [AGPLv3](./LICENSE) 许可证。
89
+
90
+ ## Credits / 致谢
91
+
92
+ This project is inspired by [raft.build](https://raft.build).
93
+ 本项目的灵感来自 [raft.build](https://raft.build)。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -22,7 +22,8 @@ bcn = "bazaar_compute_node.cli:main"
22
22
  codex = "bazaar_compute_node.contrib.codex.plugin:create_runtime"
23
23
 
24
24
  [project.entry-points."bazaar_compute_node.channels"]
25
- wecom = "bazaar_compute_node.contrib.wecom.plugin:create_channel"
25
+ wecom = "bazaar_compute_node.contrib.wecom.plugin:builder"
26
+ telegram = "bazaar_compute_node.contrib.telegram.plugin:builder"
26
27
 
27
28
  [project.entry-points."bazaar_compute_node.storages"]
28
29
  sqlite = "bazaar_compute_node.contrib.sqlite.plugin:create_storage"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -22,7 +22,8 @@ bcn = "bazaar_compute_node.cli:main"
22
22
  codex = "bazaar_compute_node.contrib.codex.plugin:create_runtime"
23
23
 
24
24
  [project.entry-points."bazaar_compute_node.channels"]
25
- wecom = "bazaar_compute_node.contrib.wecom.plugin:create_channel"
25
+ wecom = "bazaar_compute_node.contrib.wecom.plugin:builder"
26
+ telegram = "bazaar_compute_node.contrib.telegram.plugin:builder"
26
27
 
27
28
  [project.entry-points."bazaar_compute_node.storages"]
28
29
  sqlite = "bazaar_compute_node.contrib.sqlite.plugin:create_storage"
@@ -11,20 +11,20 @@ from collections.abc import Mapping, Sequence
11
11
  from pathlib import Path
12
12
 
13
13
  from ..core.channel import ChannelContext, IChannel
14
+ from ..core.concurrency import SessionLockRegistry
14
15
  from ..core.lifecycle import TimeoutBudget
15
16
  from ..core.models import RuntimeSession
16
17
  from ..core.observability import IAudit
17
- from ..core.orchestration import SessionOrchestrator
18
+ from ..core.orchestration import ReminderScheduler, SessionOrchestrator
19
+ from ..core.orchestration.reminder_command import ReminderCommandService
18
20
  from ..core.paths import resolve_data_dir, resolve_workspace_dir
19
21
  from ..core.runtime import IRuntime, RuntimeCommandContext, RuntimeSandboxMode
20
22
  from ..core.storage import IStorage, NodeIdentity
21
23
  from ..core.timerwheel import TimerWheel
22
24
  from .attachments import AttachmentMaterializer
23
- from .command import (
24
- CommandDispatcher,
25
- CommandDispatchError,
26
- )
25
+ from .command import CommandDispatchError
27
26
  from .registry import AdapterFactories
27
+ from .reminder_dispatch import CommandDispatcher
28
28
  from .transport import LocalCommandServer
29
29
  from .wrapper import install_bcc_wrapper, remove_bcc_wrapper
30
30
 
@@ -85,13 +85,14 @@ class NodeApplication:
85
85
  self._wrapper_path: Path | None = None
86
86
  self._identity: NodeIdentity | None = None
87
87
  self._session_capabilities: dict[str, tuple[str, str]] = {}
88
+ self._concurrency = SessionLockRegistry()
88
89
  self._started = False
89
90
  self._stopped = asyncio.Event()
90
91
  self._runtime_environment_include = tuple(runtime_environment_include)
91
92
  self._attachment_materializer = AttachmentMaterializer(
92
93
  self._workspace_path, self._referenced_attachment_paths
93
94
  )
94
- self.channel: IChannel = factories.channel(
95
+ self.channel: IChannel = factories.channel.build(
95
96
  ChannelContext(
96
97
  attachments=self._attachment_materializer,
97
98
  options=dict(channel_options or {}),
@@ -136,14 +137,27 @@ class NodeApplication:
136
137
  timer_wheel=self.timer_wheel,
137
138
  runtime_idle_timeout_ms=runtime_idle_timeout_ms,
138
139
  workspace=self._workspace_path,
140
+ concurrency=self._concurrency,
139
141
  on_node_initialized=self._ensure_workspace,
140
142
  )
143
+ self.reminder_scheduler = ReminderScheduler(
144
+ storage=self.storage,
145
+ timer_wheel=self.timer_wheel,
146
+ concurrency=self._concurrency,
147
+ publish_wake=self.orchestrator.publish_reminder_wake,
148
+ )
149
+ self.reminder_service = ReminderCommandService(
150
+ storage=self.storage,
151
+ concurrency=self._concurrency,
152
+ poke=self.reminder_scheduler.poke,
153
+ )
141
154
  self.command_service = self.orchestrator.command_service
142
155
  control_handler = None
143
156
  if factories.control is not None:
144
157
  control_handler = factories.control(self._adapter_context())
145
158
  self.command_dispatcher = CommandDispatcher(
146
159
  self.command_service,
160
+ reminder_service=self.reminder_service,
147
161
  timeout_budget=self.timeout_budget,
148
162
  control_handler=self._handle_control,
149
163
  session_binding_validator=self._validate_session_binding,
@@ -173,30 +187,38 @@ class NodeApplication:
173
187
  await self.orchestrator.start(
174
188
  timeout=self.timeout_budget.startup_seconds,
175
189
  )
190
+ self._started = True
191
+ await self.reminder_scheduler.start(
192
+ timeout=self.timeout_budget.startup_seconds,
193
+ )
176
194
  except BaseException:
177
195
  try:
178
196
  await self.stop()
179
197
  finally:
180
198
  await self.timer_wheel.close()
181
199
  raise
182
- self._started = True
183
200
  self._stopped.clear()
184
201
  self.command_dispatcher.start_accepting()
185
202
 
186
203
  async def stop(self) -> None:
187
204
  if not self._started:
188
205
  try:
189
- await self.command_dispatcher.drain(
206
+ await self.reminder_scheduler.stop(
190
207
  timeout=self.timeout_budget.shutdown_seconds,
191
208
  )
192
209
  finally:
193
210
  try:
194
- await self.command_server.stop()
211
+ await self.command_dispatcher.drain(
212
+ timeout=self.timeout_budget.shutdown_seconds,
213
+ )
195
214
  finally:
196
215
  try:
197
- await self._cleanup_bcc_wrapper()
216
+ await self.command_server.stop()
198
217
  finally:
199
- await self.timer_wheel.close()
218
+ try:
219
+ await self._cleanup_bcc_wrapper()
220
+ finally:
221
+ await self.timer_wheel.close()
200
222
  return
201
223
  self._started = False
202
224
  self.command_dispatcher.stop_accepting()
@@ -206,20 +228,25 @@ class NodeApplication:
206
228
  )
207
229
  finally:
208
230
  try:
209
- await self.orchestrator.stop(
231
+ await self.reminder_scheduler.stop(
210
232
  timeout=self.timeout_budget.shutdown_seconds,
211
233
  )
212
234
  finally:
213
235
  try:
214
- await self.command_server.stop()
236
+ await self.orchestrator.stop(
237
+ timeout=self.timeout_budget.shutdown_seconds,
238
+ )
215
239
  finally:
216
240
  try:
217
- await self._cleanup_bcc_wrapper()
241
+ await self.command_server.stop()
218
242
  finally:
219
243
  try:
220
- await self.timer_wheel.close()
244
+ await self._cleanup_bcc_wrapper()
221
245
  finally:
222
- self._stopped.set()
246
+ try:
247
+ await self.timer_wheel.close()
248
+ finally:
249
+ self._stopped.set()
223
250
 
224
251
  async def _cleanup_bcc_wrapper(self) -> None:
225
252
  wrapper_path = self._wrapper_path
@@ -100,7 +100,7 @@ SessionBindingValidator = Callable[[str, Mapping[str, object]], Awaitable[None]]
100
100
 
101
101
 
102
102
  class CommandDispatcher:
103
- """Translate local JSON requests into core command results."""
103
+ """Translate resource-scoped local JSON requests into core command results."""
104
104
 
105
105
  def __init__(
106
106
  self,
@@ -222,20 +222,40 @@ class CommandDispatcher:
222
222
  async def _dispatch_command(
223
223
  self, request: Mapping[str, object]
224
224
  ) -> Mapping[str, object]:
225
- session_id = request.get("session_id")
226
- if not isinstance(session_id, str) or not session_id:
225
+ resource = request.get("resource")
226
+ if not isinstance(resource, str) or not resource:
227
227
  raise CommandDispatchError(
228
- "SESSION_REQUIRED", "session_id must be a non-empty string"
228
+ "RESOURCE_REQUIRED", "resource must be a non-empty string"
229
229
  )
230
230
  command = request.get("command")
231
231
  if not isinstance(command, str) or not command:
232
232
  raise CommandDispatchError(
233
233
  "COMMAND_REQUIRED", "command must be a non-empty string"
234
234
  )
235
+ if resource == "message":
236
+ if command not in {"check", "read", "send"}:
237
+ raise CommandDispatchError(
238
+ "UNKNOWN_COMMAND", f"unsupported message command: {command}"
239
+ )
240
+ elif resource == "thread":
241
+ if command != "unfollow":
242
+ raise CommandDispatchError(
243
+ "UNKNOWN_COMMAND", f"unsupported thread command: {command}"
244
+ )
245
+ else:
246
+ raise CommandDispatchError(
247
+ "UNKNOWN_RESOURCE", f"unsupported command resource: {resource}"
248
+ )
249
+
250
+ session_id = request.get("session_id")
251
+ if not isinstance(session_id, str) or not session_id:
252
+ raise CommandDispatchError(
253
+ "SESSION_REQUIRED", "session_id must be a non-empty string"
254
+ )
235
255
  if self._session_binding_validator is not None:
236
256
  await self._session_binding_validator(session_id, request)
237
257
 
238
- if command == "check":
258
+ if resource == "message" and command == "check":
239
259
  result = await self._service.check(session_id)
240
260
  return {
241
261
  "ok": True,
@@ -252,7 +272,7 @@ class CommandDispatcher:
252
272
  },
253
273
  }
254
274
 
255
- if command == "read":
275
+ if resource == "message" and command == "read":
256
276
  target = request.get("target")
257
277
  if not isinstance(target, str) or not target:
258
278
  raise CommandDispatchError(
@@ -290,7 +310,7 @@ class CommandDispatcher:
290
310
  },
291
311
  }
292
312
 
293
- if command == "send":
313
+ if resource == "message" and command == "send":
294
314
  target = request.get("target")
295
315
  body = request.get("body")
296
316
  command_id = request.get("command_id")
@@ -343,7 +363,7 @@ class CommandDispatcher:
343
363
  "result": {"outbound": serialize_outbound(result)},
344
364
  }
345
365
 
346
- if command == "unfollow":
366
+ if resource == "thread" and command == "unfollow":
347
367
  target = request.get("target")
348
368
  if not isinstance(target, str) or not target:
349
369
  raise CommandDispatchError(
@@ -352,10 +372,12 @@ class CommandDispatcher:
352
372
  changed = await self._service.unfollow(session_id, target=target)
353
373
  return {"ok": True, "result": {"changed": changed}}
354
374
 
355
- raise CommandDispatchError("UNKNOWN_COMMAND", f"unsupported command: {command}")
375
+ raise AssertionError("validated command route has no handler")
356
376
 
357
377
 
358
378
  def format_message_time(timestamp_ms: int) -> str:
359
- return datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC).strftime(
360
- "%Y-%m-%d %H:%M:%S"
379
+ return (
380
+ datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC)
381
+ .astimezone()
382
+ .strftime("%Y-%m-%d %H:%M:%S")
361
383
  )
@@ -3,8 +3,10 @@ from __future__ import annotations
3
3
  import math
4
4
  import os
5
5
  import tomllib
6
- from dataclasses import dataclass
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass, field
7
8
  from pathlib import Path
9
+ from types import MappingProxyType
8
10
 
9
11
  from ..core.paths import resolve_data_dir
10
12
  from ..core.runtime import RuntimeSandboxMode
@@ -28,8 +30,9 @@ class NodeConfiguration:
28
30
  network_access: bool = True
29
31
  runtime_idle_timeout_seconds: float = 0
30
32
  runtime_env_include: tuple[str, ...] = ()
31
- wecom_bot_id: str | None = None
32
- wecom_websocket_url: str | None = None
33
+ channel_options: Mapping[str, Mapping[str, object]] = field(
34
+ default_factory=lambda: MappingProxyType({})
35
+ )
33
36
 
34
37
 
35
38
  class ConfigurationError(ValueError):
@@ -67,9 +70,13 @@ def load_node_configuration(path: Path | None = None) -> NodeConfiguration:
67
70
  channel = payload.get("channel", {})
68
71
  if not isinstance(channel, dict):
69
72
  raise ConfigurationError("[channel] must be a TOML table")
70
- wecom = channel.get("wecom", {})
71
- if not isinstance(wecom, dict):
72
- raise ConfigurationError("[channel.wecom] must be a TOML table")
73
+ channel_options: dict[str, Mapping[str, object]] = {}
74
+ for channel_name, options in channel.items():
75
+ if not channel_name:
76
+ raise ConfigurationError("channel name must be non-empty text")
77
+ if not isinstance(options, dict):
78
+ raise ConfigurationError(f"[channel.{channel_name}] must be a TOML table")
79
+ channel_options[channel_name] = MappingProxyType(dict(options))
73
80
  sandbox_mode = runtime.get("sandbox_mode", RuntimeSandboxMode.WORKSPACE_WRITE.value)
74
81
  if not isinstance(sandbox_mode, str):
75
82
  raise ConfigurationError("runtime.sandbox_mode must be text")
@@ -105,10 +112,7 @@ def load_node_configuration(path: Path | None = None) -> NodeConfiguration:
105
112
  runtime_env_include=_text_list(
106
113
  runtime_env.get("include", []), "runtime.env.include"
107
114
  ),
108
- wecom_bot_id=_optional_text(wecom.get("bot_id"), "channel.wecom.bot_id"),
109
- wecom_websocket_url=_optional_text(
110
- wecom.get("websocket_url"), "channel.wecom.websocket_url"
111
- ),
115
+ channel_options=MappingProxyType(channel_options),
112
116
  )
113
117
 
114
118
 
@@ -6,7 +6,7 @@ from functools import partial
6
6
  from importlib.metadata import EntryPoint, entry_points
7
7
  from typing import Any, cast
8
8
 
9
- from ..core.channel import ChannelContext, IChannel
9
+ from ..core.channel import IChannelBuilder
10
10
  from ..core.observability import IAudit
11
11
  from ..core.runtime import IRuntime, RuntimeCommandContext
12
12
  from ..core.storage import IStorage
@@ -18,7 +18,6 @@ STORAGE_ENTRY_POINT_GROUP = "bazaar_compute_node.storages"
18
18
  AUDIT_ENTRY_POINT_GROUP = "bazaar_compute_node.audits"
19
19
  CONTROL_ENTRY_POINT_GROUP = "bazaar_compute_node.controls"
20
20
 
21
- ChannelFactory = Callable[[ChannelContext], IChannel]
22
21
  RuntimeFactory = Callable[[RuntimeCommandContext], IRuntime]
23
22
  StorageFactory = Callable[[], IStorage]
24
23
  AuditFactory = Callable[[], IAudit]
@@ -26,7 +25,7 @@ AuditFactory = Callable[[], IAudit]
26
25
 
27
26
  @dataclass(frozen=True, slots=True)
28
27
  class AdapterFactories:
29
- channel: ChannelFactory
28
+ channel: IChannelBuilder
30
29
  runtime: RuntimeFactory
31
30
  storage: StorageFactory
32
31
  audit: AuditFactory
@@ -60,10 +59,7 @@ class AdapterRegistry:
60
59
  if storage_options:
61
60
  storage_factory = partial(storage_factory, dict(storage_options))
62
61
  return AdapterFactories(
63
- channel=cast(
64
- ChannelFactory,
65
- self._load(CHANNEL_ENTRY_POINT_GROUP, channel),
66
- ),
62
+ channel=self._load_channel_builder(channel),
67
63
  runtime=cast(
68
64
  RuntimeFactory,
69
65
  self._load(RUNTIME_ENTRY_POINT_GROUP, runtime),
@@ -79,6 +75,27 @@ class AdapterRegistry:
79
75
  ),
80
76
  )
81
77
 
78
+ def _load_channel_builder(self, name: str) -> IChannelBuilder:
79
+ entry_point = self._find(CHANNEL_ENTRY_POINT_GROUP, name)
80
+ if entry_point is None:
81
+ raise ProviderLoadError(
82
+ f"provider '{name}' is not installed for entry point group "
83
+ f"'{CHANNEL_ENTRY_POINT_GROUP}'"
84
+ )
85
+ try:
86
+ builder = entry_point.load()
87
+ except Exception as error:
88
+ raise ProviderLoadError(
89
+ f"failed to load provider '{name}' from "
90
+ f"'{CHANNEL_ENTRY_POINT_GROUP}': {error}"
91
+ ) from error
92
+ if not callable(getattr(builder, "build", None)):
93
+ raise ProviderLoadError(
94
+ f"provider '{name}' from '{CHANNEL_ENTRY_POINT_GROUP}' "
95
+ "does not provide a callable build method"
96
+ )
97
+ return cast(IChannelBuilder, builder)
98
+
82
99
  def _load(self, group: str, name: str) -> Any:
83
100
  entry_point = self._find(group, name)
84
101
  if entry_point is None: