bazaar-compute-node 0.1.20__tar.gz → 0.1.22__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 (105) hide show
  1. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/attachments.py +18 -4
  5. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/bcc.py +12 -19
  6. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/attachments.py +76 -21
  7. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +14 -39
  8. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +15 -39
  9. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/channel.py +3 -1
  10. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -10
  11. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/reminder.py +1 -17
  12. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/storage.py +2 -2
  13. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/LICENSE +0 -0
  14. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/README.md +0 -0
  15. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/__init__.py +0 -0
  16. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/__init__.py +0 -0
  17. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/agent.py +0 -0
  18. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/agent_management.py +0 -0
  19. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/application.py +0 -0
  20. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/command.py +0 -0
  21. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/config.py +0 -0
  22. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/registry.py +0 -0
  23. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/reminder_dispatch.py +0 -0
  24. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/system_service.py +0 -0
  25. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/transport.py +0 -0
  26. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  27. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/wrapper.py +0 -0
  28. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/cli.py +0 -0
  29. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  30. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  31. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  32. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  33. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  34. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  35. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  36. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  37. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  38. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/__init__.py +0 -0
  39. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/api.py +0 -0
  40. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/approval.py +0 -0
  41. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/channel.py +0 -0
  42. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/frame.py +0 -0
  43. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/identity.py +0 -0
  44. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/outbound.py +0 -0
  45. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/plugin.py +0 -0
  46. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/transport.py +0 -0
  47. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  48. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  49. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  50. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  51. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +0 -0
  52. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  53. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  54. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  55. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  56. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  57. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +0 -0
  58. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
  59. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  60. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  61. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  62. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  63. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  64. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/channel.py +0 -0
  65. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  66. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  67. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  68. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  69. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  70. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  71. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  72. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  73. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/__init__.py +0 -0
  74. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/approval.py +0 -0
  75. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/audit.py +0 -0
  76. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/channel.py +0 -0
  77. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/client.py +0 -0
  78. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/command.py +0 -0
  79. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/concurrency.py +0 -0
  80. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/correlation.py +0 -0
  81. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/instruction.py +0 -0
  82. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  83. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  84. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/entities.py +0 -0
  85. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  86. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  87. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/states.py +0 -0
  88. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/observability.py +0 -0
  89. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  90. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  91. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  92. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  93. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  94. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  95. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/session.py +0 -0
  96. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  97. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/outcomes.py +0 -0
  98. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/paths.py +0 -0
  99. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  100. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/runtime.py +0 -0
  101. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  102. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  103. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  104. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/i18n/english.py +0 -0
  105. {bazaar_compute_node-0.1.20 → bazaar_compute_node-0.1.22}/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.20
3
+ Version: 0.1.22
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.20"
3
+ version = "0.1.22"
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.20"
3
+ version = "0.1.22"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -88,10 +88,14 @@ class AttachmentMaterializer:
88
88
  size = 0
89
89
  descriptor: int | None = None
90
90
  try:
91
+ open_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
92
+ binary_flag = getattr(os, "O_BINARY", None)
93
+ if binary_flag is not None:
94
+ open_flags |= binary_flag
91
95
  descriptor = await asyncio.to_thread(
92
96
  os.open,
93
97
  temporary,
94
- os.O_WRONLY | os.O_CREAT | os.O_EXCL,
98
+ open_flags,
95
99
  0o600,
96
100
  )
97
101
  try:
@@ -117,6 +121,12 @@ class AttachmentMaterializer:
117
121
  raise ValueError("attachment workspace quota exceeded")
118
122
  await asyncio.to_thread(_write_all, descriptor, chunk)
119
123
  await asyncio.to_thread(os.fsync, descriptor)
124
+ stored_size = await asyncio.to_thread(os.fstat, descriptor)
125
+ if stored_size.st_size != size:
126
+ raise OSError(
127
+ "attachment write size mismatch: "
128
+ f"expected {size}, got {stored_size.st_size}"
129
+ )
120
130
  finally:
121
131
  await asyncio.to_thread(os.close, descriptor)
122
132
  descriptor = None
@@ -165,7 +175,8 @@ class AttachmentMaterializer:
165
175
  return 0
