bazaar-compute-node 0.1.3__tar.gz → 0.1.4__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 (62) hide show
  1. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/events.py +8 -33
  5. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/runtime.py +0 -2
  6. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -67
  7. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/sqlite/database.py +34 -0
  8. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/sqlite/migrations.py +15 -0
  9. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -210
  10. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/models/entities.py +6 -22
  11. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/orchestration/turn.py +49 -51
  12. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/storage.py +0 -5
  13. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/LICENSE +0 -0
  14. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/README.md +0 -0
  15. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/__init__.py +0 -0
  16. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/__init__.py +0 -0
  17. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/application.py +0 -0
  18. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/attachments.py +0 -0
  19. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/command.py +0 -0
  20. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/config.py +0 -0
  21. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/registry.py +0 -0
  22. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/transport.py +0 -0
  23. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  24. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/app/wrapper.py +0 -0
  25. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/bcc.py +0 -0
  26. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/cli.py +0 -0
  27. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  28. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/__init__.py +0 -0
  29. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/approval.py +0 -0
  30. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/client.py +0 -0
  31. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/plugin.py +0 -0
  32. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/process.py +0 -0
  33. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/codex_app_server/protocol.py +0 -0
  34. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  35. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  36. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  37. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  38. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  39. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  40. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  41. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  42. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  43. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/__init__.py +0 -0
  44. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/approval.py +0 -0
  45. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/audit.py +0 -0
  46. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/channel.py +0 -0
  47. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/client.py +0 -0
  48. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/command.py +0 -0
  49. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/concurrency.py +0 -0
  50. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/correlation.py +0 -0
  51. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/instruction.py +0 -0
  52. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  53. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  54. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/models/states.py +0 -0
  55. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/observability.py +0 -0
  56. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  57. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  58. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  59. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/orchestration/session.py +0 -0
  60. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/outcomes.py +0 -0
  61. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/paths.py +0 -0
  62. {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.4}/src/bazaar_compute_node/core/runtime.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.3
3
+ Version: 0.1.4
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.3"
3
+ version = "0.1.4"
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.3"
3
+ version = "0.1.4"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -4,7 +4,6 @@ import asyncio
4
4
  from collections.abc import Callable, Mapping
5
5
  from time import time_ns
6
6
  from typing import Self, cast
7
- from uuid import uuid7
8
7
 
9
8
  from ...core.approval import IApprovalHandler
10
9
  from ...core.models import (
@@ -21,12 +20,6 @@ from .approval import (
21
20
  parse_approval_request,
22
21
  )
23
22
 
24
- _DURABLE_ITEM_METHODS = {
25
- "item/started",
26
- "item/completed",
27
- "item/autoApprovalReview/started",
28
- "item/autoApprovalReview/completed",
29
- }
30
23
  _STREAM_EVENT_KINDS = {
31
24
  "item/agentMessage/delta": StreamEventKind.AGENT_MESSAGE_DELTA,
32
25
  "item/plan/delta": StreamEventKind.PLAN_DELTA,
@@ -56,8 +49,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
56
49
  self,
57
50
  supervisor: JsonlProcessSupervisor,
58
51
  *,
59
- node_id: str,
60
- runtime: str,
61
52
  session_id: str,
62
53
  runtime_session_id: str,
63
54
  turn_id: str,
@@ -71,8 +62,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
71
62
  on_closed: Callable[[], None] | None = None,
72
63
  ) -> None:
73
64
  self._supervisor = supervisor
74
- self._node_id = node_id
75
- self._runtime = runtime
76
65
  self._session_id = session_id
77
66
  self._runtime_session_id = runtime_session_id
78
67
  self._turn_id = turn_id
@@ -217,7 +206,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
217
206
  return self._event(
218
207
  event_name="codex.turn.error",
219
208
  state=RuntimeEventState.STARTED,
220
- error_type=error.error_type,
221
209
  error_message=error.message,
222
210
  metadata=metadata,
223
211
  )
@@ -231,9 +219,14 @@ class CodexTurnEventStream(IRuntimeTurnStream):
231
219
  return None
232
220
  if method == "item/reasoning/summaryPartAdded":
233
221
  return None
234
- if method == "turn/progress" or (
235
- method.startswith("item/") and method not in _DURABLE_ITEM_METHODS
236
- ):
222
+ if method in {
223
+ "item/started",
224
+ "item/completed",
225
+ "item/autoApprovalReview/started",
226
+ "item/autoApprovalReview/completed",
227
+ }:
228
+ return None
229
+ if method == "turn/progress" or method.startswith("item/"):
237
230
  stream_id = params.get("itemId")
238
231
  if not isinstance(stream_id, str) or not stream_id:
239
232
  stream_id = None
@@ -255,15 +248,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
255
248
  stream_id=stream_id,
256
249
  content=content,
257
250
  )
