bazaar-compute-node 0.1.6__tar.gz → 0.1.7__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 (64) hide show
  1. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/PKG-INFO +7 -3
  2. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/README.md +6 -2
  3. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/pyproject.toml +2 -2
  4. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/pyproject.toml.orig +2 -2
  5. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/application.py +54 -20
  6. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/config.py +10 -0
  7. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/cli.py +2 -0
  8. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/__init__.py +26 -14
  9. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/approval.py +25 -21
  10. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/client.py +127 -26
  11. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/events.py +9 -9
  12. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/plugin.py +2 -2
  13. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/process.py +23 -0
  14. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/protocol.py +2 -2
  15. {bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/codex_app_server → bazaar_compute_node-0.1.7/src/bazaar_compute_node/contrib/codex}/runtime.py +159 -36
  16. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -40
  17. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/sqlite/migrations.py +14 -0
  18. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -90
  19. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/orchestration/services.py +8 -0
  20. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/orchestration/session.py +721 -170
  21. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/orchestration/turn.py +75 -13
  22. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/runtime.py +36 -7
  23. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/storage.py +0 -13
  24. bazaar_compute_node-0.1.7/src/bazaar_compute_node/core/timerwheel.py +316 -0
  25. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/LICENSE +0 -0
  26. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/__init__.py +0 -0
  27. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/__init__.py +0 -0
  28. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/attachments.py +0 -0
  29. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/command.py +0 -0
  30. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/registry.py +0 -0
  31. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/transport.py +0 -0
  32. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  33. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/app/wrapper.py +0 -0
  34. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/bcc.py +0 -0
  35. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  36. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  37. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  38. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  39. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  40. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  41. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  42. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  43. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  44. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  45. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  46. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  47. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/__init__.py +0 -0
  48. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/approval.py +0 -0
  49. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/audit.py +0 -0
  50. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/channel.py +0 -0
  51. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/client.py +0 -0
  52. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/command.py +0 -0
  53. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/concurrency.py +0 -0
  54. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/correlation.py +0 -0
  55. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/instruction.py +0 -0
  56. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  57. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  58. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/models/entities.py +0 -0
  59. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/models/states.py +0 -0
  60. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/observability.py +0 -0
  61. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  62. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  63. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/outcomes.py +0 -0
  64. {bazaar_compute_node-0.1.6 → bazaar_compute_node-0.1.7}/src/bazaar_compute_node/core/paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.6
3
+ Version: 0.1.7
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
@@ -24,6 +24,7 @@ Harness 工具。
24
24
  - **自由组合**:根据使用场景分别选择 Channel 与 Runtime,并可独立替换和扩展。
25
25
  - **持续会话**:每个用户和对话拥有独立上下文,节点重启后仍可继续之前的任务。
26
26
  - **真实工作区**:Runtime 可以在独立工作区中分析、创建和修改文件,而不局限于文本回答。
27
+ - **双向附件**:Channel 收到的媒体会进入会话工作区,Runtime 也可以通过 Channel 交付工作区中的文件。
27
28
  - **可靠交付**:自动处理长结果的分批发送并记录交付状态,避免任务结果静默丢失。
28
29
  - **安全边界**:复用所选 Harness 的权限机制,按任务需要控制文件与网络访问。
29
30
  - **跨平台运行**:支持 Linux、macOS 和 Windows,并以后台进程长期运行。
@@ -57,7 +58,6 @@ bcn 将沿着可组合的 Channel、Runtime 与节点通用能力继续扩展。
57
58
 
58
59
  ### 节点能力
59
60
 
60
- - **附件发送**:让 Agent 可以通过当前 Channel 交付生成的文件;
61
61
  - **定时任务**:创建一次性或周期性任务,由节点按计划自动执行并返回结果。
62
62
 
63
63
  ## 安装
@@ -91,7 +91,8 @@ Channel 即可。
91
91
  ### WeCom
92
92
 
93
93
  WeCom Channel 对接企业微信智能机器人,支持单聊、群聊、文本与媒体输入,以及 Markdown
94
- 长消息分批返回。群聊中需要 @机器人,确保消息能够进入节点。
94
+ 长消息分批和附件返回。收到的媒体会以工作区相对路径提供给 Runtime;Runtime 可以把工作区内的
95
+ 普通文件作为附件发送,附件路径不能包含符号链接。群聊中需要 @机器人,确保消息能够进入节点。
95
96
 
