bazaar-compute-node 0.1.13__tar.gz → 0.1.15__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.13 → bazaar_compute_node-0.1.15}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/system_service.py +16 -3
  5. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/__init__.py +2 -0
  6. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/client.py +26 -0
  7. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/runtime.py +46 -7
  8. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/session.py +41 -116
  9. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/runtime.py +4 -0
  10. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/LICENSE +0 -0
  11. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/README.md +0 -0
  12. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/__init__.py +0 -0
  13. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/__init__.py +0 -0
  14. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/agent.py +0 -0
  15. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/agent_management.py +0 -0
  16. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/application.py +0 -0
  17. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/attachments.py +0 -0
  18. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/command.py +0 -0
  19. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/config.py +0 -0
  20. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/registry.py +0 -0
  21. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/reminder_dispatch.py +0 -0
  22. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/transport.py +0 -0
  23. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  24. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/app/wrapper.py +0 -0
  25. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/bcc.py +0 -0
  26. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/cli.py +0 -0
  27. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  28. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  29. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  30. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  31. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  32. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  33. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  34. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  35. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  36. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  37. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +0 -0
  38. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  39. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  40. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  41. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  42. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  43. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +0 -0
  44. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
  45. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +0 -0
  46. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  47. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +0 -0
  48. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  49. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  50. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  51. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  52. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/channel.py +0 -0
  53. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  54. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  55. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  56. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  57. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  58. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  59. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  60. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  61. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  62. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/__init__.py +0 -0
  63. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/approval.py +0 -0
  64. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/audit.py +0 -0
  65. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/channel.py +0 -0
  66. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/client.py +0 -0
  67. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/command.py +0 -0
  68. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/concurrency.py +0 -0
  69. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/correlation.py +0 -0
  70. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/instruction.py +0 -0
  71. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  72. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  73. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/models/entities.py +0 -0
  74. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  75. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  76. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/models/states.py +0 -0
  77. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/observability.py +0 -0
  78. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  79. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  80. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  81. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  82. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  83. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  84. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  85. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  86. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/outcomes.py +0 -0
  87. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/paths.py +0 -0
  88. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/reminder.py +0 -0
  89. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  90. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/storage.py +0 -0
  91. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  92. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  93. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  94. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/src/bazaar_compute_node/i18n/english.py +0 -0
  95. {bazaar_compute_node-0.1.13 → bazaar_compute_node-0.1.15}/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.13
3
+ Version: 0.1.15
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.13"
3
+ version = "0.1.15"
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.13"
3
+ version = "0.1.15"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -250,11 +250,24 @@ $environmentScript = {environment_script}
250
250
  $logPath = {log_path}
251
251
 
252
252
  if ($environmentScript -and (Test-Path -LiteralPath $environmentScript)) {{
253
- . $environmentScript
253
+ try {{
254
+ . $environmentScript
255
+ }} catch {{
256
+ $_ | Out-String | Add-Content -LiteralPath $logPath -Encoding utf8
257
+ exit 1
258
+ }}
254
259
  }}
255
260
 