258
- if method == "turn/started" or method in _DURABLE_ITEM_METHODS:
259
- metadata = self._provider_metadata(method, params)
260
- if provider_turn_id is not None:
261
- metadata["provider_turn_id"] = provider_turn_id
262
- return self._event(
263
- event_name="codex.turn.progress",
264
- state=RuntimeEventState.STARTED,
265
- metadata=metadata,
266
- )
267
251
  return None
268
252
 
269
253
  def _event(
@@ -272,25 +256,16 @@ class CodexTurnEventStream(IRuntimeTurnStream):
272
256
  event_name: str,
273
257
  state: RuntimeEventState,
274
258
  error_kind: str | None = None,
275
- error_type: str | None = None,
276
259
  error_message: str | None = None,
277
260
  metadata: Mapping[str, object] | None = None,
278
261
  ) -> RuntimeEvent:
279
262
  return RuntimeEvent(
280
- event_seq=0,
281
- event_id=str(uuid7()),
282
263
  created_at_ms=time_ns() // 1_000_000,
283
- level="error" if error_kind else "info",
284
264
  event_name=event_name,
285
265
  state=state,
286
- node_id=self._node_id,
287
- bcn_session_id=self._session_id,
288
- runtime_session_id=self._runtime_session_id,
289
266
  turn_id=self._turn_id,
290
267
  error_kind=error_kind,
291
- error_type=error_type,
292
268
  error_message=error_message,
293
- runtime=self._runtime,
294
269
  metadata=dict(metadata or {}),
295
270
  )
296
271
 
@@ -327,8 +327,6 @@ class CodexAppServerRuntime(IRuntime, IAsyncLifecycle):
327
327
  connection.active_turn_id = turn.turn_id
328
328
  return CodexTurnEventStream(
329
329
  connection.supervisor,
330
- node_id=self._context.node_id,
331
- runtime=session.runtime,
332
330
  session_id=session.bcn_session_id,
333
331
  runtime_session_id=session.id,
334
332
  turn_id=turn.turn_id,
@@ -17,8 +17,6 @@ from ...core.models import (
17
17
  OutboundDeliveryState,
18
18
  OutboundMessage,
19
19
  RuntimeAttempt,
20
- RuntimeEvent,
21
- RuntimeEventState,
22
20
  RuntimeSession,
23
21
  )
24
22
 
@@ -214,40 +212,6 @@ def _outbound_message_from_row(row: aiosqlite.Row) -> OutboundMessage:
214
212
  )
215
213
 
216
214
 
