bazaar-compute-node 0.1.14__tar.gz → 0.1.16__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 (95) hide show
  1. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/__init__.py +2 -0
  5. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/client.py +26 -0
  6. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/runtime.py +29 -6
  7. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/session.py +126 -142
  8. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/runtime.py +4 -0
  9. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/LICENSE +0 -0
  10. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/README.md +0 -0
  11. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/__init__.py +0 -0
  12. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/__init__.py +0 -0
  13. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/agent.py +0 -0
  14. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/agent_management.py +0 -0
  15. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/application.py +0 -0
  16. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/attachments.py +0 -0
  17. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/command.py +0 -0
  18. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/config.py +0 -0
  19. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/registry.py +0 -0
  20. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/reminder_dispatch.py +0 -0
  21. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/system_service.py +0 -0
  22. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/transport.py +0 -0
  23. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  24. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/app/wrapper.py +0 -0
  25. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/bcc.py +0 -0
  26. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/cli.py +0 -0
  27. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  28. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  29. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  30. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  31. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  32. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  33. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  34. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  35. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  36. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  37. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +0 -0
  38. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  39. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  40. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  41. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  42. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  43. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +0 -0
  44. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
  45. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +0 -0
  46. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  47. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +0 -0
  48. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  49. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  50. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  51. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  52. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/channel.py +0 -0
  53. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  54. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  55. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  56. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  57. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  58. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  59. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  60. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  61. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  62. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/__init__.py +0 -0
  63. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/approval.py +0 -0
  64. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/audit.py +0 -0
  65. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/channel.py +0 -0
  66. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/client.py +0 -0
  67. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/command.py +0 -0
  68. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/concurrency.py +0 -0
  69. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/correlation.py +0 -0
  70. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/instruction.py +0 -0
  71. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  72. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  73. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/models/entities.py +0 -0
  74. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  75. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  76. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/models/states.py +0 -0
  77. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/observability.py +0 -0
  78. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  79. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  80. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  81. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  82. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  83. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  84. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  85. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  86. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/outcomes.py +0 -0
  87. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/paths.py +0 -0
  88. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/reminder.py +0 -0
  89. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  90. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/storage.py +0 -0
  91. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  92. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  93. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  94. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/src/bazaar_compute_node/i18n/english.py +0 -0
  95. {bazaar_compute_node-0.1.14 → bazaar_compute_node-0.1.16}/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.14