166
176
  return sum(
167
177
  path.stat().st_size
168
- for path in root.glob("*/content.*")
178
+ for pattern in ("*/content", "*/content.*")
179
+ for path in root.glob(pattern)
169
180
  if path.is_file() and not path.is_symlink()
170
181
  )
171
182
 
@@ -183,8 +194,11 @@ def _attachment_suffix(name: str, media_type: str | None) -> str:
183
194
  suffix = Path(name).suffix
184
195
  if _SAFE_SUFFIX.fullmatch(suffix):
185
196
  return suffix
186
- guessed = mimetypes.guess_extension(media_type or "") or ".bin"
187
- return guessed if _SAFE_SUFFIX.fullmatch(guessed) else ".bin"
197
+ media_type = (media_type or "").partition(";")[0].strip().lower()
198
+ if not media_type or media_type.endswith("/octet-stream"):
199
+ return ""
200
+ guessed = mimetypes.guess_extension(media_type) or ""
201
+ return guessed if _SAFE_SUFFIX.fullmatch(guessed) else ""
188
202
 
189
203
 
190
204
  __all__ = ["AttachmentMaterializer"]
@@ -12,7 +12,7 @@ from uuid import uuid7
12
12
 
13
13
  from .app.command import format_message_time
14
14
  from .app.transport import LocalCommandClient
15
- from .core.reminder import format_utc_timestamp, short_id
15
+ from .core.reminder import format_utc_timestamp
16
16
 
17
17
 
18
18
  class BccCommandError(RuntimeError):
@@ -202,7 +202,7 @@ def build_parser() -> argparse.ArgumentParser:
202
202
  schedule_parser.add_argument(
203
203
  "--message-id",
204
204
  metavar="<id>",
205
- help="Required full or unique short local inbound message id used as anchor.",
205
+ help="Required full uuid for the local inbound message used as anchor.",
206
206
  )
207
207
 
208
208
  reminder_subparsers.add_parser(
@@ -241,7 +241,7 @@ def build_parser() -> argparse.ArgumentParser:
241
241
  "--id",
242
242
  dest="reminder_id",
243
243
  metavar="<id>",
244
- help="Reminder id (full uuid or short prefix)",
244
+ help="Reminder id (full uuid)",
245
245
  )
246
246
  snooze_parser.add_argument(
247
247
  "--by",
@@ -258,7 +258,7 @@ def build_parser() -> argparse.ArgumentParser:
258
258
  "--id",
259
259
  dest="reminder_id",
260
260
  metavar="<id>",
261
- help="Reminder id (full uuid or short prefix)",
261
+ help="Reminder id (full uuid)",
262
262
  )