217
- def _runtime_event_from_row(row: aiosqlite.Row) -> RuntimeEvent:
218
- return RuntimeEvent(
219
- event_seq=_required_non_negative_int(row["event_seq"], "event_seq"),
220
- event_id=_required_text(row["event_id"], "event_id"),
221
- created_at_ms=_required_non_negative_int(row["created_at_ms"], "created_at_ms"),
222
- level=_required_text(row["level"], "level"),
223
- event_name=_required_text(row["event_name"], "event_name"),
224
- state=RuntimeEventState(_required_text(row["state"], "runtime_event.state")),
225
- duration_ms=_optional_non_negative_int(row["duration_ms"], "duration_ms"),
226
- node_id=_optional_text(row["node_id"], "node_id"),
227
- channel=_optional_text(row["channel"], "channel"),
228
- runtime=_optional_text(row["runtime"], "runtime"),
229
- channel_session_id=_optional_text(
230
- row["channel_session_id"], "channel_session_id"
231
- ),
232
- bcn_session_id=_optional_text(row["bcn_session_id"], "bcn_session_id"),
233
- runtime_session_id=_optional_text(
234
- row["runtime_session_id"], "runtime_session_id"
235
- ),
236
- turn_id=_optional_text(row["turn_id"], "turn_id"),
237
- request_id=_optional_text(row["request_id"], "request_id"),
238
- command_id=_optional_text(row["command_id"], "command_id"),
239
- inbound_seq=_optional_non_negative_int(row["inbound_seq"], "inbound_seq"),
240
- outbound_message_id=_optional_text(
241
- row["outbound_message_id"], "outbound_message_id"
242
- ),
243
- error_kind=_optional_text(row["error_kind"], "error_kind"),
244
- error_type=_optional_text(row["error_type"], "error_type"),
245
- error_message=_optional_text(row["error_message"], "error_message"),
246
- traceback_ref=_optional_text(row["traceback_ref"], "traceback_ref"),
247
- metadata=_decode_metadata(row["metadata_json"], "metadata_json"),
248
- )
249
-
250
-
251
215
  def _consumer_cursor_from_row(row: aiosqlite.Row) -> ConsumerCursor:
252
216
  return ConsumerCursor(
253
217
  session_id=_required_text(row["session_id"], "session_id"),
@@ -508,37 +472,6 @@ def _merge_timestamp(
508
472
  return incoming if incoming is not None else existing
509
473
 
510
474
 
511
- def _validate_runtime_event_input(event: RuntimeEvent) -> None:
512
- if not isinstance(event, RuntimeEvent):
513
- raise TypeError("event must be a RuntimeEvent")
514
- if not isinstance(event.state, RuntimeEventState):
515
- raise TypeError("runtime event state is invalid")
516
- for value, field_name in (
517
- (event.node_id, "node_id"),
518
- (event.channel, "channel"),
519
- (event.runtime, "runtime"),
520
- (event.channel_session_id, "channel_session_id"),
521
- (event.bcn_session_id, "bcn_session_id"),
522
- (event.runtime_session_id, "runtime_session_id"),
523
- (event.turn_id, "turn_id"),
524
- (event.request_id, "request_id"),
525
- (event.command_id, "command_id"),
526
- (event.outbound_message_id, "outbound_message_id"),
527
- (event.error_kind, "error_kind"),
528
- (event.error_type, "error_type"),
529
- (event.error_message, "error_message"),
530
- (event.traceback_ref, "traceback_ref"),
531
- ):
532
- _validate_optional_input_text(value, field_name)
533
-
534
-
535
- def _same_runtime_event_payload(
536
- existing: RuntimeEvent,
537
- incoming: RuntimeEvent,
538
- ) -> bool:
539
- return replace(existing, event_seq=incoming.event_seq) == incoming
540
-
541
-
542
475
  def _validate_optional_input_text(value: object, field_name: str) -> None:
543
476
  if value is not None and (not isinstance(value, str) or not value):
544
477
  raise ValueError(f"{field_name} must be a non-empty string when present")
@@ -13,6 +13,7 @@ import aiosqlite
13
13
  from ...core.paths import resolve_data_dir
14
14
  from ...core.storage import NodeIdentity
15
15
  from .migrations import (
16
+ RUNTIME_EVENTS_REMOVAL_MIGRATION,
16
17
  MigrationChecksumError,
17
18
  MigrationError,
18
19
  apply_migrations,
@@ -117,6 +118,39 @@ class SqliteDatabase:
117
118
  transaction,
118
119
  clock=_current_time_ms,
119
120
  )
121
+ compaction_row: aiosqlite.Row | None = None
122
+ async with SqliteTransaction(self) as transaction:
123
+ compaction_row = await transaction.fetchone(
124
+ "SELECT compaction_completed_at_ms "
125
+ "FROM schema_migrations WHERE version = ?",
126
+ (RUNTIME_EVENTS_REMOVAL_MIGRATION.version,),
127
+ )
128
+ if compaction_row is None:
129
+ raise MigrationError(
130
+ "runtime event removal migration is missing from ledger"
131
+ )
132
+ if compaction_row["compaction_completed_at_ms"] is None:
133
+ async with self._transaction_lock:
134
+ await connection.execute("VACUUM")
135
+ async with SqliteTransaction(self) as transaction:
136
+ await transaction.execute(
137
+ "UPDATE schema_migrations "
138
+ "SET compaction_completed_at_ms = ? WHERE version = ?",
139
+ (
140
+ _current_time_ms(),
141
+ RUNTIME_EVENTS_REMOVAL_MIGRATION.version,
142
+ ),
143
+ )
144
+ async with self._transaction_lock:
145
+ checkpoint_cursor = await connection.execute(
146
+ "PRAGMA wal_checkpoint(TRUNCATE)"
147
+ )
148
+ try:
149
+ checkpoint_row = await checkpoint_cursor.fetchone()
150
+ finally:
151
+ await checkpoint_cursor.close()
152
+ if checkpoint_row is None or checkpoint_row[0] != 0:
153
+ raise MigrationError("SQLite WAL checkpoint could not complete")
120
154
  except BaseException:
121
155
  self._connection = None
122
156
  self._node_state = None
@@ -551,6 +551,20 @@ TRANSIENT_STREAM_EVENT_MIGRATION = Migration(
551
551
  ),
552
552
  )
553
553
 
554
+ RUNTIME_EVENTS_REMOVAL_MIGRATION = Migration(
555
+ version=9,
556
+ name="remove_runtime_events",
557
+ statements=(
558
+ """
559
+ ALTER TABLE schema_migrations
560
+ ADD COLUMN compaction_completed_at_ms INTEGER
561
+ """,
562
+ """
563
+ DROP TABLE runtime_events
564
+ """,
565
+ ),
566
+ )
567
+
554
568
  MIGRATIONS: tuple[Migration, ...] = (
555
569
  SCHEMA_MIGRATION,
556
570
  SESSION_MAPPING_INDEX_MIGRATION,
@@ -560,6 +574,7 @@ MIGRATIONS: tuple[Migration, ...] = (
560
574
  INBOUND_MESSAGE_REFERENCE_INTEGRITY_MIGRATION,
561
575
  INBOUND_PROVIDER_IDENTITY_MIGRATION,
562
576
  TRANSIENT_STREAM_EVENT_MIGRATION,
577
+ RUNTIME_EVENTS_REMOVAL_MIGRATION,
563
578
  )
564
579
 
565
580
 
@@ -17,7 +17,6 @@ from ...core.models import (
17
17
  InboundMessage,
18
18
  OutboundMessage,
19
19
  RuntimeAttempt,
20
- RuntimeEvent,
21
20
  RuntimeSession,
22
21
  )
23
22
  from .codec import (
@@ -31,9 +30,7 @@ from .codec import (
31
30
  _required_non_negative_int,
32
31
  _required_positive_int,
33
32
  _runtime_attempt_from_row,
34
- _runtime_event_from_row,
35
33
  _runtime_session_from_row,
36
- _same_runtime_event_payload,
37
34
  _validate_bcn_session_update,
38
35
  _validate_channel_session_input,
39
36
  _validate_channel_session_update,
@@ -47,7 +44,6 @@ from .codec import (
47
44
  _validate_outbound_message_input,
48
45
  _validate_outbound_update,
49
46
  _validate_positive_int,
50
- _validate_runtime_event_input,
51
47
  _validate_runtime_session_update,
52
48
  )
53
49
 
@@ -664,212 +660,6 @@ class SqliteTransaction(AbstractAsyncContextManager["SqliteTransaction"]):
664
660
  )
665
661
  return canonical
666
662
 
667
- async def append_runtime_event(self, event: RuntimeEvent) -> RuntimeEvent:
668
- _validate_runtime_event_input(event)
669
- existing_row = await self._fetch_one_or_conflict(
670
- "SELECT event_seq, event_id, created_at_ms, level, event_name, state, "
671
- "duration_ms, node_id, channel, runtime, "
672
- "channel_session_id, bcn_session_id, runtime_session_id, "
673
- "turn_id, request_id, command_id, inbound_seq, outbound_message_id, "
674
- "error_kind, error_type, error_message, traceback_ref, metadata_json "
675
- "FROM runtime_events WHERE event_id = ? ORDER BY event_seq",
676
- (event.event_id,),
677
- "runtime event identity",
678
- )
679
- if existing_row is not None:
680
- existing = _runtime_event_from_row(existing_row)
681
- if _same_runtime_event_payload(existing, event):
682
- return existing
683
- raise ValueError(
684
- "runtime event id is already bound to different event content"
685
- )
686
-
687
- await self._validate_runtime_event_references(event)
688
- sequence_row = await self.fetchone(
689
- "SELECT COALESCE(MAX(event_seq), 0) + 1 AS next_event_seq "
690
- "FROM runtime_events"
691
- )
692
- if sequence_row is None:
693
- raise RuntimeError("SQLite runtime event sequence query returned no row")
694
- next_event_seq = _required_positive_int(
695
- sequence_row["next_event_seq"], "next_event_seq"
696
- )
697
- canonical = replace(event, event_seq=next_event_seq)
698
- await self.execute(
699
- "INSERT INTO runtime_events ("
700
- "event_seq, event_id, created_at_ms, level, event_name, state, "
701
- "duration_ms, node_id, channel, runtime, "
702
- "channel_session_id, bcn_session_id, runtime_session_id, "
703
- "turn_id, request_id, command_id, inbound_seq, outbound_message_id, "
704
- "error_kind, error_type, error_message, traceback_ref, metadata_json"
705
- ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
706
- (
707
- canonical.event_seq,
708
- canonical.event_id,
709
- canonical.created_at_ms,
710
- canonical.level,
711
- canonical.event_name,
712
- canonical.state.value,
713
- canonical.duration_ms,
714
- canonical.node_id,
715
- canonical.channel,
716
- canonical.runtime,
717
- canonical.channel_session_id,
718
- canonical.bcn_session_id,
719
- canonical.runtime_session_id,
720
- canonical.turn_id,
721
- canonical.request_id,
722
- canonical.command_id,
723
- canonical.inbound_seq,
724
- canonical.outbound_message_id,
725
- canonical.error_kind,
726
- canonical.error_type,
727
- canonical.error_message,
728
- canonical.traceback_ref,
729
- _encode_metadata(canonical.metadata),
730
- ),
731
- )
732
- return canonical
733
-
734
- async def _validate_runtime_event_references(self, event: RuntimeEvent) -> None:
735
- channel_session = None
736
- if event.channel_session_id is not None:
737
- channel_session = await self.get_channel_session(event.channel_session_id)
738
- if channel_session is None:
739
- raise ValueError(f"unknown channel session: {event.channel_session_id}")
740
- if event.channel is not None and event.channel != channel_session.channel:
741
- raise ValueError("runtime event channel binding does not match")
742
-
743
- bcn_session = None
744
- if event.bcn_session_id is not None:
745
- bcn_session = await self.get_bcn_session(event.bcn_session_id)
746
- if bcn_session is None:
747
- raise ValueError(f"unknown bcn session: {event.bcn_session_id}")
748
- if (
749
- event.channel_session_id is not None
750
- and bcn_session.channel_session_id != event.channel_session_id
751
- ):
752
- raise ValueError("runtime event bcn/channel binding does not match")
753
- if channel_session is None:
754
- channel_session = await self.get_channel_session(
755
- bcn_session.channel_session_id
756
- )
757
- if (
758
- event.channel is not None
759
- and channel_session is not None
760
- and event.channel != channel_session.channel
761
- ):
762
- raise ValueError("runtime event channel binding does not match")
763
-
764
- runtime_session = None
765
- if event.runtime_session_id is not None:
766
- runtime_session = await self.get_runtime_session(event.runtime_session_id)
767
- if runtime_session is None:
768
- raise ValueError(f"unknown runtime session: {event.runtime_session_id}")
769
- if (
770
- event.bcn_session_id is not None
771
- and runtime_session.bcn_session_id != event.bcn_session_id
772
- ):
773
- raise ValueError("runtime event runtime/bcn binding does not match")
774
- if (
775
- event.channel_session_id is not None
776
- and runtime_session.channel_session_id != event.channel_session_id
777
- ):
778
- raise ValueError("runtime event runtime/channel binding does not match")
779
- if event.runtime is not None and runtime_session.runtime != event.runtime:
780
- raise ValueError("runtime event runtime name does not match")
781
- if event.channel is not None:
782
- runtime_channel = await self.get_channel_session(
783
- runtime_session.channel_session_id
784
- )
785
- if (
786
- runtime_channel is not None
787
- and runtime_channel.channel != event.channel
788
- ):
789
- raise ValueError("runtime event channel binding does not match")
790
-
791
- if event.turn_id is not None:
792
- attempt = await self.get_runtime_attempt(event.turn_id)
793
- if attempt is None:
794
- raise ValueError(f"unknown runtime attempt: {event.turn_id}")
795
- if (
796
- event.runtime_session_id is not None
797
- and attempt.session_id != event.runtime_session_id
798
- ):
799
- raise ValueError("runtime event attempt/runtime binding does not match")
800
- if runtime_session is None:
801
- runtime_session = await self.get_runtime_session(attempt.session_id)
802
- if (
803
- event.bcn_session_id is not None
804
- and runtime_session is not None
805
- and runtime_session.bcn_session_id != event.bcn_session_id
806
- ):
807
- raise ValueError("runtime event turn/bcn binding does not match")
808
- if runtime_session is not None:
809
- if (
810
- event.channel_session_id is not None
811
- and runtime_session.channel_session_id != event.channel_session_id
812
- ):
813
- raise ValueError(
814
- "runtime event turn/channel binding does not match"
815
- )
816
- if (
817
- event.runtime is not None
818
- and runtime_session.runtime != event.runtime
819
- ):
820
- raise ValueError("runtime event turn/runtime name does not match")
821
- if event.channel is not None:
822
- turn_channel = await self.get_channel_session(
823
- runtime_session.channel_session_id
824
- )
825
- if (
826
- turn_channel is not None
827
- and turn_channel.channel != event.channel
828
- ):
829
- raise ValueError(
830
- "runtime event turn/channel binding does not match"
831
- )
832
-
833
- if event.outbound_message_id is not None:
834
- outbound = await self.get_outbound_message(event.outbound_message_id)
835
- if outbound is None:
836
- raise ValueError(
837
- f"unknown outbound message: {event.outbound_message_id}"
838
- )
839
- if (
840
- event.bcn_session_id is not None
841
- and outbound.session_id != event.bcn_session_id
842
- ):
843
- raise ValueError("runtime event outbound/bcn binding does not match")
844
- if (
845
- event.channel_session_id is not None
846
- and outbound.channel_session_id != event.channel_session_id
847
- ):
848
- raise ValueError(
849
- "runtime event outbound/channel binding does not match"
850
- )
851
- if event.channel is not None:
852
- outbound_channel = await self.get_channel_session(
853
- outbound.channel_session_id
854
- )
855
- if (
856
- outbound_channel is not None
857
- and outbound_channel.channel != event.channel
858
- ):
859
- raise ValueError(
860
- "runtime event outbound/channel binding does not match"
861
- )
862
-
863
- if event.inbound_seq is not None and event.bcn_session_id is not None:
864
- row = await self.fetchone(
865
- "SELECT 1 FROM inbound_messages WHERE session_id = ? AND seq = ?",
866
- (event.bcn_session_id, event.inbound_seq),
867
- )
868
- if row is None:
869
- raise ValueError(
870
- f"unknown inbound sequence for bcn session: {event.inbound_seq}"
871
- )
872
-
873
663
  async def save_channel_session(self, session: ChannelSession) -> None:
874
664
  _validate_channel_session_input(session)
875
665
  existing = await self.get_channel_session(session.id)
@@ -476,39 +476,23 @@ class StreamEvent:
476
476
 
477
477
  @dataclass(frozen=True, slots=True)
478
478
  class RuntimeEvent:
479
- event_seq: int
480
- event_id: str
481
479
  created_at_ms: int
482
- level: str
483
480
  event_name: str
484
481
  state: RuntimeEventState
485
- duration_ms: int | None = None
486
- node_id: str | None = None
487
- channel: str | None = None
488
- channel_session_id: str | None = None
489
- bcn_session_id: str | None = None
490
- runtime_session_id: str | None = None
491
482
  turn_id: str | None = None
492
- request_id: str | None = None
493
- command_id: str | None = None
494
- inbound_seq: int | None = None
495
- outbound_message_id: str | None = None
496
483
  error_kind: str | None = None
497
- error_type: str | None = None
498
484
  error_message: str | None = None
499
- traceback_ref: str | None = None
500
- runtime: str | None = None
501
485
  metadata: Metadata = field(default_factory=dict)
502
486
 
503
487
  def __post_init__(self) -> None:
504
- _validate_non_negative(self.event_seq, "event_seq")
505
- _validate_text(self.event_id, "event_id")
506
488
  _validate_non_negative(self.created_at_ms, "created_at_ms")
507
- _validate_text(self.level, "level")
508
489
  _validate_text(self.event_name, "event_name")
490
+ if not isinstance(self.state, RuntimeEventState):
491
+ raise TypeError("state must be a RuntimeEventState")
509
492
  for value, field_name in (
510
- (self.duration_ms, "duration_ms"),
511
- (self.inbound_seq, "inbound_seq"),
493
+ (self.turn_id, "turn_id"),
494
+ (self.error_kind, "error_kind"),
495
+ (self.error_message, "error_message"),
512
496
  ):
513
497
  if value is not None:
514
- _validate_non_negative(value, field_name)
498
+ _validate_text(value, field_name)
@@ -354,58 +354,56 @@ class SessionTurnCoordinator:
354
354
  if event.turn_id is not None and event.turn_id != turn.turn_id:
355
355
  raise ValueError("runtime event turn correlation mismatch")
356
356
  async with self._concurrency.for_session(message.session_id):
357
- async with self._storage.transaction() as transaction:
358
- event = await transaction.append_runtime_event(event)
359
- agent_signal = _runtime_event_agent_signal(event)
360
- if not _is_turn_event(event.event_name):
361
- target_state = turn.state
362
- elif event.state is RuntimeEventState.STARTED:
363
- target_state = RuntimeTurnState.RUNNING
364
- elif event.state is RuntimeEventState.COMPLETED:
365
- target_state = RuntimeTurnState.COMPLETED
366
- elif event.state is RuntimeEventState.FAILED:
367
- target_state = RuntimeTurnState.FAILED
368
- elif event.state is RuntimeEventState.CANCELLED:
369
- target_state = RuntimeTurnState.CANCELLED
370
- else:
371
- target_state = RuntimeTurnState.UNKNOWN
372
- provider_turn_id = event.metadata.get("provider_turn_id")
373
- if provider_turn_id is not None and (
374
- not isinstance(provider_turn_id, str) or not provider_turn_id
375
- ):
376
- raise ValueError("runtime event provider_turn_id is invalid")
377
- if (
378
- provider_turn_id is not None
379
- and turn.provider_turn_id is not None
380
- and turn.provider_turn_id != provider_turn_id
381
- ):
382
- raise ValueError("runtime event provider turn correlation mismatch")
383
- error_kind = event.error_kind
384
- if event.state is RuntimeEventState.FAILED and error_kind is None:
385
- error_kind = ErrorKind.PROVIDER_FAILED.value
386
- if event.state is RuntimeEventState.UNKNOWN and error_kind is None:
387
- error_kind = ErrorKind.PROVIDER_UNKNOWN.value
388
- if event.state is RuntimeEventState.CANCELLED and error_kind is None:
389
- error_kind = ErrorKind.CANCELLED.value
390
- updated_turn = turn.transition_to(
391
- target_state,
392
- at_ms=event.created_at_ms,
393
- error_kind=error_kind,
394
- error_message=event.error_message,
395
- latest_event_name=event.event_name,
396
- )
397
- if provider_turn_id is not None:
398
- updated_turn = replace(
399
- updated_turn,
400
- provider_turn_id=provider_turn_id,
401
- )
402
- agent_tick = AgentTick(
403
- source=AgentTickSource.RUNTIME,
404
- signal=agent_signal,
405
- observed_at_ms=self._clock(),
406
- error_kind=error_kind,
407
- error_message=event.error_message,
357
+ agent_signal = _runtime_event_agent_signal(event)
358
+ if not _is_turn_event(event.event_name):
359
+ target_state = turn.state
360
+ elif event.state is RuntimeEventState.STARTED:
361
+ target_state = RuntimeTurnState.RUNNING
362
+ elif event.state is RuntimeEventState.COMPLETED:
363
+ target_state = RuntimeTurnState.COMPLETED
364
+ elif event.state is RuntimeEventState.FAILED:
365
+ target_state = RuntimeTurnState.FAILED
366
+ elif event.state is RuntimeEventState.CANCELLED:
367
+ target_state = RuntimeTurnState.CANCELLED
368
+ else:
369
+ target_state = RuntimeTurnState.UNKNOWN
370
+ provider_turn_id = event.metadata.get("provider_turn_id")
371
+ if provider_turn_id is not None and (
372
+ not isinstance(provider_turn_id, str) or not provider_turn_id
373
+ ):
374
+ raise ValueError("runtime event provider_turn_id is invalid")
375
+ if (
376
+ provider_turn_id is not None
377
+ and turn.provider_turn_id is not None
378
+ and turn.provider_turn_id != provider_turn_id
379
+ ):
380
+ raise ValueError("runtime event provider turn correlation mismatch")
381
+ error_kind = event.error_kind
382
+ if event.state is RuntimeEventState.FAILED and error_kind is None:
383
+ error_kind = ErrorKind.PROVIDER_FAILED.value
384
+ if event.state is RuntimeEventState.UNKNOWN and error_kind is None:
385
+ error_kind = ErrorKind.PROVIDER_UNKNOWN.value
386
+ if event.state is RuntimeEventState.CANCELLED and error_kind is None:
387
+ error_kind = ErrorKind.CANCELLED.value
388
+ updated_turn = turn.transition_to(
389
+ target_state,
390
+ at_ms=event.created_at_ms,
391
+ error_kind=error_kind,
392
+ error_message=event.error_message,
393
+ latest_event_name=event.event_name,
394
+ )
395
+ if provider_turn_id is not None:
396
+ updated_turn = replace(
397
+ updated_turn,
398
+ provider_turn_id=provider_turn_id,
408
399
  )
400
+ agent_tick = AgentTick(
401
+ source=AgentTickSource.RUNTIME,
402
+ signal=agent_signal,
403
+ observed_at_ms=self._clock(),
404
+ error_kind=error_kind,
405
+ error_message=event.error_message,
406
+ )
409
407
  if _is_terminal_turn_event(event):
410
408
  self._turns.pop(turn.turn_id, None)
411
409
  else:
@@ -12,7 +12,6 @@ from .models import (
12
12
  InboundMessage,
13
13
  OutboundMessage,
14
14
  RuntimeAttempt,
15
- RuntimeEvent,
16
15
  RuntimeSession,
17
16
  )
18
17
 
@@ -140,10 +139,6 @@ class IStorageTransaction(Protocol):
140
139
  """Persist a draft or delivery transition and return its canonical row."""
141
140
  ...
142
141
 
143
- async def append_runtime_event(self, event: RuntimeEvent) -> RuntimeEvent:
144
- """Append an operational event and return its canonical local sequence."""
145
- ...
146
-
147
142
 
148
143
  class IStorage(IAsyncLifecycle, Protocol):
149
144
  """Provider-neutral storage lifecycle and explicit transaction factory."""