3
+ Version: 0.1.16
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.14"
3
+ version = "0.1.16"
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.14"
3
+ version = "0.1.16"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -13,6 +13,7 @@ from .client import (
13
13
  build_turn_interrupt_params,
14
14
  build_turn_start_params,
15
15
  build_turn_steer_params,
16
+ parse_background_terminals_response,
16
17
  parse_error_notification,
17
18
  parse_fs_changed_notification,
18
19
  parse_fs_watch_response,
@@ -67,6 +68,7 @@ __all__ = [
67
68
  "build_turn_interrupt_params",
68
69
  "build_turn_start_params",
69
70
  "build_turn_steer_params",
71
+ "parse_background_terminals_response",
70
72
  "parse_error_notification",
71
73
  "parse_fs_changed_notification",
72
74
  "parse_fs_watch_response",
@@ -331,6 +331,19 @@ class Client:
331
331
  timeout=timeout,
332
332
  )
333
333
 
334
+ async def list_background_terminals(
335
+ self,
336
+ thread_id: str,
337
+ *,
338
+ timeout: float,
339
+ ) -> JsonlMessage:
340
+ _validate_non_empty_string("thread_id", thread_id)
341
+ return await self.supervisor.request(
342
+ "thread/backgroundTerminals/list",
343
+ {"threadId": thread_id},
344
+ timeout=timeout,
345
+ )
346
+
334
347
  async def receive(self, *, timeout: float | None = None) -> JsonlMessage:
335
348
  return await self.supervisor.receive(timeout=timeout)
336
349
 
@@ -420,6 +433,18 @@ def parse_turn_steer_response(response: Mapping[str, object]) -> str:
420
433
  return _require_text(result, "turnId", "result.turnId")
421
434
 
422
435
 
436
+ def parse_background_terminals_response(
437
+ response: Mapping[str, object],
438
+ ) -> bool:
439
+ result = _require_mapping(response, "result")
440
+ data = result.get("data")
441
+ if not isinstance(data, list):
442
+ raise AppServerProtocolError("result.data must be an array")
443
+ if any(not isinstance(item, Mapping) for item in data):
444
+ raise AppServerProtocolError("result.data items must be objects")
445
+ return bool(data)
446
+
447
+
423
448
  def parse_turn_notification(message: Mapping[str, object]) -> tuple[str, TurnInfo]:
424
449
  params = _require_mapping(message, "params")
425
450
  thread_id = _require_text(params, "threadId", "params.threadId")
@@ -532,6 +557,7 @@ __all__ = [
532
557
  "build_thread_start_params",
533
558
  "build_turn_interrupt_params",
534
559
  "build_turn_start_params",
560
+ "parse_background_terminals_response",
535
561
  "parse_error_notification",
536
562
  "parse_fs_changed_notification",
537
563
  "parse_fs_watch_response",
@@ -32,6 +32,7 @@ from ...core.runtime import (
32
32
  )
33
33
  from .client import (
34
34
  Client,
35
+ parse_background_terminals_response,
35
36
  parse_fs_changed_notification,
36
37
  parse_fs_watch_response,
37
38
  parse_initialize_response,
@@ -112,6 +113,11 @@ class Runtime(IRuntime, IAsyncLifecycle):
112
113
  del timeout
113
114
  if self._stopping:
114
115
  raise RuntimeError("Codex App Server runtime is stopping")
116
+ if os.name == "nt":
117
+ self._logger.warning(
118
+ "Codex idle timeout is not supported on Windows; "
119
+ "see openai/codex#15461."
120
+ )
115
121
  self._started = True
116
122
 
117
123
  async def stop(self, *, timeout: float) -> None:
@@ -516,6 +522,28 @@ class Runtime(IRuntime, IAsyncLifecycle):
516
522
  return False
517
523
  return True
518
524
 
525
+ async def has_background_job(
526
+ self,
527
+ session: RuntimeSession,
528
+ *,
529
+ timeout: float,
530
+ ) -> bool:
531
+ if os.name == "nt":
532
+ # Detached OS processes are not exposed by the Windows Codex provider.
533
+ return True
534
+ connection = self._connections.get(session.id)
535
+ provider_thread_id = session.provider_thread_id
536
+ if connection is None or not provider_thread_id:
537
+ return False
538
+ try:
539
+ response = await connection.client.list_background_terminals(
540
+ provider_thread_id,
541
+ timeout=timeout,
542
+ )
543
+ except AppServerProtocolError, JsonlTransportError:
544
+ return False
545
+ return parse_background_terminals_response(response)
546
+
519
547
  async def stop_session(
520
548
  self,
521
549
  session: RuntimeSession,
@@ -528,12 +556,7 @@ class Runtime(IRuntime, IAsyncLifecycle):
528
556
  status=ProviderCallStatus.CONFIRMED,
529
557
  value=session,
530
558
  )
531
- try:
532
- await connection.supervisor.stop(timeout=timeout)
533
- except asyncio.CancelledError:
534
- raise
535
- except Exception as error: # noqa: BLE001
536
- return _provider_result(error)
559
+ await self._stop_connection(connection, timeout=timeout)
537
560
  return ProviderCallResult(
538
561
  status=ProviderCallStatus.CONFIRMED,
539
562
  value=session,
@@ -68,7 +68,6 @@ def _current_time_ms() -> int:
68
68
  class _IngressItem:
69
69
  message: InboundMessage
70
70
  completion: asyncio.Future[RuntimeTurn | None]
71
- activity_at_ms: int
72
71
 
73
72
 
74
73
  @dataclass(slots=True)
@@ -82,7 +81,6 @@ class _RuntimeNotification:
82
81
  message: InboundMessage
83
82
  context: _DurableSessionContext
84
83
  completion: asyncio.Future[RuntimeTurn | None]
85
- activity_at_ms: int
86
84
 
87
85
 
88
86
  @dataclass(frozen=True, slots=True)
@@ -92,14 +90,6 @@ class _ReminderNotification:
92
90
  anchor_message: InboundMessage
93
91
  context: _DurableSessionContext
94
92
  wake_id: str
95
- activity_at_ms: int
96
-
97
-
98
- @dataclass(slots=True)
99
- class _RuntimeActivity:
100
- context: _DurableSessionContext
101
- completion: asyncio.Future[RuntimeTurn | None]
102
- activity_at_ms: int
103
93
 
104
94
 
105
95
  @dataclass(frozen=True, slots=True)
@@ -112,11 +102,7 @@ class _RuntimeExpiry:
112
102
 
113
103
  type _WakeNotification = _RuntimeNotification | _ReminderNotification
114
104
  type _RuntimeQueueItem = (
115
- _RuntimeNotification
116
- | _ReminderNotification
117
- | _RuntimeActivity
118
- | _RuntimeExpiry
119
- | RuntimeExpire
105
+ _RuntimeNotification | _ReminderNotification | _RuntimeExpiry | RuntimeExpire
120
106
  )
121
107
 
122
108
 
@@ -218,6 +204,7 @@ class SessionOrchestrator(IAsyncLifecycle):
218
204
  clock=self._clock,
219
205
  )
220
206
  self._active_tasks: set[asyncio.Task[RuntimeTurn | None]] = set()
207
+ self._runtime_teardown_tasks: set[asyncio.Task[None]] = set()
221
208
  self._ingress_queues: dict[tuple[str, str], asyncio.Queue[_IngressItem]] = {}
222
209
  self._ingress_workers: dict[tuple[str, str], asyncio.Task[None]] = {}
223
210
  self._runtime_queues: dict[str, asyncio.Queue[_RuntimeQueueItem]] = {}
@@ -291,7 +278,6 @@ class SessionOrchestrator(IAsyncLifecycle):
291
278
  anchor_message=anchor_message,
292
279
  context=context,
293
280
  wake_id=str(uuid7()),
294
- activity_at_ms=self._timer_wheel.now_ms,
295
281
  )
296
282
  )
297
283
 
@@ -348,42 +334,14 @@ class SessionOrchestrator(IAsyncLifecycle):
348
334
  binding.watcher.cancel()
349
335
  await asyncio.gather(binding.watcher, return_exceptions=True)
350
336
 
351
- async def _refresh_runtime_timer(
352
- self,
353
- runtime_session: RuntimeSession,
354
- activity_at_ms: int,
355
- *,
356
- full_timeout: bool = False,
357
- ) -> None:
337
+ async def _start_runtime_timer(self, runtime_session: RuntimeSession) -> None:
358
338
  if self._runtime_idle_timeout_ms <= 0:
359
339
  return
360
- binding = self._runtime_timers.get(runtime_session.bcn_session_id)
361
- delay_ms = self._runtime_idle_timeout_ms
362
- if not full_timeout:
363
- delay_ms = max(
364
- 0,
365
- delay_ms - (self._timer_wheel.now_ms - activity_at_ms),
366
- )
367
- if (
368
- binding is not None
369
- and binding.runtime_session_id == runtime_session.id
370
- and binding.timer.active
371
- ):
372
- try:
373
- binding.timer.reset(delay_ms)
374
- except RuntimeError:
375
- if binding.timer.active:
376
- raise
377
- else:
378
- binding.expired = False
379
- return
380
- if binding is not None:
381
- binding.timer.cancel()
382
- if not binding.watcher.done():
383
- binding.watcher.cancel()
384
- await asyncio.gather(binding.watcher, return_exceptions=True)
385
- timer = self._timer_wheel.create(delay_ms)
386
- queue = self._runtime_queues[runtime_session.bcn_session_id]
340
+ queue = self._runtime_queues.get(runtime_session.bcn_session_id)
341
+ if queue is None:
342
+ return
343
+ await self._cancel_runtime_timer(runtime_session)
344
+ timer = self._timer_wheel.create(self._runtime_idle_timeout_ms)
387
345
  watcher = asyncio.create_task(
388
346
  self._forward_runtime_session_expiry(runtime_session, timer, queue),
389
347
  name=f"bcn-runtime-expiry-{runtime_session.bcn_session_id}",
@@ -394,6 +352,26 @@ class SessionOrchestrator(IAsyncLifecycle):
394
352
  watcher,
395
353
  )
396
354
 
355
+ async def _start_runtime_timer_if_idle(self, session_id: str) -> None:
356
+ runtime_session = self.runtime_session(session_id)
357
+ if (
358
+ runtime_session is None
359
+ or runtime_session.id in self._expired_runtime_ids
360
+ or self._state_machine.get(session_id) is not SessionRuntimeState.IDLE
361
+ ):
362
+ return
363
+ try:
364
+ if await self._runtime.has_background_job(
365
+ runtime_session,
366
+ timeout=self._timeout_budget.provider_call_seconds,
367
+ ):
368
+ return
369
+ except asyncio.CancelledError:
370
+ raise
371
+ except Exception:
372
+ self._logger.exception("runtime background job check failed")
373
+ await self._start_runtime_timer(runtime_session)
374
+
397
375
  async def _forward_runtime_session_expiry(
398
376
  self,
399
377
  runtime_session: RuntimeSession,
@@ -431,7 +409,7 @@ class SessionOrchestrator(IAsyncLifecycle):
431
409
  )
432
410
  except BaseException:
433
411
  try:
434
- await self._runtime.stop_session(
412
+ await self._stop_runtime_session(
435
413
  context.runtime_session,
436
414
  timeout=self._timeout_budget.provider_call_seconds,
437
415
  )
@@ -520,6 +498,8 @@ class SessionOrchestrator(IAsyncLifecycle):
520
498
  for runtime_session in tuple(self._runtime_sessions.values()):
521
499
  await self._stop_runtime_session(runtime_session, timeout=timeout)
522
500
 
501
+ await self._wait_for_runtime_teardown_tasks(timeout=timeout)
502
+
523
503
  try:
524
504
  await self._runtime.stop(timeout=timeout)
525
505
  except asyncio.CancelledError:
@@ -567,9 +547,7 @@ class SessionOrchestrator(IAsyncLifecycle):
567
547
  self._ingress_loop(ingress_queue),
568
548
  name=f"bcn-ingress-{message.channel_session_id}",
569
549
  )
570
- ingress_queue.put_nowait(
571
- _IngressItem(message, completion, self._timer_wheel.now_ms)
572
- )
550
+ ingress_queue.put_nowait(_IngressItem(message, completion))
573
551
  return await completion
574
552
 
575
553
  async def _ingress_loop(self, queue: asyncio.Queue[_IngressItem]) -> None:
@@ -590,24 +568,15 @@ class SessionOrchestrator(IAsyncLifecycle):
590
568
  or self.runtime_session(session_id) is not None
591
569
  ):