263
263
  update_parser.add_argument(
264
264
  "--fire-at",
@@ -284,14 +284,14 @@ def build_parser() -> argparse.ArgumentParser:
284
284
 
285
285
  cancel_parser = reminder_subparsers.add_parser(
286
286
  "cancel",
287
- help="Cancel a scheduled reminder by id (full uuid or 8-char prefix)",
288
- description="Cancel a scheduled reminder by id (full uuid or 8-char prefix)",
287
+ help="Cancel a scheduled reminder by id (full uuid)",
288
+ description="Cancel a scheduled reminder by id (full uuid)",
289
289
  )
290
290
  cancel_parser.add_argument(
291
291
  "--id",
292
292
  dest="reminder_id",
293
293
  metavar="<id>",
294
- help="Reminder id (full uuid or short prefix)",
294
+ help="Reminder id (full uuid)",
295
295
  )
296
296
  return parser
297
297
 
@@ -714,13 +714,6 @@ def _require_reminder(result: Mapping[str, object]) -> Mapping[str, object]:
714
714
  return reminder
715
715
 
716
716
 
717
- def _short_uuid(value: str) -> str:
718
- try:
719
- return short_id(value)
720
- except ValueError as error:
721
- raise BccCommandError(str(error), code="INVALID_RESPONSE") from error
722
-
723
-
724
717
  def _reminder_label(reminder: Mapping[str, object]) -> str:
725
718
  repeat_rule = reminder.get("repeat_rule")
726
719
  if repeat_rule is None:
@@ -736,7 +729,7 @@ def _quoted_title(reminder: Mapping[str, object]) -> str:
736
729
 
737
730
  def serialize_reminder_schedule(result: Mapping[str, object]) -> str:
738
731
  reminder = _require_reminder(result)
739
- reminder_id = _short_uuid(_require_text(reminder, "reminder_id"))
732
+ reminder_id = _require_text(reminder, "reminder_id")
740
733
  next_fire = _optional_non_negative_int(reminder, "next_fire_at_ms")
741
734
  if next_fire is None:
742
735
  _invalid_response("scheduled Reminder response has no next fire time")
@@ -763,7 +756,7 @@ def serialize_reminder_check(result: Mapping[str, object]) -> str:
763
756
  occurrence = item.get("occurrence")
764
757
  if not isinstance(occurrence, Mapping):
765
758
  _invalid_response("Reminder check item has no occurrence")
766
- reminder_id = _short_uuid(_require_text(occurrence, "reminder_id"))
759
+ reminder_id = _require_text(occurrence, "reminder_id")
767
760
  occurrence_no = _require_non_negative_int(occurrence, "occurrence_no")
768
761
  if occurrence_no <= 0:
769
762
  _invalid_response("Reminder occurrence number must be positive")
@@ -809,8 +802,8 @@ def serialize_reminder_list(result: Mapping[str, object]) -> str:
809
802
  return "No reminders."
810
803
  lines: list[str] = []
811
804
  for reminder in reminders:
812
- reminder_id = _short_uuid(_require_text(reminder, "reminder_id"))
813
- anchor = _short_uuid(_require_text(reminder, "anchor_message_id"))
805
+ reminder_id = _require_text(reminder, "reminder_id")
806
+ anchor = _require_text(reminder, "anchor_message_id")
814
807
  state = _require_text(reminder, "state")
815
808
  title = _quoted_title(reminder)
816
809
  label = _reminder_label(reminder)
@@ -850,7 +843,7 @@ def _serialize_reminder_mutation(
850
843
  include_next: bool,
851
844
  ) -> str:
852
845
  reminder = _require_reminder(result)
853
- reminder_id = _short_uuid(_require_text(reminder, "reminder_id"))
846
+ reminder_id = _require_text(reminder, "reminder_id")
854
847
  line = f"Reminder {verb}: #{reminder_id}"
855
848
  if not include_next:
856
849
  return line
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import json
5
+ import mimetypes
5
6
  from collections import OrderedDict
6
7
  from collections.abc import AsyncIterable, AsyncIterator, Mapping
7
8
  from contextlib import suppress
@@ -248,16 +249,22 @@ class LarkResourceCache:
248
249
  ):
249
250
  raise ValueError("resource_too_large")
250
251
  name = _safe_filename(
251
- _content_disposition_filename(
252
- response.headers.get("Content-Disposition")
253
- ),
254
- fallback=name,
252
+ name,
253
+ fallback=resource.resource_type,
255
254
  resource_type=resource.resource_type,
256
255
  )
257
256
  media_type = _safe_media_type(
258
257
  response.headers.get("Content-Type"),
259
258
  fallback=media_type,
260
259
  )
