bazaar-compute-node 0.1.15__tar.gz → 0.1.17__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.15 → bazaar_compute_node-0.1.17}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/bcc.py +4 -16
  5. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/runtime.py +10 -26
  6. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/channel.py +27 -11
  7. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/wecom/channel.py +1 -1
  8. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/instruction.py +6 -6
  9. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/session.py +85 -26
  10. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/LICENSE +0 -0
  11. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/README.md +0 -0
  12. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/__init__.py +0 -0
  13. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/__init__.py +0 -0
  14. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/agent.py +0 -0
  15. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/agent_management.py +0 -0
  16. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/application.py +0 -0
  17. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/attachments.py +0 -0
  18. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/command.py +0 -0
  19. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/config.py +0 -0
  20. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/registry.py +0 -0
  21. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/reminder_dispatch.py +0 -0
  22. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/system_service.py +0 -0
  23. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/transport.py +0 -0
  24. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  25. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/app/wrapper.py +0 -0
  26. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/cli.py +0 -0
  27. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  28. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  29. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  30. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  31. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  32. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  33. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  34. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  35. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  36. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  37. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  38. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  39. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +0 -0
  40. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  41. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  42. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  43. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  44. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  45. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +0 -0
  46. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
  47. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +0 -0
  48. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  49. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +0 -0
  50. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  51. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  52. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  53. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  54. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  55. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  56. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  57. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  58. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  59. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  60. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  61. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  62. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/__init__.py +0 -0
  63. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/approval.py +0 -0
  64. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/audit.py +0 -0
  65. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/channel.py +0 -0
  66. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/client.py +0 -0
  67. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/command.py +0 -0
  68. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/concurrency.py +0 -0
  69. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/correlation.py +0 -0
  70. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  71. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  72. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/models/entities.py +0 -0
  73. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  74. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  75. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/models/states.py +0 -0
  76. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/observability.py +0 -0
  77. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  78. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  79. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  80. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  81. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  82. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  83. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  84. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  85. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/outcomes.py +0 -0
  86. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/paths.py +0 -0
  87. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/reminder.py +0 -0
  88. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  89. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/runtime.py +0 -0
  90. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/storage.py +0 -0
  91. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  92. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  93. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  94. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/src/bazaar_compute_node/i18n/english.py +0 -0
  95. {bazaar_compute_node-0.1.15 → bazaar_compute_node-0.1.17}/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.15
3
+ Version: 0.1.17
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.15"
3
+ version = "0.1.17"
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.15"
3
+ version = "0.1.17"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -483,24 +483,12 @@ def _message_header_fields(
483
483
  sender_value = message.get("sender")
484
484
  sender: str | None = None
485
485
  if sender_value is not None:
486
- if isinstance(sender_value, str) and sender_value:
487
- sender = sender_value
488
- elif not isinstance(sender_value, Mapping):
486
+ if not isinstance(sender_value, Mapping):
489
487
  _invalid_response("command response contains an invalid message sender")
490
488
  else:
491
489
  sender_id = sender_value.get("id")
492
490
  sender_name = sender_value.get("name")
493
- if sender_id is not None and (
494
- not isinstance(sender_id, str) or not sender_id
495
- ):
496
- _invalid_response("command response contains an invalid sender id")
497
- if sender_name is not None and (
498
- not isinstance(sender_name, str) or not sender_name
499
- ):
500
- _invalid_response("command response contains an invalid sender name")
501
- sender = sender_name or sender_id
502
- if sender is None:
503
- _invalid_response("command response contains an empty message sender")
491
+ sender = f"@{sender_id}({sender_name})" if sender_name else f"@{sender_id}"
504
492
  return (
505
493
  target,
506
494
  message_id,
@@ -553,7 +541,7 @@ def _format_check_message(message: Mapping[str, object]) -> str:
553
541
  line += f" reply_to={reply_to_message_id}"
554
542
  line += "] "
555
543
  if sender is not None:
556
- line += f"@{sender}: "
544
+ line += f"{sender} "
557
545
  return line + body + _attachment_suffix(message)
558
546
 
559
547
 
@@ -583,7 +571,7 @@ def _format_read_message(
583
571
  fields.append(f"replyTo={reply_to_message_id}")
584
572
  line = f"[{index}/{count} {' '.join(fields)}] "
585
573
  if sender is not None:
586
- line += f"@{sender}: "
574
+ line += f"{sender} "
587
575
  return line + body + _attachment_suffix(message)
588
576
 
589
577
 
@@ -104,7 +104,6 @@ class Runtime(IRuntime, IAsyncLifecycle):
104
104
  self._model = model
105
105
  self._effort = effort
106
106
  self._connections: dict[str, _Connection] = {}
107
- self._teardown_tasks: set[asyncio.Task[None]] = set()
108
107
  self._expire_events: asyncio.Queue[RuntimeExpire] = asyncio.Queue()
109
108
  self._logger = logging.getLogger("bazaar_compute_node.runtime.codex")
110
109
  self._started = False
@@ -114,6 +113,11 @@ class Runtime(IRuntime, IAsyncLifecycle):
114
113
  del timeout
115
114
  if self._stopping:
116
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
+ )
117
121
  self._started = True
118
122
 
119
123
  async def stop(self, *, timeout: float) -> None:
@@ -524,6 +528,9 @@ class Runtime(IRuntime, IAsyncLifecycle):
524
528
  *,
525
529
  timeout: float,
526
530
  ) -> bool:
531
+ if os.name == "nt":
532
+ # Detached OS processes are not exposed by the Windows Codex provider.
533
+ return True
527
534
  connection = self._connections.get(session.id)
528
535
  provider_thread_id = session.provider_thread_id
529
536
  if connection is None or not provider_thread_id:
@@ -549,11 +556,10 @@ class Runtime(IRuntime, IAsyncLifecycle):
549
556
  status=ProviderCallStatus.CONFIRMED,
550
557
  value=session,
551
558
  )