96
97
  在 `~/.bcn/config.toml` 中配置 Bot ID:
97
98
 
@@ -128,9 +129,12 @@ Runtime 即可。
128
129
  [runtime]
129
130
  sandbox_mode = "workspace-write"
130
131
  network_access = true
132
+ idle_timeout = 0
131
133
  ```
132
134
 
133
135
  如需覆盖 Codex 的默认模型或推理强度,可以在 `[runtime]` 中增加 `model` 和 `effort`。
136
+ `idle_timeout` 以秒为单位并支持小数;默认值 `0` 与所有 `<= 0` 的值表示 runtime session
137
+ 常驻。设置为正值后,每条新收到的消息都会刷新空闲回收期限。
134
138
  除非你明确需要使用运行用户已有的主机权限,否则不要将 `sandbox_mode` 改为
135
139
  `danger-full-access`。
136
140
 
@@ -11,6 +11,7 @@ Harness 工具。
11
11
  - **自由组合**:根据使用场景分别选择 Channel 与 Runtime,并可独立替换和扩展。
12
12
  - **持续会话**:每个用户和对话拥有独立上下文,节点重启后仍可继续之前的任务。
13
13
  - **真实工作区**:Runtime 可以在独立工作区中分析、创建和修改文件,而不局限于文本回答。
14
+ - **双向附件**:Channel 收到的媒体会进入会话工作区,Runtime 也可以通过 Channel 交付工作区中的文件。
14
15
  - **可靠交付**:自动处理长结果的分批发送并记录交付状态,避免任务结果静默丢失。
15
16
  - **安全边界**:复用所选 Harness 的权限机制,按任务需要控制文件与网络访问。
16
17
  - **跨平台运行**:支持 Linux、macOS 和 Windows,并以后台进程长期运行。
@@ -44,7 +45,6 @@ bcn 将沿着可组合的 Channel、Runtime 与节点通用能力继续扩展。
44
45
 
45
46
  ### 节点能力
46
47
 
47
- - **附件发送**:让 Agent 可以通过当前 Channel 交付生成的文件;
48
48
  - **定时任务**:创建一次性或周期性任务,由节点按计划自动执行并返回结果。
49
49
 
50
50
  ## 安装
@@ -78,7 +78,8 @@ Channel 即可。
78
78
  ### WeCom
79
79
 
80
80
  WeCom Channel 对接企业微信智能机器人,支持单聊、群聊、文本与媒体输入,以及 Markdown
81
- 长消息分批返回。群聊中需要 @机器人,确保消息能够进入节点。
81
+ 长消息分批和附件返回。收到的媒体会以工作区相对路径提供给 Runtime;Runtime 可以把工作区内的
82
+ 普通文件作为附件发送,附件路径不能包含符号链接。群聊中需要 @机器人,确保消息能够进入节点。
82
83
 
83
84
  在 `~/.bcn/config.toml` 中配置 Bot ID:
84
85
 
@@ -115,9 +116,12 @@ Runtime 即可。
115
116
  [runtime]
116
117
  sandbox_mode = "workspace-write"
117
118
  network_access = true
119
+ idle_timeout = 0
118
120
  ```
119
121
 
120
122
  如需覆盖 Codex 的默认模型或推理强度,可以在 `[runtime]` 中增加 `model` 和 `effort`。
123
+ `idle_timeout` 以秒为单位并支持小数;默认值 `0` 与所有 `<= 0` 的值表示 runtime session
124
+ 常驻。设置为正值后,每条新收到的消息都会刷新空闲回收期限。
121
125
  除非你明确需要使用运行用户已有的主机权限,否则不要将 `sandbox_mode` 改为
122
126
  `danger-full-access`。
123
127
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -19,7 +19,7 @@ Repository = "https://github.com/yuchanns/bazaar-compute-node"
19
19
  bcn = "bazaar_compute_node.cli:main"
20
20
 
21
21
  [project.entry-points."bazaar_compute_node.runtimes"]
22
- codex = "bazaar_compute_node.contrib.codex_app_server.plugin:create_runtime"
22
+ codex = "bazaar_compute_node.contrib.codex.plugin:create_runtime"
23
23
 
24
24
  [project.entry-points."bazaar_compute_node.channels"]