260
+ name = _resolve_resource_name(
261
+ event_name=name,
262
+ response_name=_content_disposition_filename(
263
+ response.headers.get("Content-Disposition")
264
+ ),
265
+ media_type=media_type,
266
+ resource_type=resource.resource_type,
267
+ )
261
268
  return await self._materializer.materialize(
262
269
  _bounded_stream(
263
270
  response.content.iter_chunked(_RESOURCE_CHUNK_BYTES)
@@ -475,7 +482,7 @@ def _resource_from_mapping(
475
482
  break
476
483
  name = _safe_filename(
477
484
  raw_name,
478
- fallback=f"{resource_type}.bin",
485
+ fallback=resource_type,
479
486
  resource_type=resource_type,
480
487
  )
481
488
  media_type = _safe_media_type(mapping.get("mime_type"))
@@ -550,23 +557,71 @@ def _safe_filename(
550
557
  resource_type: str,
551
558
  ) -> str:
552
559
  candidate = value if isinstance(value, str) and value else fallback
553
- if (
554
- not candidate
555
- or len(candidate) > 255
556
- or "/" in candidate
557
- or "\\" in candidate
558
- or any(ord(character) < 32 or ord(character) == 127 for character in candidate)
559
- ):
560
+ if not _is_safe_filename(candidate):
560
561
  candidate = fallback
561
- if (
562
- not candidate
563
- or len(candidate) > 255
564
- or "/" in candidate
565
- or "\\" in candidate
566
- or any(ord(character) < 32 or ord(character) == 127 for character in candidate)
567
- ):
568
- return f"{resource_type}.bin"
569
- return candidate
562
+ return candidate if _is_safe_filename(candidate) else resource_type
563
+
564
+
565
+ def _resolve_resource_name(
566
+ *,
567
+ event_name: str,
568
+ response_name: str | None,
569
+ media_type: str | None,
570
+ resource_type: str,
571
+ ) -> str:
572
+ response_name = response_name if _is_safe_filename(response_name) else None
573
+ if _safe_suffix(event_name):
574
+ name = event_name
575
+ elif response_name is not None and _safe_suffix(response_name):
576
+ name = response_name
577
+ elif event_name != resource_type:
578
+ name = event_name
579
+ elif response_name is not None:
580
+ name = response_name
581
+ else:
582
+ name = event_name
583
+
584
+ if _safe_suffix(name):
585
+ return name
586
+ suffix = _suffix_from_media_type(media_type)
587
+ if not suffix and resource_type == "audio":
588
+ suffix = ".mp3"
589
+ return f"{name}{suffix}" if suffix else name
590
+
591
+
592
+ def _is_safe_filename(value: object) -> bool:
593
+ return (
594
+ isinstance(value, str)
595
+ and bool(value)
596
+ and len(value) <= 255
597
+ and "/" not in value
598
+ and "\\" not in value
599
+ and not any(ord(character) < 32 or ord(character) == 127 for character in value)
600
+ )
601
+
602
+
603
+ def _safe_suffix(name: str) -> str:
604
+ dot = name.rfind(".")
605
+ if dot <= 0:
606
+ return ""
607
+ suffix = name[dot:]
608
+ return (
609
+ suffix
610
+ if 1 < len(suffix) <= 11 and suffix[1:].isascii() and suffix[1:].isalnum()
611
+ else ""
612
+ )
613
+
614
+
615
+ def _suffix_from_media_type(media_type: str | None) -> str:
616
+ media_type = (media_type or "").partition(";")[0].strip().lower()
617
+ if not media_type or media_type.endswith("/octet-stream"):
618
+ return ""
619
+ suffix = mimetypes.guess_extension(media_type) or ""
620
+ return (
621
+ suffix
622
+ if 1 < len(suffix) <= 11 and suffix[1:].isascii() and suffix[1:].isalnum()
623
+ else ""
624
+ )
570
625
 
571
626
 
572
627
  def _safe_media_type(value: object, *, fallback: str | None = None) -> str | None:
@@ -49,25 +49,15 @@ class ReminderTransaction(SqliteTransaction):
49
49
  async def resolve_inbound_message(
50
50
  self,
51
51
  session_id: str,
52
- message_id_or_prefix: str,
52
+ message_id: str,
53
53
  ) -> InboundMessage | None:
54
54
  validate_non_empty_text(session_id, "session_id")
55
- reference = canonical_id_reference(message_id_or_prefix)
56
- if len(reference) == 8:
57
- rows = await self.fetchall(
58
- f"SELECT {_INBOUND_COLUMNS} FROM inbound_messages "
59
- "WHERE session_id = ? AND message_id LIKE ? ORDER BY seq",
60
- (session_id, f"{reference}%"),
61
- )
62
- if len(rows) > 1:
63
- raise ValueError("inbound message id prefix is ambiguous")
64
- row = rows[0] if rows else None
65
- else:
66
- row = await self.fetchone(
67
- f"SELECT {_INBOUND_COLUMNS} FROM inbound_messages "
68
- "WHERE session_id = ? AND message_id = ?",
69
- (session_id, reference),
70
- )
55
+ reference = canonical_id_reference(message_id)
56
+ row = await self.fetchone(
57
+ f"SELECT {_INBOUND_COLUMNS} FROM inbound_messages "
58
+ "WHERE session_id = ? AND message_id = ?",
59
+ (session_id, reference),
60
+ )
71
61
  if row is None:
72
62
  return None
73
63
  return inbound_message_from_row(row, await self._attachments(row["message_id"]))
@@ -75,26 +65,15 @@ class ReminderTransaction(SqliteTransaction):
75
65
  async def get_reminder(
76
66
  self,
77
67
  owner_session_id: str,
78
- reminder_id_or_prefix: str,
68
+ reminder_id: str,
79
69
  ) -> Reminder | None:
80
70
  validate_non_empty_text(owner_session_id, "owner_session_id")
81
- reference = canonical_id_reference(reminder_id_or_prefix)
82
- if len(reference) == 8:
83
- rows = await self.fetchall(
84
- f"SELECT {_REMINDER_COLUMNS} FROM reminders "
85
- "WHERE owner_session_id = ? AND reminder_id LIKE ? "
86
- "ORDER BY reminder_id",
87
- (owner_session_id, f"{reference}%"),
88
- )
89
- if len(rows) > 1:
90
- raise ValueError("reminder id prefix is ambiguous")
91
- row = rows[0] if rows else None
92
- else:
93
- row = await self.fetchone(
94
- f"SELECT {_REMINDER_COLUMNS} FROM reminders "
95
- "WHERE owner_session_id = ? AND reminder_id = ?",
96
- (owner_session_id, reference),
97
- )
71
+ reference = canonical_id_reference(reminder_id)
72
+ row = await self.fetchone(
73
+ f"SELECT {_REMINDER_COLUMNS} FROM reminders "
74
+ "WHERE owner_session_id = ? AND reminder_id = ?",
75
+ (owner_session_id, reference),
76
+ )
98
77
  return reminder_from_row(row) if row is not None else None
99
78
 
100
79
  async def list_reminders(
@@ -129,8 +108,6 @@ class ReminderTransaction(SqliteTransaction):
129
108
  if await self.get_bcn_session(reminder.owner_session_id) is None:
130
109
  raise ValueError(f"unknown bcn session: {reminder.owner_session_id}")
131
110
  anchor_reference = canonical_id_reference(reminder.anchor_message_id)
132
- if len(anchor_reference) != 36:
133
- raise ValueError("anchor_message_id must be a full local message id")
134
111
  anchor = await self.resolve_inbound_message(
135
112
  reminder.owner_session_id, anchor_reference
136
113
  )
@@ -555,8 +532,6 @@ class ReminderTransaction(SqliteTransaction):
555
532
  marked: list[ReminderOccurrence] = []
556
533
  for occurrence_id in occurrence_ids:
557
534
  reference = canonical_id_reference(occurrence_id)
558
- if len(reference) != 36:
559
- raise ValueError("occurrence ids must be full UUIDs")
560
535
  row = await self.fetchone(
561
536
  f"SELECT {_OCCURRENCE_COLUMNS} FROM reminder_occurrences "
562
537
  "WHERE occurrence_id = ? AND owner_session_id = ?",
@@ -484,26 +484,15 @@ class ReminderTransaction(_ReminderTransaction):
484
484
  async def resolve_inbound_message(
485
485
  self,
486
486
  session_id: str,
487
- message_id_or_prefix: str,
487
+ message_id: str,
488
488
  ) -> InboundMessage | None:
489
489
  validate_non_empty_text(session_id, "session_id")
490
- reference = canonical_id_reference(message_id_or_prefix)
491
- if len(reference) == 8:
492
- rows = await self.fetchall(
493
- f"SELECT {_INBOUND_COLUMNS} FROM inbound_messages "
494
- "WHERE agent_id = bcn_agent_id() AND session_id = ? "
495
- "AND message_id LIKE ? ORDER BY seq",
496
- (session_id, f"{reference}%"),
497
- )
498
- if len(rows) > 1:
499
- raise ValueError("inbound message id prefix is ambiguous")
500
- row = rows[0] if rows else None
501
- else:
502
- row = await self.fetchone(
503
- f"SELECT {_INBOUND_COLUMNS} FROM inbound_messages "
504
- "WHERE agent_id = bcn_agent_id() AND session_id = ? AND message_id = ?",
505
- (session_id, reference),
506
- )
490
+ reference = canonical_id_reference(message_id)
491
+ row = await self.fetchone(
492
+ f"SELECT {_INBOUND_COLUMNS} FROM inbound_messages "
493
+ "WHERE agent_id = bcn_agent_id() AND session_id = ? AND message_id = ?",
494
+ (session_id, reference),
495
+ )
507
496
  if row is None:
508
497
  return None
509
498
  return inbound_message_from_row(
@@ -514,27 +503,16 @@ class ReminderTransaction(_ReminderTransaction):
514
503
  async def get_reminder(
515
504
  self,
516
505
  owner_session_id: str,
517
- reminder_id_or_prefix: str,
506
+ reminder_id: str,
518
507
  ) -> Reminder | None:
519
508
  validate_non_empty_text(owner_session_id, "owner_session_id")
520
- reference = canonical_id_reference(reminder_id_or_prefix)
521
- if len(reference) == 8:
522
- rows = await self.fetchall(
523
- f"SELECT {_REMINDER_COLUMNS} FROM reminders "
524
- "WHERE agent_id = bcn_agent_id() AND owner_session_id = ? "
525
- "AND reminder_id LIKE ? ORDER BY reminder_id",
526
- (owner_session_id, f"{reference}%"),
527
- )
528
- if len(rows) > 1:
529
- raise ValueError("reminder id prefix is ambiguous")
530
- row = rows[0] if rows else None
531
- else:
532
- row = await self.fetchone(
533
- f"SELECT {_REMINDER_COLUMNS} FROM reminders "
534
- "WHERE agent_id = bcn_agent_id() AND owner_session_id = ? "
535
- "AND reminder_id = ?",
536
- (owner_session_id, reference),
537
- )
509
+ reference = canonical_id_reference(reminder_id)
510
+ row = await self.fetchone(
511
+ f"SELECT {_REMINDER_COLUMNS} FROM reminders "
512
+ "WHERE agent_id = bcn_agent_id() AND owner_session_id = ? "
513
+ "AND reminder_id = ?",
514
+ (owner_session_id, reference),
515
+ )
538
516
  return reminder_from_row(row) if row is not None else None
539
517
 
540
518
  async def list_reminders(
@@ -709,8 +687,6 @@ class ReminderTransaction(_ReminderTransaction):
709
687
  marked: list[ReminderOccurrence] = []
710
688
  for occurrence_id in occurrence_ids:
711
689
  reference = canonical_id_reference(occurrence_id)
712
- if len(reference) != 36:
713
- raise ValueError("occurrence ids must be full UUIDs")
714
690
  row = await self.fetchone(
715
691
  f"SELECT {_OCCURRENCE_COLUMNS} FROM reminder_occurrences "
716
692
  "WHERE agent_id = bcn_agent_id() AND occurrence_id = ? "
@@ -32,6 +32,7 @@ from ...core.models import (
32
32
  InboundAttachment,
33
33
  InboundMessage,
34
34
  SenderIdentity,
35
+ SenderKind,
35
36
  )
36
37
  from ...core.outcomes import ProviderCallResult, ProviderCallStatus
37
38
  from ...core.runtime import RuntimeStreamItem
@@ -1005,7 +1006,7 @@ class WeComChannel(IChannel):
1005
1006
  canonical_target = f"{target_prefix}:{channel_session_id}"
1006
1007
  received_at_ms = time_ns() // 1_000_000
1007
1008
  content = await self._content(body, message_type)
1008
- metadata: dict[str, object] = {}
1009
+ metadata: dict[str, object] = {"sender_kind": SenderKind.HUMAN.value}
1009
1010
  create_time = body.get("create_time")
1010
1011
  if isinstance(create_time, int) and not isinstance(create_time, bool):
1011
1012
  metadata["provider_create_time"] = create_time
@@ -1041,6 +1042,7 @@ class WeComChannel(IChannel):
1041
1042
  mentions_agent=False,
1042
1043
  notifies_runtime=False,
1043
1044
  attachments=quote_content.attachments,
1045
+ metadata={"sender_kind": SenderKind.HUMAN.value},
1044
1046
  )
1045
1047
  )
1046
1048
  else:
@@ -317,11 +317,6 @@ class ReminderCommandService(IReminderService):
317
317
  try:
318
318
  anchor = await transaction.resolve_inbound_message(session_id, message_id)
319
319
  except ValueError as error:
320
- if "ambiguous" in str(error).casefold():
321
- raise ReminderCommandFailure(
322
- "REMINDER_ANCHOR_AMBIGUOUS",
323
- str(error),
324
- ) from error
325
320
  raise ReminderCommandFailure(
326
321
  "REMINDER_ANCHOR_NOT_FOUND",
327
322
  str(error),
@@ -342,11 +337,6 @@ class ReminderCommandService(IReminderService):
342
337
  try:
343
338
  reminder = await transaction.get_reminder(session_id, reminder_id)
344
339
  except ValueError as error:
345
- if "ambiguous" in str(error).casefold():
346
- raise ReminderCommandFailure(
347
- "REMINDER_ID_AMBIGUOUS",
348
- str(error),
349
- ) from error
350
340
  raise ReminderCommandFailure(
351
341
  "REMINDER_NOT_FOUND",
352
342
  str(error),
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
3
  from dataclasses import dataclass
5
4
  from typing import Self
6
5
  from unicodedata import category
@@ -21,8 +20,6 @@ from .reminder_schedule import (
21
20
  resolve_schedule,
22
21
  )
23
22
 
24
- _ID_PREFIX_PATTERN = re.compile(r"[0-9a-fA-F]{8}\Z")
25
-
26
23
  __all__ = [
27
24
  "RecurrenceKind",
28
25
  "ReminderCancelRequest",
@@ -49,7 +46,6 @@ __all__ = [
49
46
  "parse_fire_at",
50
47
  "parse_repeat_rule",
51
48
  "resolve_schedule",
52
- "short_id",
53
49
  ]
54
50
 
55
51
 
@@ -299,27 +295,15 @@ class ReminderCancelResult:
299
295
  def canonical_id_reference(value: str) -> str:
300
296
  if not isinstance(value, str) or not value:
301
297
  raise ValueError("id reference must be a non-empty string")
302
- if _ID_PREFIX_PATTERN.fullmatch(value) is not None:
303
- return value.lower()
304
298
  try:
305
299
  parsed = UUID(value)
306
300
  except ValueError as error:
307
- raise ValueError(
308
- "id reference must be a UUID or 8-character hexadecimal prefix"
309
- ) from error
301
+ raise ValueError("id reference must be a canonical full UUID") from error
310
302
  if str(parsed) != value.lower():
311
303
  raise ValueError("full UUID references must use canonical hyphenated form")
312
304
  return str(parsed)
313
305
 
314
306
 
315
- def short_id(value: str) -> str:
316
- try:
317
- parsed = UUID(value)
318
- except (TypeError, ValueError) as error:
319
- raise ValueError("value must be a full UUID") from error
320
- return parsed.hex[:8]
321
-
322
-
323
307
  def _require_non_negative_int(value: int, field_name: str) -> None:
324
308
  if isinstance(value, bool) or not isinstance(value, int) or value < 0:
325
309
  raise ValueError(f"{field_name} must be a non-negative integer")
@@ -42,7 +42,7 @@ class IStorageTransaction(Protocol):
42
42
  ) -> InboundMessage | None: ...
43
43
 
44
44
  async def resolve_inbound_message(
45
- self, session_id: str, message_id_or_prefix: str
45
+ self, session_id: str, message_id: str
46
46
  ) -> InboundMessage | None: ...
47
47
 
48
48
  async def list_ready_attachment_paths(self) -> tuple[str, ...]: ...
@@ -75,7 +75,7 @@ class IStorageTransaction(Protocol):
75
75
  ) -> OutboundMessage: ...
76
76
 
77
77
  async def get_reminder(
78
- self, owner_session_id: str, reminder_id_or_prefix: str
78
+ self, owner_session_id: str, reminder_id: str
79
79
  ) -> Reminder | None: ...
80
80
 
81
81
  async def list_reminders(