552
- self._schedule_connection_teardown(connection, timeout=timeout)
559
+ await self._stop_connection(connection, timeout=timeout)
553
560
  return ProviderCallResult(
554
- status=ProviderCallStatus.QUEUED,
561
+ status=ProviderCallStatus.CONFIRMED,
555
562
  value=session,
556
- receipt={"teardown": "scheduled"},
557
563
  )
558
564
 
559
565
  async def _open_connection(
@@ -660,28 +666,6 @@ class Runtime(IRuntime, IAsyncLifecycle):
660
666
  except OSError, TimeoutError, JsonlTransportError:
661
667
  return
662
668
 
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
-
685
669
  def _clear_active_turn(self, session_id: str, turn_id: str) -> None:
686
670
  connection = self._connections.get(session_id)
687
671
  if connection is not None and connection.active_turn_id == turn_id:
@@ -67,6 +67,7 @@ class TelegramChannel(IChannel):
67
67
  self._api: TelegramBotApi | None = None
68
68
  self._bot_id: int | None = None
69
69
  self._bot_username: str | None = None
70
+ self._bot_first_name: str | None = None
70
71
  self._state = "stopped"
71
72
  self._poll_attempts = 0
72
73
  self._last_poll_at_ms: int | None = None
@@ -102,6 +103,7 @@ class TelegramChannel(IChannel):
102
103
  "started_at_s": self._started_at_s,
103
104
  "bot_id": self._bot_id,
104
105
  "bot_username": self._bot_username,
106
+ "bot_first_name": self._bot_first_name,
105
107
  "poll_timeout_seconds": 50,
106
108
  "poll_attempts": self._poll_attempts,
107
109
  "last_poll_at_ms": self._last_poll_at_ms,
@@ -127,9 +129,15 @@ class TelegramChannel(IChannel):
127
129
  def get_identity(self) -> ChannelIdentity | None:
128
130
  bot_id = self._bot_id
129
131
  bot_username = self._bot_username
130
- if bot_id is None or bot_username is None:
132
+ bot_first_name = self._bot_first_name
133
+ if bot_id is None or bot_first_name is None:
131
134
  return None
132
- return ChannelIdentity(id=str(bot_id), name=bot_username)
135
+ return ChannelIdentity(
136
+ id=str(bot_id),
137
+ name=(
138
+ f"{bot_username}({bot_first_name})" if bot_username else bot_first_name
139
+ ),
140
+ )
133
141
 
134
142
  async def start(self, *, timeout: float) -> None:
135
143
  if self._runner is not None:
@@ -156,16 +164,22 @@ class TelegramChannel(IChannel):
156
164
  bot_id = bot.get("id")
157
165
  is_bot = bot.get("is_bot")
158
166
  username = bot.get("username")
167
+ first_name = bot.get("first_name")
159
168
  if (
160
169
  not isinstance(bot_id, int)
161
170
  or isinstance(bot_id, bool)
162
171
  or is_bot is not True
163
- or not isinstance(username, str)
164
- or not username
172
+ or (
173
+ username is not None
174
+ and (not isinstance(username, str) or not username)
175
+ )
176
+ or not isinstance(first_name, str)
177
+ or not first_name
165
178
  ):
166
179
  raise ValueError("Telegram getMe returned an invalid bot identity")
167
180
  self._bot_id = bot_id
168
- self._bot_username = username
181
+ self._bot_username = username if isinstance(username, str) else None
182
+ self._bot_first_name = first_name
169
183
  self._typing_runner = asyncio.create_task(
170
184
  self._run_typing_dispatcher(),
171
185
  name="bcn-telegram-typing",
@@ -195,6 +209,7 @@ class TelegramChannel(IChannel):
195
209
  self._session = None
196
210
  self._bot_id = None
197
211
  self._bot_username = None
212
+ self._bot_first_name = None
198
213
  self._typing_leases.clear()
199
214
  await session.close()
200
215
  self._state = "stopped"
@@ -227,6 +242,7 @@ class TelegramChannel(IChannel):
227
242
  self._session = None
228
243
  self._bot_id = None
229
244
  self._bot_username = None
245
+ self._bot_first_name = None
230
246
  if session is not None and not session.closed:
231
247
  await session.close()
232
248
  self._ready.clear()
@@ -462,8 +478,8 @@ class TelegramChannel(IChannel):
462
478
  update_id: int,
463
479
  ) -> None:
464
480
  bot_id = self._bot_id
465
- bot_username = self._bot_username
466
- if bot_id is None or bot_username is None:
481
+ bot_username = self._bot_username or ""
482
+ if bot_id is None:
467
483
  raise RuntimeError("Telegram bot identity is not initialized")
468
484
 
469
485
  provider_sender = message.get("from")
@@ -631,8 +647,8 @@ class TelegramChannel(IChannel):
631
647
  if topic_id is None or topic_id != identity.topic_id:
632
648
  return None
633
649
  bot_id = self._bot_id
634
- bot_username = self._bot_username
635
- if bot_id is None or bot_username is None:
650
+ bot_username = self._bot_username or ""
651
+ if bot_id is None:
636
652
  raise RuntimeError("Telegram bot identity is not initialized")
637
653
  content = await self._content(
638
654
  reply,
@@ -826,11 +842,11 @@ class TelegramChannel(IChannel):
826
842
  if entity_text is None:
827
843
  continue
828
844
  if entity_type == "mention":
829
- if entity_text.casefold() == f"@{username}":
845
+ if username and entity_text.casefold() == f"@{username}":
830
846
  return True
831
847
  continue
832
848
  _, separator, target = entity_text.rpartition("@")
833
- if separator and target.casefold() == username:
849
+ if separator and username and target.casefold() == username:
834
850
  return True
835
851
  return False
836
852
 
@@ -136,7 +136,7 @@ class WeComChannel(IChannel):
136
136
  }
137
137
 
138
138
  def get_identity(self) -> ChannelIdentity | None:
139
- return None
139
+ return ChannelIdentity(id=self._bot_id)
140
140
 
141
141
  async def start(self, *, timeout: float) -> None:
142
142
  if self._runner is not None:
@@ -71,10 +71,10 @@ Do not obstruct a human-directed use of a credential: use or send it on the requ
71
71
  Messages you receive have a single RFC 5424-style structured data header followed by the sender and content:
72
72
 
73
73
  ```
74
- [target=<thread-target> msg=00000000 time=2026-03-15T01:00:00 type=human] @richard: hello everyone
75
- [target=<thread-target> msg=11111111 time=2026-03-15T01:00:01 type=agent] @Alice: hi there
76
- [target=dm:@richard msg=22222222 time=2026-03-15T01:00:02 type=human] @richard: hey, can you help?
77
- [target=<thread-target> msg=33333333 time=2026-03-15T01:00:03 type=human] @richard: thread reply
74
+ [target=<thread-target> msg=00000000 time=2026-03-15T01:00:00 type=human] @yuchanns(Hanchin Hsieh) hello everyone
75
+ [target=<thread-target> msg=11111111 time=2026-03-15T01:00:01 type=agent] @Alice(Aeris) hi there
76
+ [target=dm:@yuchanss msg=22222222 time=2026-03-15T01:00:02 type=human] @yuchanns(Hanchin Hsieh) hey, can you help?
77
+ [target=<thread-target> msg=33333333 time=2026-03-15T01:00:03 type=human] @yuchanns(Hanchin Hsieh) thread reply
78
78
  ```
79
79
 
80
80
  Prompt examples use obvious placeholder IDs such as `00000000`, `11111111`, and `22222222`. They show the shape of a real message ID but are not actual messages. Do not cite them as evidence; use only IDs from messages you actually received or read.
@@ -257,9 +257,9 @@ class DeveloperInstructionContext:
257
257
  def render(self) -> str:
258
258
  rendered = DEVELOPER_INSTRUCTIONS
259
259
  identity_statement = (
260
- f'You\'re "{self.bot_name}", A.K.A "{self.agent_name}"'
260
+ f"You're {self.bot_name}, A.K.A {self.agent_name}"
261
261
  if self.bot_name is not None
262
- else f'You\'re "{self.agent_name}"'
262
+ else f"You're {self.agent_name}"
263
263
  )
264
264
  for placeholder, value in (
265
265
  ("{{identity_statement}}", identity_statement),
@@ -204,6 +204,7 @@ class SessionOrchestrator(IAsyncLifecycle):
204
204
  clock=self._clock,
205
205
  )
206
206
  self._active_tasks: set[asyncio.Task[RuntimeTurn | None]] = set()
207
+ self._runtime_teardown_tasks: set[asyncio.Task[None]] = set()
207
208
  self._ingress_queues: dict[tuple[str, str], asyncio.Queue[_IngressItem]] = {}
208
209
  self._ingress_workers: dict[tuple[str, str], asyncio.Task[None]] = {}
209
210
  self._runtime_queues: dict[str, asyncio.Queue[_RuntimeQueueItem]] = {}
@@ -408,7 +409,7 @@ class SessionOrchestrator(IAsyncLifecycle):
408
409
  )