25
25
  wecom = "bazaar_compute_node.contrib.wecom.plugin:create_channel"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -19,7 +19,7 @@ Repository = "https://github.com/yuchanns/bazaar-compute-node"
19
19
  bcn = "bazaar_compute_node.cli:main"
20
20
 
21
21
  [project.entry-points."bazaar_compute_node.runtimes"]
22
- codex = "bazaar_compute_node.contrib.codex_app_server.plugin:create_runtime"
22
+ codex = "bazaar_compute_node.contrib.codex.plugin:create_runtime"
23
23
 
24
24
  [project.entry-points."bazaar_compute_node.channels"]
25
25
  wecom = "bazaar_compute_node.contrib.wecom.plugin:create_channel"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import hmac
5
+ import math
5
6
  import os
6
7
  import re
7
8
  import secrets
@@ -17,6 +18,7 @@ from ..core.orchestration import SessionOrchestrator
17
18
  from ..core.paths import resolve_data_dir, resolve_workspace_dir
18
19
  from ..core.runtime import IRuntime, RuntimeCommandContext, RuntimeSandboxMode
19
20
  from ..core.storage import IStorage, NodeIdentity
21
+ from ..core.timerwheel import TimerWheel
20
22
  from .attachments import AttachmentMaterializer
21
23
  from .command import (
22
24
  CommandDispatcher,
@@ -64,6 +66,7 @@ class NodeApplication:
64
66
  runtime_options: Mapping[str, str] | None = None,
65
67
  runtime_sandbox_mode: RuntimeSandboxMode = RuntimeSandboxMode.WORKSPACE_WRITE,
66
68
  runtime_network_access: bool = True,
69
+ runtime_idle_timeout_seconds: float = 0,
67
70
  channel_options: Mapping[str, object] | None = None,
68
71
  runtime_environment_include: Sequence[str] = (),
69
72
  timeout_budget: TimeoutBudget | None = None,
@@ -81,8 +84,7 @@ class NodeApplication:
81
84
  self.command_log: list[CommandRecord] = []
82
85
  self._wrapper_path: Path | None = None
83
86
  self._identity: NodeIdentity | None = None
84
- self._session_capabilities: dict[str, str] = {}
85
- self._runtime_session_ids: dict[str, str] = {}
87
+ self._session_capabilities: dict[str, tuple[str, str]] = {}
86
88
  self._started = False
87
89
  self._stopped = asyncio.Event()
88
90
  self._runtime_environment_include = tuple(runtime_environment_include)
@@ -106,6 +108,23 @@ class NodeApplication:
106
108
  startup_timeout_seconds=self.timeout_budget.startup_seconds,
107
109
  )
108
110
  self.runtime: IRuntime = factories.runtime(self._runtime_context)
111
+ if (
112
+ isinstance(runtime_idle_timeout_seconds, bool)
113
+ or not isinstance(runtime_idle_timeout_seconds, int | float)
114
+ or not math.isfinite(runtime_idle_timeout_seconds)
115
+ ):
116
+ raise ValueError("runtime_idle_timeout_seconds must be a finite number")
117
+ self.timer_wheel = TimerWheel()
118
+ if (
119
+ runtime_idle_timeout_seconds > 0
120
+ and runtime_idle_timeout_seconds * 1_000 > self.timer_wheel.maximum_delay_ms
121
+ ):
122
+ raise ValueError("runtime_idle_timeout_seconds exceeds the timer horizon")
123
+ runtime_idle_timeout_ms = (
124
+ math.ceil(runtime_idle_timeout_seconds * 1_000)
125
+ if runtime_idle_timeout_seconds > 0
126
+ else 0
127
+ )
109
128
  self.orchestrator = SessionOrchestrator(
110
129
  node_id=node_id,
111
130
  workspace_id=workspace_id,
@@ -114,6 +133,8 @@ class NodeApplication:
114
133
  storage=self.storage,
115
134
  audit=self.audit,
116
135
  timeout_budget=self.timeout_budget,
136
+ timer_wheel=self.timer_wheel,
137
+ runtime_idle_timeout_ms=runtime_idle_timeout_ms,
117
138
  workspace=self._workspace_path,
118
139
  on_node_initialized=self._ensure_workspace,
119
140
  )
@@ -147,12 +168,16 @@ class NodeApplication:
147
168
  install_bcc_wrapper, self.data_dir / "bin"
148
169
  )
