bazaar-compute-node 0.1.27__tar.gz → 0.1.28__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 (137) hide show
  1. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/PKG-INFO +2 -1
  2. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/pyproject.toml +2 -1
  3. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/pyproject.toml.orig +2 -1
  4. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/command.py +50 -20
  5. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/bcc.py +9 -0
  6. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/api.py +12 -0
  7. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/approval.py +7 -3
  8. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/channel.py +127 -2
  9. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/codec.py +8 -0
  10. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/registry.py +2 -0
  11. bazaar_compute_node-0.1.28/src/bazaar_compute_node/contrib/sqlite/migrations/v22_readable_targets.py +20 -0
  12. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/repository/facade.py +19 -17
  13. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/repository/messages.py +56 -14
  14. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/repository/sessions.py +13 -3
  15. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/approval.py +12 -9
  16. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/channel.py +69 -0
  17. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/command.py +27 -4
  18. bazaar_compute_node-0.1.28/src/bazaar_compute_node/core/instruction.py +52 -0
  19. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/models/__init__.py +2 -0
  20. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/models/entities.py +51 -0
  21. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/command.py +33 -30
  22. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/storage.py +94 -19
  23. bazaar_compute_node-0.1.28/src/bazaar_compute_node/i18n/catalog.py +79 -0
  24. bazaar_compute_node-0.1.28/src/bazaar_compute_node/rendering.py +86 -0
  25. bazaar_compute_node-0.1.27/src/bazaar_compute_node/core/instruction.py → bazaar_compute_node-0.1.28/src/bazaar_compute_node/resources/developer_instructions.md +11 -68
  26. bazaar_compute_node-0.1.28/src/bazaar_compute_node/resources/locales/en.toml +49 -0
  27. bazaar_compute_node-0.1.28/src/bazaar_compute_node/resources/locales/zh-CN.toml +49 -0
  28. bazaar_compute_node-0.1.27/src/bazaar_compute_node/i18n/catalog.py +0 -46
  29. bazaar_compute_node-0.1.27/src/bazaar_compute_node/i18n/english.py +0 -87
  30. bazaar_compute_node-0.1.27/src/bazaar_compute_node/i18n/schinese.py +0 -73
  31. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/LICENSE +0 -0
  32. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/README.md +0 -0
  33. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/__init__.py +0 -0
  34. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/__init__.py +0 -0
  35. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/agent.py +0 -0
  36. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/agent_management.py +0 -0
  37. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/application.py +0 -0
  38. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/attachments.py +0 -0
  39. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/config.py +0 -0
  40. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/registry.py +0 -0
  41. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/resource_dispatch.py +0 -0
  42. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/system_service.py +0 -0
  43. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/transport.py +0 -0
  44. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  45. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/app/wrapper.py +0 -0
  46. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/cli.py +0 -0
  47. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  48. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  49. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  50. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  51. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  52. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  53. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  54. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  55. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  56. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/__init__.py +0 -0
  57. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/attachments.py +0 -0
  58. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/frame.py +0 -0
  59. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/identity.py +0 -0
  60. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/outbound.py +0 -0
  61. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/plugin.py +0 -0
  62. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/lark/transport.py +0 -0
  63. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  64. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  65. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  66. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  67. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  68. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/executor.py +0 -0
  69. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/__init__.py +0 -0
  70. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/model.py +0 -0
  71. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v01_initial_node_schema.py +0 -0
  72. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v02_session_mapping_indexes.py +0 -0
  73. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v03_message_log_indexes.py +0 -0
  74. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v04_runtime_attempt_facts.py +0 -0
  75. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v05_inbound_message_references.py +0 -0
  76. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v06_inbound_message_reference_integrity.py +0 -0
  77. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v07_inbound_provider_identity.py +0 -0
  78. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v08_transient_stream_events.py +0 -0
  79. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v09_remove_runtime_events.py +0 -0
  80. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v10_add_outbound_attachments.py +0 -0
  81. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v11_remove_runtime_session_mapping.py +0 -0
  82. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v12_add_reminders.py +0 -0
  83. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v13_add_agent_ownership.py +0 -0
  84. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v14_add_inbox_discovery_indexes.py +0 -0
  85. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v15_add_handoffs.py +0 -0
  86. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v16_remove_outbound_drafts.py +0 -0
  87. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v17_remove_agent_identity_triggers.py +0 -0
  88. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v18_unify_messages.py +0 -0
  89. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v19_reminder_system_messages.py +0 -0
  90. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v20_remove_freshness_persistence.py +0 -0
  91. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/migrations/v21_remove_handoffs.py +0 -0
  92. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  93. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  94. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/repository/__init__.py +0 -0
  95. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/repository/base.py +0 -0
  96. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/repository/reminders.py +0 -0
  97. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/sqlite/storage.py +0 -0
  98. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  99. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  100. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  101. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  102. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  103. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  104. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  105. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  106. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  107. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  108. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  109. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  110. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/__init__.py +0 -0
  111. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/approval.py +0 -0
  112. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/audit.py +0 -0
  113. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/channel.py +0 -0
  114. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/client.py +0 -0
  115. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/concurrency.py +0 -0
  116. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/correlation.py +0 -0
  117. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/inbox.py +0 -0
  118. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  119. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  120. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  121. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/models/states.py +0 -0
  122. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/observability.py +0 -0
  123. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  124. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  125. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  126. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  127. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  128. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  129. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/session.py +0 -0
  130. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  131. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/outcomes.py +0 -0
  132. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/paths.py +0 -0
  133. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/reminder.py +0 -0
  134. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  135. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/runtime.py +0 -0
  136. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  137. {bazaar_compute_node-0.1.27 → bazaar_compute_node-0.1.28}/src/bazaar_compute_node/i18n/__init__.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.27