409
410
  except BaseException:
410
411
  try:
411
- await self._runtime.stop_session(
412
+ await self._stop_runtime_session(
412
413
  context.runtime_session,
413
414
  timeout=self._timeout_budget.provider_call_seconds,
414
415
  )
@@ -497,6 +498,8 @@ class SessionOrchestrator(IAsyncLifecycle):
497
498
  for runtime_session in tuple(self._runtime_sessions.values()):
498
499
  await self._stop_runtime_session(runtime_session, timeout=timeout)
499
500
 
501
+ await self._wait_for_runtime_teardown_tasks(timeout=timeout)
502
+
500
503
  try:
501
504
  await self._runtime.stop(timeout=timeout)
502
505
  except asyncio.CancelledError:
@@ -1515,38 +1518,45 @@ class SessionOrchestrator(IAsyncLifecycle):
1515
1518
  async with self._concurrency.for_session(runtime_session.bcn_session_id):
1516
1519
  await self._stop_runtime_session_locked(runtime_session, timeout=timeout)
1517
1520
 
1518
- 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(
1519
1532
  self,
1520
1533
  runtime_session: RuntimeSession,
1521
1534
  *,
1535
+ correlation: CorrelationContext,
1522
1536
  timeout: float,
1523
1537
  ) -> None:
1524
- process_correlation = CorrelationContext(
1525
- node_id=self.agent_id,
1526
- channel_session_id=runtime_session.channel_session_id,
1527
- bcn_session_id=runtime_session.bcn_session_id,
1528
- runtime_session_id=runtime_session.id,
1529
- provider_thread_id=runtime_session.provider_thread_id,
1530
- )
1531
- await self._audit.append(
1532
- event_name="runtime.process.stop.requested",
1533
- state=RuntimeEventState.STARTED,
1534
- correlation=process_correlation,
1535
- metadata={
1536
- "runtime": runtime_session.runtime,
1537
- "workspace_id": runtime_session.workspace_id,
1538
- },
1539
- )
1540
- await self._state_machine.apply_locked(
1541
- runtime_session.bcn_session_id,
1542
- SessionRuntimeObservation(
1543
- source=SessionRuntimeObservationSource.SESSION,
1544
- signal=SessionRuntimeSignal.STOP_REQUESTED,
1545
- 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,
1546
1543
  ),
1544
+ name=f"bcn-runtime-teardown-{runtime_session.id}",
1547
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:
1548
1556
  try:
1549
1557
  result = await self._runtime.stop_session(runtime_session, timeout=timeout)
1558
+ except asyncio.CancelledError:
1559
+ raise
1550
1560
  except Exception as error: # noqa: BLE001
1551
1561
  result = None
1552
1562
  stop_error = error
@@ -1563,7 +1573,6 @@ class SessionOrchestrator(IAsyncLifecycle):
1563
1573
  else ErrorKind.PROVIDER_UNKNOWN.value
1564
1574
  )
1565
1575
  error_message = result.error_message if result is not None else str(stop_error)
1566
- await self._discard_runtime_session(runtime_session)
1567
1576
  await self._audit.append(
1568
1577
  event_name=(
1569
1578
  "runtime.process.stop.completed"
@@ -1579,7 +1588,7 @@ class SessionOrchestrator(IAsyncLifecycle):
1579
1588
  if unknown
1580
1589
  else RuntimeEventState.FAILED
1581
1590
  ),
1582
- correlation=process_correlation,
1591
+ correlation=correlation,
1583
1592
  error_kind=(
1584
1593
  ErrorKind(error_kind)
1585
1594
  if error_kind in ErrorKind._value2member_map_
@@ -1595,3 +1604,53 @@ class SessionOrchestrator(IAsyncLifecycle):
1595
1604
  "workspace_id": runtime_session.workspace_id,
1596
1605
  },
1597
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)