149
170
  try:
171
+ await self.timer_wheel.start()
150
172
  await self.command_server.start()
151
173
  await self.orchestrator.start(
152
174
  timeout=self.timeout_budget.startup_seconds,
153
175
  )
154
176
  except BaseException:
155
- await self.stop()
177
+ try:
178
+ await self.stop()
179
+ finally:
180
+ await self.timer_wheel.close()
156
181
  raise
157
182
  self._started = True
158
183
  self._stopped.clear()
@@ -168,7 +193,10 @@ class NodeApplication:
168
193
  try:
169
194
  await self.command_server.stop()
170
195
  finally:
171
- await self._cleanup_bcc_wrapper()
196
+ try:
197
+ await self._cleanup_bcc_wrapper()
198
+ finally:
199
+ await self.timer_wheel.close()
172
200
  return
173
201
  self._started = False
174
202
  self.command_dispatcher.stop_accepting()
@@ -188,7 +216,10 @@ class NodeApplication:
188
216
  try:
189
217
  await self._cleanup_bcc_wrapper()
190
218
  finally:
191
- self._stopped.set()
219
+ try:
220
+ await self.timer_wheel.close()
221
+ finally:
222
+ self._stopped.set()
192
223
 
193
224
  async def _cleanup_bcc_wrapper(self) -> None:
194
225
  wrapper_path = self._wrapper_path
@@ -278,18 +309,19 @@ class NodeApplication:
278
309
  "SESSION_NOT_FOUND",
279
310
  f"unknown bcn session: {session_id}",
280
311
  )
281
- runtime_session = await transaction.find_runtime_session(session_id)
282
- if runtime_session is None or runtime_session_id != (runtime_session.id):
312
+ runtime_session = self.orchestrator.runtime_session(session_id)
313
+ if runtime_session is None or runtime_session_id != runtime_session.id:
283
314
  raise CommandDispatchError(
284
315
  "SESSION_BINDING_FAILED",
285
316
  "runtime session binding is invalid",
286
317
  )
287
- expected_capability = self._session_capabilities.get(session_id)
318
+ capability_binding = self._session_capabilities.get(session_id)
288
319
  if (
289
- expected_capability is None
320
+ capability_binding is None
321
+ or capability_binding[0] != runtime_session.id
290
322
  or not isinstance(session_capability, str)
291
323
  or not hmac.compare_digest(
292
- session_capability.encode(), expected_capability.encode()
324
+ session_capability.encode(), capability_binding[1].encode()
293
325
  )
294
326
  ):
295
327
  raise CommandDispatchError(
@@ -298,7 +330,9 @@ class NodeApplication:
298
330
  )
299
331
 
300
332
  def _runtime_environment(self, session: RuntimeSession) -> Mapping[str, str]:
301
- self._runtime_session_ids[session.bcn_session_id] = session.id
333
+ runtime_session = self.orchestrator.runtime_session(session.bcn_session_id)
334
+ if runtime_session is None or runtime_session.id != session.id:
335
+ raise RuntimeError("runtime session is not the current live binding")
302
336
  return self._build_command_environment(
303
337
  session.bcn_session_id,
304
338
  session.id,
@@ -316,10 +350,11 @@ class NodeApplication:
316
350
  wrapper_path = self._wrapper_path
317
351
  if wrapper_path is None:
318
352
  raise RuntimeError("bcc wrapper is not installed")
319
- session_capability = self._session_capabilities.setdefault(
320
- session_id,
321
- secrets.token_urlsafe(32),
322
- )
353
+ capability_binding = self._session_capabilities.get(session_id)
354
+ if capability_binding is None or capability_binding[0] != runtime_session_id:
355
+ capability_binding = (runtime_session_id, secrets.token_urlsafe(32))
356
+ self._session_capabilities[session_id] = capability_binding
357
+ session_capability = capability_binding[1]
323
358
  wrapper_directory = str(wrapper_path.parent)
324
359
  allowed = set(_PLATFORM_ENVIRONMENT)
325
360
  for name in self.runtime.environment_variable_names():
@@ -368,13 +403,12 @@ class NodeApplication:
368
403
  if wrapper_path is None:
369
404
  raise RuntimeError("bcc wrapper is not installed")
370
405
  self.command_log.append((session_id, tuple(arguments)))
371
- runtime_session_id = self._runtime_session_ids.get(
372
- session_id,
373
- f"runtime-{session_id}",
374
- )
406
+ runtime_session = self.orchestrator.runtime_session(session_id)
407
+ if runtime_session is None:
408
+ raise RuntimeError("runtime session is not live")
375
409
  environment = self._build_command_environment(
376
410
  session_id,
377
- runtime_session_id,
411
+ runtime_session.id,
378
412
  )
379
413
  process = await asyncio.create_subprocess_exec(
380
414
  str(wrapper_path),
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import math
3
4
  import os
4
5
  import tomllib
5
6
  from dataclasses import dataclass
@@ -25,6 +26,7 @@ class NodeConfiguration:
25
26
  effort: str | None = None
26
27
  sandbox_mode: RuntimeSandboxMode = RuntimeSandboxMode.WORKSPACE_WRITE
27
28
  network_access: bool = True
29
+ runtime_idle_timeout_seconds: float = 0
28
30
  runtime_env_include: tuple[str, ...] = ()
29
31
  wecom_bot_id: str | None = None
30
32
  wecom_websocket_url: str | None = None
@@ -81,6 +83,13 @@ def load_node_configuration(path: Path | None = None) -> NodeConfiguration:
81
83
  network_access = runtime.get("network_access", True)
82
84
  if not isinstance(network_access, bool):
83
85
  raise ConfigurationError("runtime.network_access must be a boolean")
86
+ idle_timeout = runtime.get("idle_timeout", 0)
87
+ if (
88
+ isinstance(idle_timeout, bool)
89
+ or not isinstance(idle_timeout, int | float)
90
+ or not math.isfinite(idle_timeout)
91
+ ):
92
+ raise ConfigurationError("runtime.idle_timeout must be a finite number")
84
93
  return NodeConfiguration(
85
94
  channel=_optional_text(node.get("channel"), "node.channel"),
86
95
  runtime=_optional_text(node.get("runtime"), "node.runtime"),
@@ -92,6 +101,7 @@ def load_node_configuration(path: Path | None = None) -> NodeConfiguration:
92
101
  effort=_optional_text(runtime.get("effort"), "runtime.effort"),
93
102
  sandbox_mode=parsed_sandbox_mode,
94
103
  network_access=network_access,
104
+ runtime_idle_timeout_seconds=float(idle_timeout),
95
105
  runtime_env_include=_text_list(
96
106
  runtime_env.get("include", []), "runtime.env.include"
97
107
  ),
@@ -173,6 +173,7 @@ def _apply_runtime_configuration(
173
173
  if args.audit is None:
174
174
  args.audit = DEFAULT_AUDIT
175
175
  args.runtime_env_include = configuration.runtime_env_include
176
+ args.runtime_idle_timeout_seconds = configuration.runtime_idle_timeout_seconds
176
177
  args.channel_options = {
177
178
  "bot_id": configuration.wecom_bot_id,
178
179
  "websocket_url": configuration.wecom_websocket_url,
@@ -189,6 +190,7 @@ async def _run_node(args: argparse.Namespace, parser: argparse.ArgumentParser) -
189
190
  runtime_options=_runtime_options(args),
190
191
  runtime_sandbox_mode=args.sandbox_mode,
191
192
  runtime_network_access=args.network_access,
193
+ runtime_idle_timeout_seconds=args.runtime_idle_timeout_seconds,
192
194
  channel_options=args.channel_options if args.channel == "wecom" else {},
193
195
  runtime_environment_include=args.runtime_env_include,
194
196
  )
@@ -1,11 +1,12 @@
1
1
  """Codex App Server process, protocol, and runtime adapter."""
2
2
 
3
3
  from .client import (
4
- CodexAppServerClient,
5
- CodexAppServerProtocolError,
6
- CodexErrorInfo,
7
- CodexThreadInfo,
8
- CodexTurnInfo,
4
+ Client,
5
+ ErrorInfo,
6
+ FsChangedInfo,
7
+ ThreadInfo,
8
+ TurnInfo,
9
+ build_fs_watch_params,
9
10
  build_initialize_params,
10
11
  build_thread_resume_params,
11
12
  build_thread_start_params,
@@ -13,18 +14,23 @@ from .client import (
13
14
  build_turn_start_params,
14
15
  build_turn_steer_params,
15
16
  parse_error_notification,
17
+ parse_fs_changed_notification,
18
+ parse_fs_watch_response,
19
+ parse_initialize_response,
20
+ parse_skills_changed_notification,
16
21
  parse_thread_response,
17
22
  parse_turn_notification,
18
23
  parse_turn_response,
19
24
  parse_turn_steer_response,
20
25
  )
21
- from .events import CodexTurnEventStream
26
+ from .events import TurnEventStream
22
27
  from .process import (
23
28
  JsonlProcessSpec,
24
29
  JsonlProcessState,
25
30
  JsonlProcessSupervisor,
26
31
  )
27
32
  from .protocol import (
33
+ AppServerProtocolError,
28
34
  JsonlMessage,
29
35
  JsonlProcessExited,
30
36
  JsonlProcessNotRunning,
@@ -33,16 +39,13 @@ from .protocol import (
33
39
  JsonlRequestTimeout,
34
40
  JsonlTransportError,
35
41
  )
36
- from .runtime import CodexAppServerRuntime
42
+ from .runtime import Runtime
37
43
 
38
44
  __all__ = [
39
- "CodexAppServerClient",
40
- "CodexAppServerProtocolError",
41
- "CodexAppServerRuntime",
42
- "CodexErrorInfo",
43
- "CodexThreadInfo",
44
- "CodexTurnEventStream",
45
- "CodexTurnInfo",
45
+ "AppServerProtocolError",
46
+ "Client",
47
+ "ErrorInfo",
48
+ "FsChangedInfo",
46
49
  "JsonlMessage",
47
50
  "JsonlProcessExited",
48
51
  "JsonlProcessNotRunning",
@@ -53,6 +56,11 @@ __all__ = [
53
56
  "JsonlRemoteError",
54
57
  "JsonlRequestTimeout",
55
58
  "JsonlTransportError",
59
+ "Runtime",
60
+ "ThreadInfo",
61
+ "TurnEventStream",
62
+ "TurnInfo",
63
+ "build_fs_watch_params",
56
64
  "build_initialize_params",
57
65
  "build_thread_resume_params",
58
66
  "build_thread_start_params",
@@ -60,6 +68,10 @@ __all__ = [
60
68
  "build_turn_start_params",
61
69
  "build_turn_steer_params",
62
70
  "parse_error_notification",
71
+ "parse_fs_changed_notification",
72
+ "parse_fs_watch_response",
73
+ "parse_initialize_response",
74
+ "parse_skills_changed_notification",
63
75
  "parse_thread_response",
64
76
  "parse_turn_notification",
65
77
  "parse_turn_response",
@@ -4,9 +4,15 @@ from collections.abc import Mapping
4
4
  from dataclasses import dataclass
5
5
  from typing import cast
6
6
 
7
- from ...core.models import ApprovalDecision, ApprovalRequest, ApprovalResult
7
+ from ...core.models import (
8
+ ApprovalDecision,
9
+ ApprovalResult,
10
+ )
11
+ from ...core.models import (
12
+ ApprovalRequest as CoreApprovalRequest,
13
+ )
8
14
  from .protocol import (
9
- CodexAppServerProtocolError,
15
+ AppServerProtocolError,
10
16
  JsonlMessage,
11
17
  JsonlRequestId,
12
18
  is_request_id,
@@ -26,13 +32,13 @@ _APPROVAL_METHODS = frozenset(
26
32
 
27
33
 
28
34
  @dataclass(frozen=True, slots=True)
29
- class CodexApprovalRequest:
35
+ class ApprovalEnvelope:
30
36
  """Provider request plus its neutral approval projection."""
31
37
 
32
38
  request_id: JsonlRequestId
33
39
  method: str
34
40
  params: Mapping[str, object]
35
- request: ApprovalRequest
41
+ request: CoreApprovalRequest
36
42
 
37
43
 
38
44
  def is_approval_method(method: object) -> bool:
@@ -47,28 +53,26 @@ def parse_approval_request(
47
53
  turn_id: str,
48
54
  provider_thread_id: str,
49
55
  provider_turn_id: str | None,
50
- ) -> CodexApprovalRequest:
56
+ ) -> ApprovalEnvelope:
51
57
  method = message.get("method")
52
58
  if not isinstance(method, str) or not is_approval_method(method):
53
- raise CodexAppServerProtocolError("message is not an approval request")
59
+ raise AppServerProtocolError("message is not an approval request")
54
60
  request_id = message.get("id")
55
61
  if not is_request_id(request_id):
56
- raise CodexAppServerProtocolError(
57
- "approval request id must be an integer or string"
58
- )
62
+ raise AppServerProtocolError("approval request id must be an integer or string")
59
63
  request_id = cast(JsonlRequestId, request_id)
60
64
  params = message.get("params")
61
65
  if not isinstance(params, Mapping):
62
- raise CodexAppServerProtocolError("approval request params must be an object")
66
+ raise AppServerProtocolError("approval request params must be an object")
63
67
 
64
68
  thread_value = _require_text(params, "threadId")
65
69
  if thread_value != provider_thread_id:
66
- raise CodexAppServerProtocolError(
70
+ raise AppServerProtocolError(
67
71
  "approval request thread does not match runtime thread"
68
72
  )
69
73
  provider_turn_value = _require_text(params, "turnId")
70
74
  if provider_turn_id is None or provider_turn_value != provider_turn_id:
71
- raise CodexAppServerProtocolError(
75
+ raise AppServerProtocolError(
72
76
  "approval request turn does not match runtime turn"
73
77
  )
74
78
  provider_item_id = _require_text(params, "itemId")
@@ -78,7 +82,7 @@ def parse_approval_request(
78
82
  or isinstance(started_at_ms, bool)
79
83
  or started_at_ms < 0
80
84
  ):
81
- raise CodexAppServerProtocolError(
85
+ raise AppServerProtocolError(
82
86
  "approval request startedAtMs must be a non-negative integer"
83
87
  )
84
88
  action = {
@@ -89,7 +93,7 @@ def parse_approval_request(
89
93
  if method == _PERMISSIONS_METHOD and not isinstance(
90
94
  params.get("permissions"), Mapping
91
95
  ):
92
- raise CodexAppServerProtocolError(
96
+ raise AppServerProtocolError(
93
97
  "permissions approval request has no permissions object"
94
98
  )
95
99
 
@@ -104,11 +108,11 @@ def parse_approval_request(
104
108
  value = params.get(provider_key)
105
109
  if isinstance(value, str) and value:
106
110
  metadata[metadata_key] = value
107
- return CodexApprovalRequest(
111
+ return ApprovalEnvelope(
108
112
  request_id=request_id,
109
113
  method=method,
110
114
  params=params,
111
- request=ApprovalRequest(
115
+ request=CoreApprovalRequest(
112
116
  request_id=str(request_id),
113
117
  session_id=session_id,
114
118
  runtime_session_id=runtime_session_id,
@@ -121,7 +125,7 @@ def parse_approval_request(
121
125
 
122
126
 
123
127
  def build_approval_response(
124
- approval: CodexApprovalRequest,
128
+ approval: ApprovalEnvelope,
125
129
  result: ApprovalResult,
126
130
  ) -> Mapping[str, object]:
127
131
  if result.request_id != approval.request.request_id:
@@ -134,11 +138,11 @@ def build_approval_response(
134
138
  return {"permissions": {}}
135
139
  permissions = approval.params.get("permissions")
136
140
  if not isinstance(permissions, Mapping):
137
- raise CodexAppServerProtocolError(
141
+ raise AppServerProtocolError(
138
142
  "permissions approval request has no permissions object"
139
143
  )
140
144
  return {"permissions": dict(permissions), "scope": "turn"}
141
- raise CodexAppServerProtocolError("unsupported approval response method")
145
+ raise AppServerProtocolError("unsupported approval response method")
142
146
 
143
147
 
144
148
  def approval_error(error: BaseException) -> Mapping[str, object]:
@@ -153,14 +157,14 @@ def approval_error(error: BaseException) -> Mapping[str, object]:
153
157
  def _require_text(params: Mapping[str, object], field_name: str) -> str:
154
158
  value = params.get(field_name)
155
159
  if not isinstance(value, str) or not value:
156
- raise CodexAppServerProtocolError(
160
+ raise AppServerProtocolError(
157
161
  f"approval request {field_name} must be non-empty text"
158
162
  )
159
163
  return value
160
164
 
161
165
 
162
166
  __all__ = [
163
- "CodexApprovalRequest",
167
+ "ApprovalEnvelope",
164
168
  "approval_error",
165
169
  "build_approval_response",
166
170
  "is_approval_method",