3
+ Version: 0.1.28
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
7
7
  Requires-Dist: aiohttp>=3.12.0
8
8
  Requires-Dist: aiosqlite>=0.20.0
9
9
  Requires-Dist: cryptography>=45.0.0
10
+ Requires-Dist: jinja2>=3.1.6
10
11
  Requires-Dist: pydantic>=2.0.0
11
12
  Requires-Dist: protobug==1.0.0
12
13
  Requires-Dist: tzdata>=2024.1 ; sys_platform == 'win32'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.27"
3
+ version = "0.1.28"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "aiohttp>=3.12.0",
11
11
  "aiosqlite>=0.20.0",
12
12
  "cryptography>=45.0.0",
13
+ "jinja2>=3.1.6",
13
14
  "pydantic>=2.0.0",
14
15
  "protobug==1.0.0",
15
16
  "tzdata>=2024.1; sys_platform == 'win32'",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.27"
3
+ version = "0.1.28"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "aiohttp>=3.12.0",
11
11
  "aiosqlite>=0.20.0",
12
12
  "cryptography>=45.0.0",
13
+ "jinja2>=3.1.6",
13
14
  "pydantic>=2.0.0",
14
15
  "protobug==1.0.0",
15
16
  "tzdata>=2024.1; sys_platform == 'win32'",
@@ -21,7 +21,9 @@ from ..core.command import (
21
21
  ICommandService,
22
22
  InboxListResult,
23
23
  MessageSendFreshnessHold,
24
+ MessageSendSuccess,
24
25
  SessionNotFoundError,
26
+ TargetProjection,
25
27
  )
26
28
  from ..core.lifecycle import TimeoutBudget