592
570
  runtime_queue = self._runtime_queue_for_session(session_id)
593
- if runtime_queue is None:
571
+ if runtime_queue is None or not message.notifies_runtime:
594
572
  if not item.completion.done():
595
573
  item.completion.set_result(None)
596
- elif message.notifies_runtime:
574
+ else:
597
575
  runtime_queue.put_nowait(
598
576
  _RuntimeNotification(
599
577
  message,
600
578
  context,
601
579
  item.completion,
602
- item.activity_at_ms,
603
- )
604
- )
605
- else:
606
- runtime_queue.put_nowait(
607
- _RuntimeActivity(
608
- context,
609
- item.completion,
610
- item.activity_at_ms,
611
580
  )
612
581
  )
613
582
  except asyncio.CancelledError:
@@ -628,22 +597,6 @@ class SessionOrchestrator(IAsyncLifecycle):
628
597
  pending: list[_RuntimeQueueItem] = []
629
598
  while True:
630
599
  item = pending.pop(0) if pending else await queue.get()
631
- if isinstance(item, _RuntimeActivity):
632
- try:
633
- runtime_session = self.runtime_session(item.context.bcn_session.id)
634
- if runtime_session is not None:
635
- await self._refresh_runtime_timer(
636
- runtime_session,
637
- item.activity_at_ms,
638
- )
639
- if not item.completion.done():
640
- item.completion.set_result(None)
641
- except Exception as error: # noqa: BLE001
642
- if not item.completion.done():
643
- item.completion.set_exception(error)
644
- finally:
645
- queue.task_done()
646
- continue
647
600
  if isinstance(item, _RuntimeExpiry):