256
- & $executable run --config $configPath *>> $logPath
257
- exit $LASTEXITCODE
261
+ # Native stderr is routed to the service log and must not terminate the wrapper.
262
+ $ErrorActionPreference = 'Continue'
263
+ try {{
264
+ & $executable run --config $configPath *>> $logPath
265
+ $exitCode = $LASTEXITCODE
266
+ }} catch {{
267
+ $_ | Out-String | Add-Content -LiteralPath $logPath -Encoding utf8
268
+ exit 1
269
+ }}
270
+ exit $exitCode
258
271
  """
259
272
 
260
273
 
@@ -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,
@@ -103,6 +104,7 @@ class Runtime(IRuntime, IAsyncLifecycle):
103
104
  self._model = model
104
105
  self._effort = effort
105
106
  self._connections: dict[str, _Connection] = {}
107
+ self._teardown_tasks: set[asyncio.Task[None]] = set()
106
108
  self._expire_events: asyncio.Queue[RuntimeExpire] = asyncio.Queue()
107
109
  self._logger = logging.getLogger("bazaar_compute_node.runtime.codex")
108
110
  self._started = False
@@ -516,6 +518,25 @@ class Runtime(IRuntime, IAsyncLifecycle):
516
518
  return False
517
519
  return True
518
520
 
521
+ async def has_background_job(
522
+ self,
523
+ session: RuntimeSession,
524
+ *,
525
+ timeout: float,
526
+ ) -> bool:
527
+ connection = self._connections.get(session.id)
528
+ provider_thread_id = session.provider_thread_id
529
+ if connection is None or not provider_thread_id:
530
+ return False
531
+ try:
532
+ response = await connection.client.list_background_terminals(
533
+ provider_thread_id,
534
+ timeout=timeout,
535
+ )
536
+ except AppServerProtocolError, JsonlTransportError:
537
+ return False
538
+ return parse_background_terminals_response(response)
539
+
519
540
  async def stop_session(
520
541
  self,
521
542
  session: RuntimeSession,
@@ -528,15 +549,11 @@ class Runtime(IRuntime, IAsyncLifecycle):
528
549
  status=ProviderCallStatus.CONFIRMED,
529
550
  value=session,
530
551
  )
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)
552
+ self._schedule_connection_teardown(connection, timeout=timeout)
537
553
  return ProviderCallResult(
538
- status=ProviderCallStatus.CONFIRMED,
554
+ status=ProviderCallStatus.QUEUED,
539
555
  value=session,
556
+ receipt={"teardown": "scheduled"},
540
557
  )
541
558
 
542
559
  async def _open_connection(
@@ -643,6 +660,28 @@ class Runtime(IRuntime, IAsyncLifecycle):
643
660
  except OSError, TimeoutError, JsonlTransportError:
644
661
  return
645
662
 
663
+ def _schedule_connection_teardown(
664
+ self,
665
+ connection: _Connection,
666
+ *,
667
+ timeout: float,
668
+ ) -> None:
669
+ task = asyncio.create_task(
670
+ self._stop_connection(connection, timeout=timeout),
671
+ name=f"bcn-codex-teardown-{connection.provider_thread_id}",
672
+ )
673
+ self._teardown_tasks.add(task)
674
+ task.add_done_callback(self._forget_teardown_task)
675
+
676
+ def _forget_teardown_task(self, task: asyncio.Task[None]) -> None:
677
+ self._teardown_tasks.discard(task)
678
+ if task.cancelled():
679
+ return
680
+ try:
681
+ task.result()
682
+ except Exception:
683
+ self._logger.exception("Codex runtime connection teardown failed")
684
+
646
685
  def _clear_active_turn(self, session_id: str, turn_id: str) -> None:
647
686
  connection = self._connections.get(session_id)
648
687
  if connection is not None and connection.active_turn_id == turn_id:
@@ -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
 
@@ -291,7 +277,6 @@ class SessionOrchestrator(IAsyncLifecycle):
291
277
  anchor_message=anchor_message,
292
278
  context=context,
293
279
  wake_id=str(uuid7()),
294
- activity_at_ms=self._timer_wheel.now_ms,
295
280
  )
296
281
  )
297
282
 
@@ -348,42 +333,14 @@ class SessionOrchestrator(IAsyncLifecycle):
348
333
  binding.watcher.cancel()
349
334
  await asyncio.gather(binding.watcher, return_exceptions=True)
350
335
 
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:
336
+ async def _start_runtime_timer(self, runtime_session: RuntimeSession) -> None:
358
337
  if self._runtime_idle_timeout_ms <= 0:
359
338
  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]
339
+ queue = self._runtime_queues.get(runtime_session.bcn_session_id)
340
+ if queue is None:
341
+ return
342
+ await self._cancel_runtime_timer(runtime_session)
343
+ timer = self._timer_wheel.create(self._runtime_idle_timeout_ms)
387
344
  watcher = asyncio.create_task(
388
345
  self._forward_runtime_session_expiry(runtime_session, timer, queue),
389
346
  name=f"bcn-runtime-expiry-{runtime_session.bcn_session_id}",
@@ -394,6 +351,26 @@ class SessionOrchestrator(IAsyncLifecycle):
394
351
  watcher,
395
352
  )
396
353
 
354
+ async def _start_runtime_timer_if_idle(self, session_id: str) -> None:
355
+ runtime_session = self.runtime_session(session_id)
356
+ if (
357
+ runtime_session is None
358
+ or runtime_session.id in self._expired_runtime_ids
359
+ or self._state_machine.get(session_id) is not SessionRuntimeState.IDLE
360
+ ):
361
+ return
362
+ try:
363
+ if await self._runtime.has_background_job(
364
+ runtime_session,
365
+ timeout=self._timeout_budget.provider_call_seconds,
366
+ ):
367
+ return
368
+ except asyncio.CancelledError:
369
+ raise
370
+ except Exception:
371
+ self._logger.exception("runtime background job check failed")
372
+ await self._start_runtime_timer(runtime_session)
373
+
397
374
  async def _forward_runtime_session_expiry(
398
375
  self,
399
376
  runtime_session: RuntimeSession,
@@ -567,9 +544,7 @@ class SessionOrchestrator(IAsyncLifecycle):
567
544
  self._ingress_loop(ingress_queue),
568
545
  name=f"bcn-ingress-{message.channel_session_id}",
569
546
  )
570
- ingress_queue.put_nowait(
571
- _IngressItem(message, completion, self._timer_wheel.now_ms)
572
- )
547
+ ingress_queue.put_nowait(_IngressItem(message, completion))
573
548
  return await completion
574
549
 
575
550
  async def _ingress_loop(self, queue: asyncio.Queue[_IngressItem]) -> None:
@@ -590,24 +565,15 @@ class SessionOrchestrator(IAsyncLifecycle):
590
565
  or self.runtime_session(session_id) is not None
591
566
  ):
592
567
  runtime_queue = self._runtime_queue_for_session(session_id)
593
- if runtime_queue is None:
568
+ if runtime_queue is None or not message.notifies_runtime:
594
569
  if not item.completion.done():
595
570
  item.completion.set_result(None)
596
- elif message.notifies_runtime:
571
+ else:
597
572
  runtime_queue.put_nowait(
598
573
  _RuntimeNotification(
599
574
  message,
600
575
  context,
601
576
  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
577
  )
612
578
  )
613
579
  except asyncio.CancelledError:
@@ -628,22 +594,6 @@ class SessionOrchestrator(IAsyncLifecycle):
628
594
  pending: list[_RuntimeQueueItem] = []
629
595
  while True:
630
596
  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
597
  if isinstance(item, _RuntimeExpiry):
648
598
  try:
649
599
  await self._handle_runtime_expiry(
@@ -679,15 +629,9 @@ class SessionOrchestrator(IAsyncLifecycle):
679
629
  pending.insert(0, candidate)
680
630
  break
681
631
  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
- )
632
+ runtime_session = self.runtime_session(batch[0].context.bcn_session.id)
633
+ if runtime_session is not None:
634
+ await self._cancel_runtime_timer(runtime_session)
691
635
  turn_task = asyncio.create_task(
692
636
  self._run_notification(batch[0]),
693
637
  name=f"bcn-turn-{batch[0].context.bcn_session.id}",
@@ -711,24 +655,9 @@ class SessionOrchestrator(IAsyncLifecycle):
711
655
  queued_item.context.bcn_session.id
712
656
  )
713
657
  if runtime_session is not None:
714
- await self._refresh_runtime_timer(
715
- runtime_session,
716
- queued_item.activity_at_ms,
717
- )
658
+ await self._cancel_runtime_timer(runtime_session)
718
659
  pending.append(queued_item)
719
660
  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
661
  elif isinstance(queued_item, _RuntimeExpiry):
733
662
  await self._handle_runtime_expiry(
734
663
  queued_item,
@@ -749,6 +678,9 @@ class SessionOrchestrator(IAsyncLifecycle):
749
678
  queue_item_consumed = False
750
679
 
751
680
  result = turn_task.result()
681
+ await self._start_runtime_timer_if_idle(
682
+ batch[0].context.bcn_session.id,
683
+ )
752
684
  route_message = (
753
685
  batch[0].message
754
686
  if isinstance(batch[0], _RuntimeNotification)
@@ -784,7 +716,7 @@ class SessionOrchestrator(IAsyncLifecycle):
784
716
  queue_item_consumed = True
785
717
  for queued_item in (*batch, *pending):
786
718
  if (
787
- isinstance(queued_item, _RuntimeNotification | _RuntimeActivity)
719
+ isinstance(queued_item, _RuntimeNotification)
788
720
  and not queued_item.completion.done()
789
721
  ):
790
722
  queued_item.completion.cancel()
@@ -795,6 +727,9 @@ class SessionOrchestrator(IAsyncLifecycle):
795
727
  except Exception as error:
796
728
  turn_task.cancel()
797
729
  await asyncio.gather(turn_task, return_exceptions=True)
730
+ await self._start_runtime_timer_if_idle(
731
+ batch[0].context.bcn_session.id,
732
+ )
798
733
  for notification in batch:
799
734
  if (
800
735
  isinstance(notification, _RuntimeNotification)
@@ -1205,16 +1140,6 @@ class SessionOrchestrator(IAsyncLifecycle):
1205
1140
  self._state_machine.get(context.bcn_session.id)
1206
1141
  is SessionRuntimeState.IDLE
1207
1142
  ):
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
1143
  break
1219
1144
  if establishment_attempt == 0:
1220
1145
  await self._discard_runtime_session(context.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]: ...