27
29
  from ..core.models import (
@@ -62,7 +64,14 @@ def _serialize_attachment(
62
64
  }
63
65
 
64
66
 
65
- def serialize_message(message: Message) -> dict[str, object]:
67
+ def serialize_message(
68
+ message: Message,
69
+ target_projections: tuple[TargetProjection, ...] = (),
70
+ ) -> dict[str, object]:
71
+ targets = {
72
+ projection.canonical_target: projection.display_target
73
+ for projection in target_projections
74
+ }
66
75
  return {
67
76
  "seq": message.seq,
68
77
  "message_id": message.message_id,
@@ -84,13 +93,19 @@ def serialize_message(message: Message) -> dict[str, object]:
84
93
  if message.system_message_kind is None
85
94
  else message.system_message_kind.value
86
95
  ),
87
- "system_message_source_target": message.metadata.get(
88
- "system_message_source_target"
96
+ "system_message_source_target": (
97
+ targets.get(source_target, source_target)
98
+ if isinstance(
99
+ source_target := message.metadata.get("system_message_source_target"),
100
+ str,
101
+ )
102
+ else None
89
103
  ),
90
104
  "system_message_source_message_id": message.metadata.get(
91
105
  "system_message_source_message_id"
92
106
  ),
93
107
  "message_type": message.message_type,
108
+ "target": targets.get(message.target, message.target),
94
109
  "canonical_target": message.target,
95
110
  "target_kind": message.target_kind.value,
96
111
  "mentions_agent": message.mentions_agent,
@@ -447,10 +462,11 @@ class CommandDispatcher:
447
462
  "ok": True,
448
463
  "result": {
449
464
  "messages": [
450
- serialize_message(message) for message in result.messages
465
+ serialize_message(message, result.target_projections)
466
+ for message in result.messages
451
467
  ],
452
468
  "referenced_messages": [
453
- serialize_message(message)
469
+ serialize_message(message, result.target_projections)
454
470
  for message in result.referenced_messages
455
471
  ],
456
472
  "snapshot_seq": result.snapshot_seq,
@@ -471,7 +487,7 @@ class CommandDispatcher:
471
487
  request = cast(_MessageReadRequest, request)
472
488
  result = await self._service.read(
473
489
  session_id,
474
- target=request.target,
490
+ raw_target=request.target,
475
491
  around_message_id=request.around_message_id,
476
492
  limit=request.limit,
477
493
  )
@@ -479,10 +495,11 @@ class CommandDispatcher:
479
495
  "ok": True,
480
496
  "result": {
481
497
  "messages": [
482
- serialize_message(message) for message in result.messages
498
+ serialize_message(message, result.target_projections)
499
+ for message in result.messages
483
500
  ],
484
501
  "referenced_messages": [
485
- serialize_message(message)
502
+ serialize_message(message, result.target_projections)
486
503
  for message in result.referenced_messages
487
504
  ],
488
505
  "snapshot_seq": result.snapshot_seq,
@@ -496,7 +513,7 @@ class CommandDispatcher:
496
513
  result = await self._service.send(
497
514
  session_id=session_id,
498
515
  command_id=request.command_id,
499
- target=request.target,
516
+ raw_target=request.target,
500
517
  body=request.body,
501
518
  created_at_ms=request.created_at_ms,
502
519
  attachment_paths=tuple(request.attachment_paths),
@@ -508,22 +525,25 @@ class CommandDispatcher:
508
525
  "ok": True,
509
526
  "result": {"text": format_freshness_hold(result)},
510
527
  }
511
- delivery_state = result.delivery_state
528
+ if not isinstance(result, MessageSendSuccess):
529
+ raise AssertionError("message send returned an unsupported result")
530
+ message = result.message
531
+ delivery_state = message.delivery_state
512
532
  if delivery_state is None:
513
533
  raise RuntimeError("outbound message has no delivery state")
514
534
  if delivery_state is OutboundDeliveryState.SENT:
515
535
  text = (
516
- f"Message sent to {result.target}. Message ID: {result.message_id}"
536
+ f"Message sent to {result.target}. Message ID: {message.message_id}"
517
537
  )
518
538
  elif delivery_state is OutboundDeliveryState.QUEUED:
519
539
  text = (
520
540
  f"Message queued to {result.target}. "
521
- f"Message ID: {result.message_id}"
541
+ f"Message ID: {message.message_id}"
522
542
  )
523
543
  elif delivery_state is OutboundDeliveryState.PARTIAL:
524
544
  raise CommandDispatchError(
525
545
  "SEND_PARTIAL",
526
- result.error_message
546
+ message.error_message
527
547
  or "Message delivery was only partially confirmed.",
528
548
  next_action=(
529
549
  "Do not retry the complete message automatically; reconcile "
@@ -533,13 +553,13 @@ class CommandDispatcher:
533
553
  elif delivery_state is OutboundDeliveryState.UNKNOWN:
534
554
  raise CommandDispatchError(
535
555
  "SEND_UNKNOWN",
536
- result.error_message or "Message delivery outcome is unknown.",
556
+ message.error_message or "Message delivery outcome is unknown.",
537
557
  next_action="Reconcile channel delivery before retrying.",
538
558
  )
539
559
  elif delivery_state is OutboundDeliveryState.FAILED:
540
560
  raise CommandDispatchError(
541
561
  "SEND_FAILED",
542
- result.error_message or "Message delivery failed.",
562
+ message.error_message or "Message delivery failed.",
543
563
  next_action="Fix the provider error before retrying.",
544
564
  )
545
565
  else:
@@ -553,8 +573,14 @@ class CommandDispatcher:
553
573
 
554
574
  if resource == "thread" and command == "unfollow":
555
575
  request = cast(_ThreadUnfollowRequest, request)
556
- changed = await self._service.unfollow(session_id, target=request.target)
557
- return {"ok": True, "result": {"changed": changed}}
576
+ result = await self._service.unfollow(
577
+ session_id,
578
+ raw_target=request.target,
579
+ )
580
+ return {
581
+ "ok": True,
582
+ "result": {"target": result.target, "changed": result.changed},
583
+ }
558
584
 
559
585
  raise AssertionError("validated command route has no handler")
560
586
 
@@ -578,7 +604,7 @@ def _message_timestamp(message: Mapping[str, object]) -> int:
578
604
  def _message_header_fields(
579
605
  message: Mapping[str, object],
580
606
  ) -> tuple[str, str, str, str, str | None, str]:
581
- target = cast(str, message["canonical_target"])
607
+ target = cast(str, message["target"])
582
608
  message_id = cast(str, message["message_id"])
583
609
  sender_kind = cast(str, message["sender_kind"])
584
610
  sender_value = message["sender"]
@@ -694,9 +720,13 @@ def format_read_message(
694
720
 
695
721
 
696
722
  def format_freshness_hold(result: MessageSendFreshnessHold) -> str:
697
- messages = [serialize_message(message) for message in result.messages]
723
+ messages = [
724
+ serialize_message(message, result.target_projections)
725
+ for message in result.messages
726
+ ]
698
727
  referenced_messages = [
699
- serialize_message(message) for message in result.referenced_messages
728
+ serialize_message(message, result.target_projections)
729
+ for message in result.referenced_messages
700
730
  ]
701
731
  shown = len(messages)
702
732
  total = result.newer_message_total
@@ -578,6 +578,13 @@ def serialize_send(result: Mapping[str, object]) -> str:
578
578
  ) from error
579
579
 
580
580
 
581
+ def serialize_unfollow(result: Mapping[str, object]) -> str:
582
+ target = cast(str, result["target"])
583
+ if cast(bool, result["changed"]):
584
+ return f"Thread unfollowed: {target}"
585
+ return f"Thread was already unfollowed: {target}"
586
+
587
+
581
588
  def _reminder(result: Mapping[str, object]) -> Mapping[str, object]:
582
589
  return cast(Mapping[str, object], result["reminder"])
583
590
 
@@ -694,6 +701,8 @@ async def async_main(argv: Sequence[str] | None = None) -> int:
694
701
  "cancel": serialize_reminder_cancel,
695
702
  }[args.command]
696
703
  print(serializer(result))
704
+ elif args.resource == "thread" and args.command == "unfollow":
705
+ print(serialize_unfollow(result))
697
706
  return 0
698
707
 
699
708
 
@@ -378,6 +378,18 @@ class LarkApi:
378
378
  params={"user_id_type": "open_id"},
379
379
  )
380
380
 
381
+ async def get_chat(
382
+ self,
383
+ chat_id: str,
384
+ *,
385
+ timeout: float,
386
+ ) -> Mapping[str, object]:
387
+ return await self._get_json(
388
+ "chat_get",
389
+ f"/open-apis/im/v1/chats/{quote(chat_id, safe='')}",
390
+ timeout=timeout,
391
+ )
392
+
381
393
  async def get_message(
382
394
  self,
383
395
  message_id: str,
@@ -442,9 +442,13 @@ def _approval_card_content(
442
442
  if action_key is not None
443
443
  else request.approval.action.replace("_", " ")
444
444
  )
445
- markdown = translator.text("approval.prompt.action", {"action": action})
446
- if request.approval.description:
447
- markdown = f"{markdown}\n\n{request.approval.description}"
445
+ markdown = translator.text(
446
+ "approval.prompt.lark",
447
+ {
448
+ "action": action,
449
+ "description": request.approval.description,
450
+ },
451
+ )
448
452
  elements: list[dict[str, object]] = [{"tag": "markdown", "content": markdown}]
449
453
  if decision is None:
450
454
  elements.append(
@@ -7,6 +7,7 @@ from collections import OrderedDict
7
7
  from collections.abc import AsyncIterator, Mapping
8
8
  from dataclasses import dataclass
9
9
  from time import monotonic, time_ns
10
+ from unicodedata import category
10
11
 
11
12
  import aiohttp
12
13
 
@@ -22,6 +23,7 @@ from ...core.models import (
22
23
  ApprovalDecision,
23
24
  ApprovalResult,
24
25
  ChannelTargetKind,
26
+ ChannelTargetPresentation,
25
27
  InboundAttachment,
26
28
  Message,
27
29
  MessageDirection,
@@ -47,9 +49,14 @@ from .transport import LarkTransport
47
49
 
48
50
  _STOP = object()
49
51
  _EVENT_TYPE = "im.message.receive_v1"
50
- _CONTACT_CACHE_TTL_SECONDS = 5 * 60
52
+ _CONTACT_CACHE_TTL_SECONDS = 24 * 60 * 60
53
+ _CONTACT_FAILURE_CACHE_TTL_SECONDS = 5 * 60
51
54
  _CONTACT_CACHE_MAX_ENTRIES = 256
52
55
  _CONTACT_TIMEOUT_SECONDS = 5.0
56
+ _CHAT_CACHE_TTL_SECONDS = 24 * 60 * 60
57
+ _CHAT_FAILURE_CACHE_TTL_SECONDS = 5 * 60
58
+ _CHAT_CACHE_MAX_ENTRIES = 256
59
+ _CHAT_TIMEOUT_SECONDS = 5.0
53
60
  _PARENT_TIMEOUT_SECONDS = 10.0
54
61
  _RESOURCE_TIMEOUT_SECONDS = 60.0
55
62
  _TYPING_TIMEOUT_SECONDS = 3.0
@@ -113,6 +120,14 @@ class LarkChannel(IChannel):
113
120
  self._contact_lookup_requests = 0
114
121
  self._contact_lookup_failures = 0
115
122
  self._contact_cache_hits = 0
123
+ self._chat_cache: OrderedDict[
124
+ tuple[str, str, str], tuple[float, str | None]
125
+ ] = OrderedDict()
126
+ self._chat_inflight: dict[tuple[str, str, str], asyncio.Task[str | None]] = {}
127
+ self._chat_lock = asyncio.Lock()
128
+ self._chat_lookup_requests = 0
129
+ self._chat_lookup_failures = 0
130
+ self._chat_cache_hits = 0
116
131
  self._resource_cache = LarkResourceCache(context.attachments)
117
132
  self._resources_materialized = 0
118
133
  self._resource_failures = 0
@@ -162,6 +177,9 @@ class LarkChannel(IChannel):
162
177
  "contact_lookup_requests": self._contact_lookup_requests,
163
178
  "contact_lookup_failures": self._contact_lookup_failures,
164
179
  "contact_cache_hits": self._contact_cache_hits,
180
+ "chat_lookup_requests": self._chat_lookup_requests,
181
+ "chat_lookup_failures": self._chat_lookup_failures,
182
+ "chat_cache_hits": self._chat_cache_hits,
165
183
  "resources_materialized": self._resources_materialized,
166
184
  "resource_failures": self._resource_failures,
167
185
  "last_resource_disposition": self._last_resource_disposition,
@@ -265,6 +283,7 @@ class LarkChannel(IChannel):
265
283
  await self._stop_typing(timeout=timeout)
266
284
  await self._resource_cache.close()
267
285
  await self._close_contact_cache()
286
+ await self._close_chat_cache()
268
287
  if api is not None:
269
288
  self._token_refresh_failures = api.token_refresh_failures
270
289
  await api.stop()
@@ -376,6 +395,14 @@ class LarkChannel(IChannel):
376
395
  chat_id=chat_id,
377
396
  thread_id=thread_id,
378
397
  )
398
+ presentation = None
399
+ if target_kind is ChannelTargetKind.GROUP:
400
+ name = await self._chat_name(
401
+ tenant_key=tenant_key,
402
+ chat_id=chat_id,
403
+ )
404
+ if name is not None:
405
+ presentation = ChannelTargetPresentation(display_name=name)
379
406
  received_at_ms = time_ns() // 1_000_000
380
407
  metadata = {
381
408
  "lark_event_type": _EVENT_TYPE,
@@ -402,6 +429,7 @@ class LarkChannel(IChannel):
402
429
  parent,
403
430
  thread_identity=thread_identity,
404
431
  target_kind=target_kind,
432
+ presentation=presentation,
405
433
  sender_payload=_message_sender(parent),
406
434
  tenant_key=tenant_key,
407
435
  mentions_agent=False,
@@ -426,6 +454,7 @@ class LarkChannel(IChannel):
426
454
  message,
427
455
  thread_identity=thread_identity,
428
456
  target_kind=target_kind,
457
+ presentation=presentation,
429
458
  sender_payload=sender,
430
459
  tenant_key=tenant_key,
431
460
  mentions_agent=target_kind is ChannelTargetKind.GROUP,
@@ -454,6 +483,7 @@ class LarkChannel(IChannel):
454
483
  *,
455
484
  thread_identity: LarkThreadIdentity,
456
485
  target_kind: ChannelTargetKind,
486
+ presentation: ChannelTargetPresentation | None,
457
487
  sender_payload: Mapping[str, object] | None,
458
488
  tenant_key: str,
459
489
  mentions_agent: bool,
@@ -514,6 +544,7 @@ class LarkChannel(IChannel):
514
544
  target=f"{target_kind.value}:{thread_identity.channel_session_id}",
515
545
  body=projection.body,
516
546
  target_kind=target_kind,
547
+ target_presentation=presentation,
517
548
  mentions_agent=mentions_agent,
518
549
  notifies_runtime=notifies_runtime,
519
550
  attachments=attachments,
@@ -637,7 +668,12 @@ class LarkChannel(IChannel):
637
668
  self._contact_inflight.pop(key, None)
638
669
  if completed:
639
670
  self._contact_cache[key] = (
640
- monotonic() + _CONTACT_CACHE_TTL_SECONDS,
671
+ monotonic()
672
+ + (
673
+ _CONTACT_CACHE_TTL_SECONDS
674
+ if name is not None
675
+ else _CONTACT_FAILURE_CACHE_TTL_SECONDS
676
+ ),
641
677
  name,
642
678
  )
643
679
  self._contact_cache.move_to_end(key)
@@ -654,6 +690,95 @@ class LarkChannel(IChannel):
654
690
  if tasks:
655
691
  await asyncio.gather(*tasks, return_exceptions=True)
656
692
 
693
+ async def _chat_name(self, *, tenant_key: str, chat_id: str) -> str | None:
694
+ key = (self._app_id, tenant_key, chat_id)
695
+ now = monotonic()
696
+ async with self._chat_lock:
697
+ cached = self._chat_cache.get(key)
698
+ if cached is not None:
699
+ expires_at, name = cached
700
+ if expires_at > now:
701
+ self._chat_cache_hits += 1
702
+ self._chat_cache.move_to_end(key)
703
+ return name
704
+ self._chat_cache.pop(key, None)
705
+ task = self._chat_inflight.get(key)
706
+ if task is None:
707
+ task = asyncio.create_task(
708
+ self._load_chat_name(key, chat_id),
709
+ name="bcn-lark-chat",
710
+ )
711
+ self._chat_inflight[key] = task
712
+ return await asyncio.shield(task)
713
+
714
+ async def _load_chat_name(
715
+ self,
716
+ key: tuple[str, str, str],
717
+ chat_id: str,
718
+ ) -> str | None:
719
+ name: str | None = None
720
+ completed = False
721
+ try:
722
+ api = self._api
723
+ if api is None:
724
+ completed = True
725
+ return None
726
+ self._chat_lookup_requests += 1
727
+ response = await api.get_chat(chat_id, timeout=_CHAT_TIMEOUT_SECONDS)
728
+ data = response.get("data")
729
+ raw_name = data.get("name") if isinstance(data, Mapping) else None
730
+ if (
731
+ isinstance(raw_name, str)
732
+ and raw_name.strip()
733
+ and "]" not in raw_name
734
+ and not any(category(character) == "Cc" for character in raw_name)
735
+ ):
736
+ name = raw_name
737
+ else:
738
+ self._chat_lookup_failures += 1
739
+ self._observe(
740
+ "lark.chat.lookup_failed",
741
+ error_kind="invalid_name",
742
+ )
743
+ completed = True
744
+ return name
745
+ except asyncio.CancelledError:
746
+ raise
747
+ except Exception as error: # noqa: BLE001
748
+ self._chat_lookup_failures += 1
749
+ self._observe(
750
+ "lark.chat.lookup_failed",
751
+ error_kind=type(error).__name__,
752
+ )
753
+ completed = True
754
+ return None
755
+ finally:
756
+ async with self._chat_lock:
757
+ self._chat_inflight.pop(key, None)
758
+ if completed:
759
+ self._chat_cache[key] = (
760
+ monotonic()
761
+ + (
762
+ _CHAT_CACHE_TTL_SECONDS
763
+ if name is not None
764
+ else _CHAT_FAILURE_CACHE_TTL_SECONDS
765
+ ),
766
+ name,
767
+ )
768
+ self._chat_cache.move_to_end(key)
769
+ while len(self._chat_cache) > _CHAT_CACHE_MAX_ENTRIES:
770
+ self._chat_cache.popitem(last=False)
771
+
772
+ async def _close_chat_cache(self) -> None:
773
+ async with self._chat_lock:
774
+ tasks = tuple(self._chat_inflight.values())
775
+ self._chat_inflight.clear()
776
+ self._chat_cache.clear()
777
+ for task in tasks:
778
+ task.cancel()
779
+ if tasks:
780
+ await asyncio.gather(*tasks, return_exceptions=True)
781
+
657
782
  async def _materialize_resources(
658
783
  self,
659
784
  provider_message_id: str,
@@ -45,6 +45,11 @@ def channel_session_from_row(row: aiosqlite.Row) -> ChannelSession:
45
45
  following=bool(following),
46
46
  last_inbound_at_ms=cast(int | None, row["last_inbound_at_ms"]),
47
47
  last_outbound_at_ms=cast(int | None, row["last_outbound_at_ms"]),
48
+ target_display_name=_optional_text(
49
+ row["target_display_name"], "target_display_name"
50
+ ),
51
+ target_handle=_optional_text(row["target_handle"], "target_handle"),
52
+ target_handle_key=_optional_text(row["target_handle_key"], "target_handle_key"),
48
53
  metadata=_decode_metadata(
49
54
  row["provider_identity_ref_json"], "provider_identity_ref_json"
50
55
  ),
@@ -439,6 +444,9 @@ def validate_channel_session_update(
439
444
  following=incoming.following,
440
445
  last_inbound_at_ms=incoming.last_inbound_at_ms,
441
446
  last_outbound_at_ms=incoming.last_outbound_at_ms,
447
+ target_display_name=incoming.target_display_name,
448
+ target_handle=incoming.target_handle,
449
+ target_handle_key=incoming.target_handle_key,
442
450
  metadata=incoming.metadata,
443
451
  )
444
452
 
@@ -32,6 +32,7 @@ from .v20_remove_freshness_persistence import (
32
32
  FRESHNESS_PERSISTENCE_REMOVAL_MIGRATION,
33
33
  )
34
34
  from .v21_remove_handoffs import HANDOFF_REMOVAL_MIGRATION
35
+ from .v22_readable_targets import READABLE_TARGET_MIGRATION
35
36
 
36
37
  if TYPE_CHECKING:
37
38
  from ..executor import SqliteSession
@@ -78,6 +79,7 @@ MIGRATIONS = _migration_ledger(
78
79
  REMINDER_SYSTEM_MESSAGE_MIGRATION,
79
80
  FRESHNESS_PERSISTENCE_REMOVAL_MIGRATION,
80
81
  HANDOFF_REMOVAL_MIGRATION,
82
+ READABLE_TARGET_MIGRATION,
81
83
  )
82
84
 
83
85
 
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from .model import Migration
4
+
5
+ READABLE_TARGET_MIGRATION = Migration(
6
+ version=22,
7
+ name="readable_targets",
8
+ statements=(
9
+ "ALTER TABLE channel_sessions ADD COLUMN target_display_name TEXT",
10
+ "ALTER TABLE channel_sessions ADD COLUMN target_handle TEXT",
11
+ "ALTER TABLE channel_sessions ADD COLUMN target_handle_key TEXT",
12
+ """
13
+ CREATE INDEX idx_channel_sessions_target_handle
14
+ ON channel_sessions (agent_id, target_kind, target_handle_key)
15
+ WHERE target_handle_key IS NOT NULL
16
+ """,
17
+ ),
18
+ )
19
+
20
+ __all__ = ["READABLE_TARGET_MIGRATION"]
@@ -63,18 +63,24 @@ class SqliteRepository(
63
63
  or message.mentions_agent
64
64
  ),
65
65
  )
66
+ if message.target_presentation is not None:
67
+ channel_session = channel_session.with_target_presentation(
68
+ message.target_presentation,
69
+ updated_at_ms=now_ms,
70
+ )
66
71
  await self.save_channel_session(channel_session)
67
- elif (
68
- existing_message is None
69
- and message.mentions_agent
70
- and not channel_session.following
71
- ):
72
- channel_session = replace(
73
- channel_session,
74
- following=True,
75
- updated_at_ms=now_ms,
76
- )
77
- await self.save_channel_session(channel_session)
72
+ elif existing_message is None:
73
+ if message.target_presentation is not None:
74
+ channel_session = channel_session.with_target_presentation(
75
+ message.target_presentation,
76
+ updated_at_ms=now_ms,
77
+ )
78
+ if message.mentions_agent and not channel_session.following:
79
+ channel_session = replace(
80
+ channel_session,
81
+ following=True,
82
+ updated_at_ms=now_ms,
83
+ )
78
84
 
79
85
  bcn_session = await self.find_bcn_session(channel_session.id)
80
86
  bcn_session_created = bcn_session is None
@@ -94,16 +100,12 @@ class SqliteRepository(
94
100
  or channel_session.following
95
101
  or message.mentions_agent
96
102
  )
97
- canonical_target = message.target
98
- if channel_session.id != message.channel_session_id:
99
- canonical_target = (
100
- f"{channel_session.target_kind.value}:{channel_session.id}"
101
- )
102
103
  message = replace(
103
104
  message,
104
105
  session_id=bcn_session.id,
105
106
  channel_session_id=channel_session.id,
106
- target=canonical_target,
107
+ target=channel_session.canonical_target,
108
+ target_presentation=None,
107
109
  notifies_runtime=notifies_runtime,
108
110
  )
109
111