648
601
  try:
649
602
  await self._handle_runtime_expiry(
@@ -679,15 +632,9 @@ class SessionOrchestrator(IAsyncLifecycle):
679
632
  pending.insert(0, candidate)
680
633
  break
681
634
  batch.append(candidate)
682
- for notification in batch:
683
- runtime_session = self.runtime_session(
684
- notification.context.bcn_session.id
685
- )
686
- if runtime_session is not None:
687
- await self._refresh_runtime_timer(
688
- runtime_session,
689
- notification.activity_at_ms,
690
- )
635
+ runtime_session = self.runtime_session(batch[0].context.bcn_session.id)
636
+ if runtime_session is not None:
637
+ await self._cancel_runtime_timer(runtime_session)
691
638
  turn_task = asyncio.create_task(
692
639
  self._run_notification(batch[0]),
693
640
  name=f"bcn-turn-{batch[0].context.bcn_session.id}",
@@ -711,24 +658,9 @@ class SessionOrchestrator(IAsyncLifecycle):
711
658
  queued_item.context.bcn_session.id
712
659
  )
713
660
  if runtime_session is not None:
714
- await self._refresh_runtime_timer(
715
- runtime_session,
716
- queued_item.activity_at_ms,
717
- )
661
+ await self._cancel_runtime_timer(runtime_session)
718
662
  pending.append(queued_item)
719
663
  await self._steer_active_turn(queued_item)
720
- elif isinstance(queued_item, _RuntimeActivity):
721
- runtime_session = self.runtime_session(
722
- queued_item.context.bcn_session.id
723
- )
724
- if runtime_session is not None:
725
- await self._refresh_runtime_timer(
726
- runtime_session,
727
- queued_item.activity_at_ms,
728
- )
729
- if not queued_item.completion.done():
730
- queued_item.completion.set_result(None)
731
- queue.task_done()
732
664
  elif isinstance(queued_item, _RuntimeExpiry):
733
665
  await self._handle_runtime_expiry(
734
666
  queued_item,
@@ -749,6 +681,9 @@ class SessionOrchestrator(IAsyncLifecycle):
749
681
  queue_item_consumed = False
750
682
 
751
683
  result = turn_task.result()
684
+ await self._start_runtime_timer_if_idle(
685
+ batch[0].context.bcn_session.id,
686
+ )
752
687
  route_message = (
753
688
  batch[0].message
754
689
  if isinstance(batch[0], _RuntimeNotification)
@@ -784,7 +719,7 @@ class SessionOrchestrator(IAsyncLifecycle):
784
719
  queue_item_consumed = True
785
720
  for queued_item in (*batch, *pending):
786
721
  if (
787
- isinstance(queued_item, _RuntimeNotification | _RuntimeActivity)
722
+ isinstance(queued_item, _RuntimeNotification)
788
723
  and not queued_item.completion.done()
789
724
  ):
790
725
  queued_item.completion.cancel()
@@ -795,6 +730,9 @@ class SessionOrchestrator(IAsyncLifecycle):
795
730
  except Exception as error:
796
731
  turn_task.cancel()
797
732
  await asyncio.gather(turn_task, return_exceptions=True)
733
+ await self._start_runtime_timer_if_idle(
734
+ batch[0].context.bcn_session.id,
735
+ )
798
736
  for notification in batch:
799
737
  if (
800
738
  isinstance(notification, _RuntimeNotification)
@@ -1205,16 +1143,6 @@ class SessionOrchestrator(IAsyncLifecycle):
1205
1143
  self._state_machine.get(context.bcn_session.id)
1206
1144
  is SessionRuntimeState.IDLE
1207
1145
  ):
1208
- binding = self._runtime_timers.get(context.bcn_session.id)
1209
- if (
1210
- binding is None
1211
- or binding.runtime_session_id != context.runtime_session.id
1212
- ):
1213
- await self._refresh_runtime_timer(
1214
- context.runtime_session,
1215
- notification.activity_at_ms,
1216
- full_timeout=establishment_attempt > 0,
1217
- )
1218
1146
  break
1219
1147
  if establishment_attempt == 0:
1220
1148
  await self._discard_runtime_session(context.runtime_session)
@@ -1590,38 +1518,45 @@ class SessionOrchestrator(IAsyncLifecycle):
1590
1518
  async with self._concurrency.for_session(runtime_session.bcn_session_id):
1591
1519
  await self._stop_runtime_session_locked(runtime_session, timeout=timeout)
1592
1520
 
1593
- async def _stop_runtime_session_locked(
1521
+ async def _wait_for_runtime_teardown_tasks(self, *, timeout: float) -> None:
1522
+ tasks = tuple(self._runtime_teardown_tasks)
1523
+ if not tasks:
1524
+ return
1525
+ gathered = asyncio.gather(*tasks, return_exceptions=True)
1526
+ try:
1527
+ await asyncio.wait_for(asyncio.shield(gathered), timeout=timeout)
1528
+ except TimeoutError:
1529
+ self._shutdown_errors.append("runtime session teardown: shutdown timeout")
1530
+
1531
+ def _schedule_runtime_session_teardown(
1594
1532
  self,
1595
1533
  runtime_session: RuntimeSession,
1596
1534
  *,
1535
+ correlation: CorrelationContext,
1597
1536
  timeout: float,
1598
1537
  ) -> None:
1599
- process_correlation = CorrelationContext(
1600
- node_id=self.agent_id,
1601
- channel_session_id=runtime_session.channel_session_id,
1602
- bcn_session_id=runtime_session.bcn_session_id,
1603
- runtime_session_id=runtime_session.id,
1604
- provider_thread_id=runtime_session.provider_thread_id,
1605
- )
1606
- await self._audit.append(
1607
- event_name="runtime.process.stop.requested",
1608
- state=RuntimeEventState.STARTED,
1609
- correlation=process_correlation,
1610
- metadata={
1611
- "runtime": runtime_session.runtime,
1612
- "workspace_id": runtime_session.workspace_id,
1613
- },
1614
- )
1615
- await self._state_machine.apply_locked(
1616
- runtime_session.bcn_session_id,
1617
- SessionRuntimeObservation(
1618
- source=SessionRuntimeObservationSource.SESSION,
1619
- signal=SessionRuntimeSignal.STOP_REQUESTED,
1620
- observed_at_ms=self._clock(),
1538
+ task = asyncio.create_task(
1539
+ self._complete_runtime_session_teardown(
1540
+ runtime_session,
1541
+ correlation=correlation,
1542
+ timeout=timeout,
1621
1543
  ),
1544
+ name=f"bcn-runtime-teardown-{runtime_session.id}",
1622
1545
  )
1546
+ self._runtime_teardown_tasks.add(task)
1547
+ task.add_done_callback(self._forget_runtime_teardown_task)
1548
+
1549
+ async def _complete_runtime_session_teardown(
1550
+ self,
1551
+ runtime_session: RuntimeSession,
1552
+ *,
1553
+ correlation: CorrelationContext,
1554
+ timeout: float,
1555
+ ) -> None:
1623
1556
  try:
1624
1557
  result = await self._runtime.stop_session(runtime_session, timeout=timeout)
1558
+ except asyncio.CancelledError:
1559
+ raise
1625
1560
  except Exception as error: # noqa: BLE001
1626
1561
  result = None
1627
1562
  stop_error = error
@@ -1638,7 +1573,6 @@ class SessionOrchestrator(IAsyncLifecycle):
1638
1573
  else ErrorKind.PROVIDER_UNKNOWN.value
1639
1574
  )
1640
1575
  error_message = result.error_message if result is not None else str(stop_error)
1641
- await self._discard_runtime_session(runtime_session)
1642
1576
  await self._audit.append(
1643
1577
  event_name=(
1644
1578
  "runtime.process.stop.completed"
@@ -1654,7 +1588,7 @@ class SessionOrchestrator(IAsyncLifecycle):
1654
1588
  if unknown
1655
1589
  else RuntimeEventState.FAILED
1656
1590
  ),
1657
- correlation=process_correlation,
1591
+ correlation=correlation,
1658
1592
  error_kind=(
1659
1593
  ErrorKind(error_kind)
1660
1594
  if error_kind in ErrorKind._value2member_map_
@@ -1670,3 +1604,53 @@ class SessionOrchestrator(IAsyncLifecycle):
1670
1604
  "workspace_id": runtime_session.workspace_id,
1671
1605
  },
1672
1606
  )
1607
+
1608
+ def _forget_runtime_teardown_task(self, task: asyncio.Task[None]) -> None:
1609
+ self._runtime_teardown_tasks.discard(task)
1610
+ if task.cancelled():
1611
+ return
1612
+ error = task.exception()
1613
+ if error is None:
1614
+ return
1615
+ self._logger.error(
1616
+ "runtime session teardown failed: %s",
1617
+ error,
1618
+ exc_info=(type(error), error, error.__traceback__),
1619
+ )
1620
+
1621
+ async def _stop_runtime_session_locked(
1622
+ self,
1623
+ runtime_session: RuntimeSession,
1624
+ *,
1625
+ timeout: float,
1626
+ ) -> None:
1627
+ process_correlation = CorrelationContext(
1628
+ node_id=self.agent_id,
1629
+ channel_session_id=runtime_session.channel_session_id,
1630
+ bcn_session_id=runtime_session.bcn_session_id,
1631
+ runtime_session_id=runtime_session.id,
1632
+ provider_thread_id=runtime_session.provider_thread_id,
1633
+ )
1634
+ await self._audit.append(
1635
+ event_name="runtime.process.stop.requested",
1636
+ state=RuntimeEventState.STARTED,
1637
+ correlation=process_correlation,
1638
+ metadata={
1639
+ "runtime": runtime_session.runtime,
1640
+ "workspace_id": runtime_session.workspace_id,
1641
+ },
1642
+ )
1643
+ await self._state_machine.apply_locked(
1644
+ runtime_session.bcn_session_id,
1645
+ SessionRuntimeObservation(
1646
+ source=SessionRuntimeObservationSource.SESSION,
1647
+ signal=SessionRuntimeSignal.STOP_REQUESTED,
1648
+ observed_at_ms=self._clock(),
1649
+ ),
1650
+ )
1651
+ self._schedule_runtime_session_teardown(
1652
+ runtime_session,
1653
+ correlation=process_correlation,
1654
+ timeout=timeout,
1655
+ )
1656
+ await self._discard_runtime_session(runtime_session)
@@ -140,6 +140,10 @@ class IRuntime(IAsyncLifecycle, Protocol):
140
140
  timeout: float,
141
141
  ) -> ProviderCallResult[RuntimeTurn]: ...
142
142
 
143
+ async def has_background_job(
144
+ self, session: RuntimeSession, *, timeout: float
145
+ ) -> bool: ...
146
+
143
147
  async def stop_session(
144
148
  self, session: RuntimeSession, *, timeout: float
145
149
  ) -> ProviderCallResult[RuntimeSession]: ...