onec-interactive-runtime-core 0.1.19__py3-none-any.whl

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 (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. onec_runtime_build/parser_target_development.py +116 -0
@@ -0,0 +1,1431 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+ import json
6
+ from threading import RLock
7
+ from uuid import UUID, uuid4, uuid5
8
+
9
+ from onec_runtime.bsl.source_maps import (
10
+ MappingRelation,
11
+ SourceSpan,
12
+ SourceUnitRef,
13
+ )
14
+ from onec_runtime.bsl.worker_reload_source_map import CompactReloadSourceMap
15
+ from onec_runtime.errors import ProtocolError
16
+ from onec_runtime.rdbg.models import ModuleLocation, StackFrame, StopEvent, TargetId
17
+ from onec_runtime.stop_routing import StopReason
18
+ from onec_runtime.worker_universe import (
19
+ WorkerGenerationDebugView,
20
+ WorkerGenerationHandle,
21
+ WorkerModuleDebugView,
22
+ )
23
+
24
+
25
+ class WorkerBreakpointRejection(StrEnum):
26
+ MODULE_NOT_IN_CANDIDATE = "module_not_in_candidate"
27
+ SOURCE_IDENTITY_MISMATCH = "source_identity_mismatch"
28
+ SOURCE_REVISION_NOT_ADMITTED = "source_revision_not_admitted"
29
+ NO_RETAINED_COMPATIBLE_ARTIFACT = "no_retained_compatible_artifact"
30
+ LINE_OUT_OF_RANGE = "line_out_of_range"
31
+ UNMAPPED_SOURCE_LINE = "unmapped_source_line"
32
+ SYNTHETIC_TARGET = "synthetic_target"
33
+ AMBIGUOUS_LINE_MAPPING = "ambiguous_line_mapping"
34
+
35
+
36
+ class WorkerBreakpointReloadPolicy(StrEnum):
37
+ STRICT = "strict"
38
+ RESET_INCOMPATIBLE = "reset_incompatible"
39
+
40
+
41
+ class WorkerBreakpointResolution(StrEnum):
42
+ PENDING = "pending"
43
+ RESOLVED = "resolved"
44
+ REJECTED = "rejected"
45
+
46
+
47
+ class WorkerBreakpointReloadOutcome(StrEnum):
48
+ COMMITTED = "committed"
49
+ ABORTED = "aborted"
50
+ QUARANTINED = "quarantined"
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class WorkerBreakpoint:
55
+ id: UUID
56
+ source_unit: SourceUnitRef
57
+ canonical_module: str
58
+ line: int
59
+ column: int | None = None
60
+
61
+ def __post_init__(self) -> None:
62
+ if (
63
+ type(self.id) is not UUID
64
+ or not isinstance(self.source_unit, SourceUnitRef)
65
+ or type(self.canonical_module) is not str
66
+ or not self.canonical_module
67
+ or self.canonical_module != self.canonical_module.casefold()
68
+ or type(self.line) is not int
69
+ or self.line < 1
70
+ or self.column is not None
71
+ ):
72
+ raise ValueError("worker breakpoint is invalid")
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class WorkerBreakpointGenerationStatus:
77
+ generation: WorkerGenerationHandle
78
+ resolution: WorkerBreakpointResolution
79
+ reason: WorkerBreakpointRejection | None
80
+
81
+ def __post_init__(self) -> None:
82
+ if (
83
+ not isinstance(self.generation, WorkerGenerationHandle)
84
+ or type(self.resolution) is not WorkerBreakpointResolution
85
+ or not _valid_resolution_reason(self.resolution, self.reason)
86
+ ):
87
+ raise ValueError("worker breakpoint generation status is invalid")
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class WorkerBreakpointStatus:
92
+ breakpoint: WorkerBreakpoint
93
+ enabled: bool
94
+ resolution: WorkerBreakpointResolution
95
+ reason: WorkerBreakpointRejection | None
96
+ generations: tuple[WorkerBreakpointGenerationStatus, ...]
97
+ installed_binding_count: int
98
+ catalog_version: int
99
+
100
+ def __post_init__(self) -> None:
101
+ if (
102
+ not isinstance(self.breakpoint, WorkerBreakpoint)
103
+ or type(self.enabled) is not bool
104
+ or type(self.resolution) is not WorkerBreakpointResolution
105
+ or not _valid_resolution_reason(self.resolution, self.reason)
106
+ or type(self.generations) is not tuple
107
+ or any(
108
+ not isinstance(item, WorkerBreakpointGenerationStatus)
109
+ for item in self.generations
110
+ )
111
+ or type(self.installed_binding_count) is not int
112
+ or self.installed_binding_count < 0
113
+ or type(self.catalog_version) is not int
114
+ or self.catalog_version < 0
115
+ ):
116
+ raise ValueError("worker breakpoint status is invalid")
117
+
118
+
119
+ @dataclass(frozen=True, slots=True)
120
+ class WorkerBreakpointReloadRemoval:
121
+ breakpoint_id: UUID
122
+ reason: WorkerBreakpointRejection
123
+
124
+ def __post_init__(self) -> None:
125
+ if (
126
+ type(self.breakpoint_id) is not UUID
127
+ or type(self.reason) is not WorkerBreakpointRejection
128
+ ):
129
+ raise ValueError("worker breakpoint reload removal is invalid")
130
+
131
+
132
+ @dataclass(frozen=True, slots=True)
133
+ class WorkerBreakpointReloadReport:
134
+ transaction_id: UUID
135
+ candidate_handle: WorkerGenerationHandle
136
+ policy: WorkerBreakpointReloadPolicy
137
+ outcome: WorkerBreakpointReloadOutcome
138
+ planned_removals: tuple[WorkerBreakpointReloadRemoval, ...]
139
+ committed_removals: tuple[UUID, ...]
140
+ catalog_version: int
141
+
142
+ def __post_init__(self) -> None:
143
+ if (
144
+ type(self.transaction_id) is not UUID
145
+ or not isinstance(self.candidate_handle, WorkerGenerationHandle)
146
+ or type(self.policy) is not WorkerBreakpointReloadPolicy
147
+ or type(self.outcome) is not WorkerBreakpointReloadOutcome
148
+ or type(self.planned_removals) is not tuple
149
+ or any(
150
+ type(item) is not WorkerBreakpointReloadRemoval
151
+ for item in self.planned_removals
152
+ )
153
+ or type(self.committed_removals) is not tuple
154
+ or any(type(item) is not UUID for item in self.committed_removals)
155
+ or type(self.catalog_version) is not int
156
+ or self.catalog_version < 0
157
+ or (
158
+ self.outcome is not WorkerBreakpointReloadOutcome.COMMITTED
159
+ and self.committed_removals
160
+ )
161
+ ):
162
+ raise ValueError("worker breakpoint reload report is invalid")
163
+
164
+
165
+ class WorkerBreakpointConflict(ProtocolError):
166
+ """STRICT publication rejected by candidate-proven incompatibilities."""
167
+
168
+ def __init__(
169
+ self,
170
+ rejections: tuple[WorkerBreakpointReloadRemoval, ...],
171
+ ) -> None:
172
+ if (
173
+ type(rejections) is not tuple
174
+ or not rejections
175
+ or any(type(item) is not WorkerBreakpointReloadRemoval for item in rejections)
176
+ ):
177
+ raise ValueError("worker breakpoint conflict details are invalid")
178
+ self.rejections = rejections
179
+ super().__init__("Worker generation conflicts with enabled logical breakpoints")
180
+
181
+
182
+ @dataclass(frozen=True, slots=True, repr=False)
183
+ class WorkerBreakpointCatalogSnapshot:
184
+ catalog_version: int
185
+ statuses: tuple[WorkerBreakpointStatus, ...]
186
+ tombstone_ids: frozenset[UUID]
187
+
188
+ def __post_init__(self) -> None:
189
+ if (
190
+ type(self.catalog_version) is not int
191
+ or self.catalog_version < 0
192
+ or type(self.statuses) is not tuple
193
+ or any(not isinstance(item, WorkerBreakpointStatus) for item in self.statuses)
194
+ or any(item.catalog_version != self.catalog_version for item in self.statuses)
195
+ or len({item.breakpoint.id for item in self.statuses}) != len(self.statuses)
196
+ or type(self.tombstone_ids) is not frozenset
197
+ or any(type(item) is not UUID for item in self.tombstone_ids)
198
+ or any(item.breakpoint.id in self.tombstone_ids for item in self.statuses)
199
+ ):
200
+ raise ValueError("worker breakpoint catalog snapshot is invalid")
201
+
202
+ def __repr__(self) -> str:
203
+ return (
204
+ "WorkerBreakpointCatalogSnapshot("
205
+ f"catalog_version={self.catalog_version}, "
206
+ f"statuses={self.statuses!r}, tombstones={len(self.tombstone_ids)})"
207
+ )
208
+
209
+
210
+ @dataclass(frozen=True, slots=True, repr=False)
211
+ class WorkerBreakpointPlan:
212
+ owner: object
213
+ proposal_id: UUID
214
+ expected_catalog_version: int
215
+ result_id: UUID
216
+ desired_slots: tuple[ModuleLocation, ...]
217
+ removed_ids: tuple[UUID, ...]
218
+ removal_details: tuple[WorkerBreakpointReloadRemoval, ...]
219
+ next_views: tuple[WorkerGenerationDebugView, ...]
220
+ next_snapshot: WorkerBreakpointCatalogSnapshot
221
+
222
+ def __post_init__(self) -> None:
223
+ if (
224
+ type(self.proposal_id) is not UUID
225
+ or type(self.expected_catalog_version) is not int
226
+ or self.expected_catalog_version < 0
227
+ or type(self.result_id) is not UUID
228
+ or type(self.desired_slots) is not tuple
229
+ or any(type(item) is not ModuleLocation for item in self.desired_slots)
230
+ or type(self.removed_ids) is not tuple
231
+ or any(type(item) is not UUID for item in self.removed_ids)
232
+ or type(self.removal_details) is not tuple
233
+ or any(
234
+ type(item) is not WorkerBreakpointReloadRemoval
235
+ for item in self.removal_details
236
+ )
237
+ or (
238
+ self.removal_details
239
+ and tuple(item.breakpoint_id for item in self.removal_details)
240
+ != self.removed_ids
241
+ )
242
+ or type(self.next_views) is not tuple
243
+ or any(type(item) is not WorkerGenerationDebugView for item in self.next_views)
244
+ or not isinstance(self.next_snapshot, WorkerBreakpointCatalogSnapshot)
245
+ or self.next_snapshot.catalog_version
246
+ not in (self.expected_catalog_version, self.expected_catalog_version + 1)
247
+ ):
248
+ raise ValueError("worker breakpoint plan is invalid")
249
+
250
+ def __repr__(self) -> str:
251
+ return (
252
+ "WorkerBreakpointPlan("
253
+ "owner=<redacted>, "
254
+ f"proposal_id={self.proposal_id!r}, "
255
+ f"expected_catalog_version={self.expected_catalog_version}, "
256
+ f"result_id={self.result_id!r}, "
257
+ f"desired_slots={len(self.desired_slots)}, "
258
+ f"removed_ids={self.removed_ids!r}, "
259
+ f"next_snapshot={self.next_snapshot!r})"
260
+ )
261
+
262
+
263
+ _PENDING_REASONS = frozenset(
264
+ {
265
+ WorkerBreakpointRejection.SOURCE_REVISION_NOT_ADMITTED,
266
+ WorkerBreakpointRejection.NO_RETAINED_COMPATIBLE_ARTIFACT,
267
+ }
268
+ )
269
+ _MAP_REJECTION_REASONS = frozenset(
270
+ {
271
+ WorkerBreakpointRejection.LINE_OUT_OF_RANGE,
272
+ WorkerBreakpointRejection.UNMAPPED_SOURCE_LINE,
273
+ WorkerBreakpointRejection.SYNTHETIC_TARGET,
274
+ WorkerBreakpointRejection.AMBIGUOUS_LINE_MAPPING,
275
+ }
276
+ )
277
+
278
+
279
+ def _valid_resolution_reason(
280
+ resolution: WorkerBreakpointResolution,
281
+ reason: WorkerBreakpointRejection | None,
282
+ ) -> bool:
283
+ if resolution is WorkerBreakpointResolution.RESOLVED:
284
+ return reason is None
285
+ if type(reason) is not WorkerBreakpointRejection:
286
+ return False
287
+ if resolution is WorkerBreakpointResolution.PENDING:
288
+ return reason in _PENDING_REASONS
289
+ return reason in _MAP_REJECTION_REASONS
290
+
291
+
292
+ @dataclass(frozen=True, slots=True)
293
+ class WorkerLineMapping:
294
+ generated_line: int | None
295
+ reason: WorkerBreakpointRejection | None
296
+
297
+ def __post_init__(self) -> None:
298
+ if (
299
+ (self.generated_line is None) == (self.reason is None)
300
+ or (
301
+ self.generated_line is not None
302
+ and (
303
+ type(self.generated_line) is not int
304
+ or self.generated_line < 1
305
+ )
306
+ )
307
+ or (
308
+ self.reason is not None
309
+ and type(self.reason) is not WorkerBreakpointRejection
310
+ )
311
+ ):
312
+ raise ValueError("worker line mapping is invalid")
313
+
314
+
315
+ @dataclass(frozen=True, slots=True)
316
+ class WorkerSourceLocation:
317
+ source_unit: SourceUnitRef
318
+ canonical_module: str
319
+ line: int
320
+ column: int | None
321
+
322
+ def __post_init__(self) -> None:
323
+ if (
324
+ not isinstance(self.source_unit, SourceUnitRef)
325
+ or type(self.canonical_module) is not str
326
+ or not self.canonical_module
327
+ or self.canonical_module != self.canonical_module.casefold()
328
+ or type(self.line) is not int
329
+ or self.line < 1
330
+ or self.column is not None
331
+ ):
332
+ raise ValueError("worker source location is invalid")
333
+
334
+
335
+ class WorkerFrameProvenance:
336
+ """Opaque proof tying a mapped frame to one target-bound physical frame."""
337
+
338
+ __slots__ = ("__owner", "__raw_frame", "__target_incarnation_id")
339
+
340
+ def __init__(
341
+ self,
342
+ owner: object,
343
+ raw_frame: StackFrame,
344
+ target_incarnation_id: UUID,
345
+ ) -> None:
346
+ if (
347
+ owner is not _WORKER_FRAME_PROVENANCE_OWNER
348
+ or type(raw_frame) is not StackFrame
349
+ or type(target_incarnation_id) is not UUID
350
+ ):
351
+ raise TypeError("Worker frame provenance is private")
352
+ self.__owner = owner
353
+ self.__raw_frame = raw_frame
354
+ self.__target_incarnation_id = target_incarnation_id
355
+
356
+ def __repr__(self) -> str:
357
+ return "WorkerFrameProvenance(<redacted>)"
358
+
359
+
360
+ _WORKER_FRAME_PROVENANCE_OWNER = object()
361
+
362
+
363
+ @dataclass(frozen=True, slots=True, repr=False)
364
+ class WorkerMappedFrame:
365
+ level: int
366
+ source: WorkerSourceLocation
367
+ generation: WorkerGenerationHandle
368
+ artifact_sha256: str
369
+ source_map_sha256: str
370
+ provenance: WorkerFrameProvenance
371
+
372
+ def __post_init__(self) -> None:
373
+ if (
374
+ type(self.level) is not int
375
+ or self.level < 0
376
+ or type(self.source) is not WorkerSourceLocation
377
+ or not isinstance(self.generation, WorkerGenerationHandle)
378
+ or type(self.artifact_sha256) is not str
379
+ or len(self.artifact_sha256) != 64
380
+ or type(self.source_map_sha256) is not str
381
+ or len(self.source_map_sha256) != 64
382
+ or type(self.provenance) is not WorkerFrameProvenance
383
+ ):
384
+ raise ValueError("mapped Worker frame is invalid")
385
+
386
+ def __repr__(self) -> str:
387
+ return (
388
+ "WorkerMappedFrame("
389
+ f"level={self.level}, source={self.source!r}, "
390
+ f"generation={self.generation!r}, "
391
+ f"artifact_sha256={self.artifact_sha256!r}, "
392
+ f"source_map_sha256={self.source_map_sha256!r}, "
393
+ "provenance=<redacted>)"
394
+ )
395
+
396
+
397
+ @dataclass(frozen=True, slots=True)
398
+ class NativeDebugFrame:
399
+ level: int
400
+ location: ModuleLocation
401
+
402
+ def __post_init__(self) -> None:
403
+ if (
404
+ type(self.level) is not int
405
+ or self.level < 0
406
+ or type(self.location) is not ModuleLocation
407
+ ):
408
+ raise ValueError("native debug frame is invalid")
409
+
410
+
411
+ @dataclass(frozen=True, slots=True, repr=False)
412
+ class WorkerBreakpointBinding:
413
+ breakpoint_id: UUID
414
+ generation: WorkerGenerationHandle
415
+ location: ModuleLocation
416
+
417
+ def __post_init__(self) -> None:
418
+ if (
419
+ type(self.breakpoint_id) is not UUID
420
+ or not isinstance(self.generation, WorkerGenerationHandle)
421
+ or type(self.location) is not ModuleLocation
422
+ ):
423
+ raise ValueError("Worker breakpoint binding is invalid")
424
+
425
+ def __repr__(self) -> str:
426
+ return (
427
+ "WorkerBreakpointBinding("
428
+ f"breakpoint_id={self.breakpoint_id!r}, "
429
+ f"generation={self.generation!r}, location=<redacted>)"
430
+ )
431
+
432
+
433
+ @dataclass(frozen=True, slots=True, repr=False)
434
+ class RuntimeDebugStop:
435
+ reason: StopReason
436
+ origin: str
437
+ operation_id: int
438
+ location: WorkerSourceLocation | ModuleLocation
439
+ frames: tuple[WorkerMappedFrame | NativeDebugFrame, ...]
440
+ breakpoint_ids: tuple[UUID, ...]
441
+
442
+ def __post_init__(self) -> None:
443
+ if (
444
+ type(self.reason) is not StopReason
445
+ or self.origin not in {"main", "capture_evaluation"}
446
+ or type(self.operation_id) is not int
447
+ or self.operation_id < 0
448
+ or type(self.location) not in {WorkerSourceLocation, ModuleLocation}
449
+ or type(self.frames) is not tuple
450
+ or any(
451
+ type(frame) not in {WorkerMappedFrame, NativeDebugFrame}
452
+ for frame in self.frames
453
+ )
454
+ or type(self.breakpoint_ids) is not tuple
455
+ or any(type(item) is not UUID for item in self.breakpoint_ids)
456
+ or len(set(self.breakpoint_ids)) != len(self.breakpoint_ids)
457
+ ):
458
+ raise ValueError("runtime debug stop is invalid")
459
+
460
+ def __repr__(self) -> str:
461
+ return (
462
+ "RuntimeDebugStop("
463
+ f"reason={self.reason!r}, origin={self.origin!r}, "
464
+ f"operation_id={self.operation_id}, location={self.location!r}, "
465
+ f"frames={self.frames!r}, breakpoint_ids={self.breakpoint_ids!r})"
466
+ )
467
+
468
+
469
+ def _overlaps(left: SourceSpan, right: SourceSpan) -> bool:
470
+ if left.start == left.end:
471
+ return right.start <= left.start <= right.end
472
+ if right.start == right.end:
473
+ return left.start <= right.start <= left.end
474
+ return left.start < right.end and right.start < left.end
475
+
476
+
477
+ def _generated_line_offsets(view: WorkerModuleDebugView, line: int) -> range:
478
+ try:
479
+ span = view.generated_line_index.line_range(line)
480
+ except ValueError as error:
481
+ raise ProtocolError("Worker generated line is outside the artifact") from error
482
+ if span.start == span.end:
483
+ return range(span.start, span.start + 1)
484
+ return range(span.start, span.end)
485
+
486
+
487
+ def map_generated_line(
488
+ view: WorkerModuleDebugView,
489
+ line: int,
490
+ ) -> WorkerSourceLocation | None:
491
+ if type(view) is not WorkerModuleDebugView:
492
+ raise TypeError("worker module debug view is required")
493
+ if type(line) is not int or line < 1:
494
+ raise ValueError("generated line must be positive")
495
+ if view.visible_context is None:
496
+ raise ProtocolError("Worker visible source context is unavailable")
497
+
498
+ exact: set[tuple[SourceUnitRef, int, int]] = set()
499
+ saw_synthetic = False
500
+ saw_unproven = False
501
+ for offset in _generated_line_offsets(view, line):
502
+ try:
503
+ mapped = view.mapped_source.source_map.map_offset(offset)
504
+ except ValueError:
505
+ continue
506
+ if mapped.relation is MappingRelation.EXACT:
507
+ if mapped.unit is None or mapped.origin_span is None:
508
+ raise ProtocolError("Worker exact mapping has no source identity")
509
+ coordinates = view.visible_context.line_column(
510
+ mapped.unit,
511
+ mapped.origin_span.start,
512
+ )
513
+ if coordinates is None:
514
+ raise ProtocolError("Worker exact mapping provenance is unavailable")
515
+ exact.add((mapped.unit, coordinates[0], coordinates[1]))
516
+ elif mapped.relation is MappingRelation.SYNTHETIC:
517
+ saw_synthetic = True
518
+ else:
519
+ saw_unproven = True
520
+
521
+ source_lines = {(unit, source_line) for unit, source_line, _column in exact}
522
+ if len(source_lines) > 1:
523
+ raise ProtocolError("Worker generated line mapping is ambiguous")
524
+ if source_lines:
525
+ unit, source_line = next(iter(source_lines))
526
+ return WorkerSourceLocation(
527
+ unit,
528
+ view.canonical_module,
529
+ source_line,
530
+ None,
531
+ )
532
+ if saw_synthetic and not saw_unproven:
533
+ return None
534
+ raise ProtocolError("Worker generated line mapping is not exact")
535
+
536
+
537
+ def resolve_source_line(
538
+ view: WorkerModuleDebugView,
539
+ source_unit: SourceUnitRef,
540
+ line: int,
541
+ ) -> WorkerLineMapping:
542
+ if type(view) is not WorkerModuleDebugView:
543
+ raise TypeError("worker module debug view is required")
544
+ if not isinstance(source_unit, SourceUnitRef):
545
+ raise TypeError("worker source unit is required")
546
+ if source_unit not in view.source_units:
547
+ raise ProtocolError("Worker source identity does not match the debug view")
548
+ if type(line) is not int or line < 1:
549
+ return WorkerLineMapping(None, WorkerBreakpointRejection.LINE_OUT_OF_RANGE)
550
+ if view.visible_context is None:
551
+ raise ProtocolError("Worker visible source context is unavailable")
552
+ requested = view.visible_context.line_range(source_unit, line)
553
+ if requested is None:
554
+ return WorkerLineMapping(None, WorkerBreakpointRejection.LINE_OUT_OF_RANGE)
555
+
556
+ exact_lines: set[int] = set()
557
+ synthetic_lines: set[int] = set()
558
+ source_map = view.mapped_source.source_map
559
+ if isinstance(source_map, CompactReloadSourceMap):
560
+ spans = source_map.mapped_spans_for_visible_span(source_unit, requested)
561
+ for relation, span in spans:
562
+ if span.start == span.end:
563
+ lines = (view.generated_line_index.offset_to_line_column(span.start)[0],)
564
+ else:
565
+ first = view.generated_line_index.offset_to_line_column(span.start)[0]
566
+ last = view.generated_line_index.offset_to_line_column(span.end - 1)[0]
567
+ lines = range(first, last + 1)
568
+ if relation is MappingRelation.EXACT:
569
+ exact_lines.update(lines)
570
+ elif relation is MappingRelation.SYNTHETIC:
571
+ synthetic_lines.update(lines)
572
+ else:
573
+ for offset in range(len(view.mapped_source.text) + 1):
574
+ try:
575
+ mapped = source_map.map_offset(offset)
576
+ except ValueError:
577
+ continue
578
+ generated_line = view.generated_line_index.offset_to_line_column(offset)[0]
579
+ if (
580
+ mapped.relation is MappingRelation.EXACT
581
+ and mapped.unit == source_unit
582
+ and mapped.origin_span is not None
583
+ and _overlaps(mapped.origin_span, requested)
584
+ ):
585
+ exact_lines.add(generated_line)
586
+ elif (
587
+ mapped.relation is MappingRelation.SYNTHETIC
588
+ and mapped.anchor_unit == source_unit
589
+ and mapped.anchor_span is not None
590
+ and _overlaps(mapped.anchor_span, requested)
591
+ ):
592
+ synthetic_lines.add(generated_line)
593
+
594
+ if not exact_lines:
595
+ return WorkerLineMapping(
596
+ None,
597
+ (
598
+ WorkerBreakpointRejection.SYNTHETIC_TARGET
599
+ if synthetic_lines
600
+ else WorkerBreakpointRejection.UNMAPPED_SOURCE_LINE
601
+ ),
602
+ )
603
+ if len(exact_lines) != 1 or exact_lines & synthetic_lines:
604
+ return WorkerLineMapping(
605
+ None,
606
+ WorkerBreakpointRejection.AMBIGUOUS_LINE_MAPPING,
607
+ )
608
+ generated_line = next(iter(exact_lines))
609
+ roundtrip = map_generated_line(view, generated_line)
610
+ if roundtrip is None or (
611
+ roundtrip.source_unit != source_unit or roundtrip.line != line
612
+ ):
613
+ return WorkerLineMapping(
614
+ None,
615
+ WorkerBreakpointRejection.AMBIGUOUS_LINE_MAPPING,
616
+ )
617
+ return WorkerLineMapping(generated_line, None)
618
+
619
+
620
+ def _breakpoint_identity(
621
+ source_unit: SourceUnitRef,
622
+ canonical_module: str,
623
+ line: int,
624
+ column: int | None,
625
+ ) -> str:
626
+ return json.dumps(
627
+ {
628
+ "canonical_module": canonical_module,
629
+ "column": column,
630
+ "line": line,
631
+ "source_kind": source_unit.kind.value,
632
+ "source_revision": source_unit.revision,
633
+ "source_sha256": source_unit.source_sha256.lower(),
634
+ "source_unit_id": source_unit.unit_id,
635
+ },
636
+ ensure_ascii=False,
637
+ sort_keys=True,
638
+ separators=(",", ":"),
639
+ )
640
+
641
+
642
+ def _module_location_key(location: ModuleLocation) -> tuple[object, ...]:
643
+ return (
644
+ location.module_type,
645
+ location.url,
646
+ location.object_id.hex,
647
+ location.property_id.hex,
648
+ location.line,
649
+ location.extension_name,
650
+ location.ext_id,
651
+ )
652
+
653
+
654
+ class WorkerBreakpointCoordinator:
655
+ """Own immutable logical Worker breakpoint catalog proposals.
656
+
657
+ This layer resolves admitted source provenance only. It intentionally does
658
+ not contact RDBG and therefore cannot claim any installed physical binding.
659
+ """
660
+
661
+ __slots__ = (
662
+ "_committed_proposals",
663
+ "_installed_slots",
664
+ "_lock",
665
+ "_owner",
666
+ "_quarantined_plans",
667
+ "_session_id",
668
+ "_snapshot",
669
+ "_views",
670
+ )
671
+
672
+ def __init__(self, *, session_id: UUID) -> None:
673
+ if type(session_id) is not UUID:
674
+ raise TypeError("worker breakpoint session id must be a UUID")
675
+ self._session_id = session_id
676
+ self._owner = object()
677
+ self._lock = RLock()
678
+ self._views: tuple[WorkerGenerationDebugView, ...] = ()
679
+ self._installed_slots: frozenset[ModuleLocation] = frozenset()
680
+ self._committed_proposals: set[UUID] = set()
681
+ self._quarantined_plans: list[WorkerBreakpointPlan] = []
682
+ self._snapshot = WorkerBreakpointCatalogSnapshot(0, (), frozenset())
683
+
684
+ def set_views(self, views: tuple[WorkerGenerationDebugView, ...]) -> None:
685
+ if (
686
+ type(views) is not tuple
687
+ or any(type(view) is not WorkerGenerationDebugView for view in views)
688
+ or len({id(view.handle) for view in views}) != len(views)
689
+ ):
690
+ raise TypeError("worker generation debug views are invalid")
691
+ with self._lock:
692
+ if views == self._views:
693
+ return
694
+ self._views = views
695
+ definitions = self._definitions(self._snapshot)
696
+ version = self._snapshot.catalog_version + 1
697
+ self._snapshot, _slots = self._build_snapshot(
698
+ definitions,
699
+ self._snapshot.tombstone_ids,
700
+ version,
701
+ )
702
+
703
+ def prepare_add(
704
+ self,
705
+ source_unit: SourceUnitRef,
706
+ canonical_module: str,
707
+ line: int,
708
+ *,
709
+ enabled: bool,
710
+ column: int | None,
711
+ ) -> WorkerBreakpointPlan:
712
+ self._validate_request(
713
+ source_unit,
714
+ canonical_module,
715
+ line,
716
+ enabled=enabled,
717
+ column=column,
718
+ )
719
+ canonical_module = canonical_module.casefold()
720
+ with self._lock:
721
+ identity = _breakpoint_identity(
722
+ source_unit,
723
+ canonical_module,
724
+ line,
725
+ column,
726
+ )
727
+ breakpoint_id = uuid5(self._session_id, identity)
728
+ for status in self._snapshot.statuses:
729
+ if status.breakpoint.id == breakpoint_id:
730
+ if (
731
+ status.breakpoint.source_unit != source_unit
732
+ or status.breakpoint.canonical_module != canonical_module
733
+ or status.breakpoint.line != line
734
+ or status.breakpoint.column != column
735
+ ):
736
+ raise ProtocolError("Worker breakpoint identity collision")
737
+ return self._plan(
738
+ result_id=breakpoint_id,
739
+ snapshot=self._snapshot,
740
+ removed_ids=(),
741
+ )
742
+ if breakpoint_id in self._snapshot.tombstone_ids:
743
+ raise ProtocolError("Removed Worker breakpoint identity cannot be reused")
744
+ breakpoint = WorkerBreakpoint(
745
+ breakpoint_id,
746
+ source_unit,
747
+ canonical_module,
748
+ line,
749
+ column,
750
+ )
751
+ definitions = self._definitions(self._snapshot) + ((breakpoint, enabled),)
752
+ next_snapshot, slots = self._build_snapshot(
753
+ definitions,
754
+ self._snapshot.tombstone_ids,
755
+ self._snapshot.catalog_version + 1,
756
+ )
757
+ return self._plan(
758
+ result_id=breakpoint_id,
759
+ snapshot=next_snapshot,
760
+ removed_ids=(),
761
+ desired_slots=slots,
762
+ )
763
+
764
+ def prepare_remove(self, breakpoint_id: UUID) -> WorkerBreakpointPlan:
765
+ self._validate_breakpoint_id(breakpoint_id)
766
+ with self._lock:
767
+ if breakpoint_id in self._snapshot.tombstone_ids:
768
+ return self._plan(
769
+ result_id=breakpoint_id,
770
+ snapshot=self._snapshot,
771
+ removed_ids=(),
772
+ )
773
+ definitions = tuple(
774
+ item
775
+ for item in self._definitions(self._snapshot)
776
+ if item[0].id != breakpoint_id
777
+ )
778
+ if len(definitions) == len(self._snapshot.statuses):
779
+ raise ProtocolError("Worker breakpoint id is unknown to this session")
780
+ tombstones = self._snapshot.tombstone_ids | {breakpoint_id}
781
+ next_snapshot, slots = self._build_snapshot(
782
+ definitions,
783
+ frozenset(tombstones),
784
+ self._snapshot.catalog_version + 1,
785
+ )
786
+ return self._plan(
787
+ result_id=breakpoint_id,
788
+ snapshot=next_snapshot,
789
+ removed_ids=(breakpoint_id,),
790
+ desired_slots=slots,
791
+ )
792
+
793
+ def prepare_enabled(
794
+ self,
795
+ breakpoint_id: UUID,
796
+ enabled: bool,
797
+ ) -> WorkerBreakpointPlan:
798
+ self._validate_breakpoint_id(breakpoint_id)
799
+ if type(enabled) is not bool:
800
+ raise TypeError("worker breakpoint enabled must be bool")
801
+ with self._lock:
802
+ if breakpoint_id in self._snapshot.tombstone_ids:
803
+ raise ProtocolError("Worker breakpoint has been removed")
804
+ definitions = list(self._definitions(self._snapshot))
805
+ for index, (breakpoint, current_enabled) in enumerate(definitions):
806
+ if breakpoint.id != breakpoint_id:
807
+ continue
808
+ if current_enabled is enabled:
809
+ return self._plan(
810
+ result_id=breakpoint_id,
811
+ snapshot=self._snapshot,
812
+ removed_ids=(),
813
+ )
814
+ definitions[index] = (breakpoint, enabled)
815
+ next_snapshot, slots = self._build_snapshot(
816
+ tuple(definitions),
817
+ self._snapshot.tombstone_ids,
818
+ self._snapshot.catalog_version + 1,
819
+ )
820
+ return self._plan(
821
+ result_id=breakpoint_id,
822
+ snapshot=next_snapshot,
823
+ removed_ids=(),
824
+ desired_slots=slots,
825
+ )
826
+ raise ProtocolError("Worker breakpoint id is unknown to this session")
827
+
828
+ def prepare_generation(
829
+ self,
830
+ candidate_view: WorkerGenerationDebugView,
831
+ policy: WorkerBreakpointReloadPolicy,
832
+ ) -> WorkerBreakpointPlan:
833
+ if type(candidate_view) is not WorkerGenerationDebugView:
834
+ raise TypeError("worker candidate debug view is required")
835
+ if type(policy) is not WorkerBreakpointReloadPolicy:
836
+ raise TypeError("worker breakpoint reload policy is required")
837
+ with self._lock:
838
+ if any(view.handle is candidate_view.handle for view in self._views):
839
+ raise ProtocolError("Worker candidate debug view is already retained")
840
+ incompatibilities = tuple(
841
+ detail
842
+ for status in self._snapshot.statuses
843
+ if (
844
+ detail := self._candidate_incompatibility(
845
+ status.breakpoint,
846
+ candidate_view,
847
+ )
848
+ )
849
+ is not None
850
+ )
851
+ if policy is WorkerBreakpointReloadPolicy.STRICT:
852
+ enabled_ids = {
853
+ status.breakpoint.id
854
+ for status in self._snapshot.statuses
855
+ if status.enabled
856
+ }
857
+ conflicts = tuple(
858
+ detail
859
+ for detail in incompatibilities
860
+ if detail.breakpoint_id in enabled_ids
861
+ )
862
+ if conflicts:
863
+ raise WorkerBreakpointConflict(conflicts)
864
+ removals: tuple[WorkerBreakpointReloadRemoval, ...] = ()
865
+ else:
866
+ removals = incompatibilities
867
+ removed_ids = tuple(item.breakpoint_id for item in removals)
868
+ removed = frozenset(removed_ids)
869
+ definitions = tuple(
870
+ item
871
+ for item in self._definitions(self._snapshot)
872
+ if item[0].id not in removed
873
+ )
874
+ next_views = self._views + (candidate_view,)
875
+ next_tombstones = self._snapshot.tombstone_ids | removed
876
+ next_snapshot, desired_slots = self._build_snapshot(
877
+ definitions,
878
+ frozenset(next_tombstones),
879
+ self._snapshot.catalog_version + 1,
880
+ views=next_views,
881
+ assume_desired_installed=True,
882
+ )
883
+ return self._plan(
884
+ result_id=uuid4(),
885
+ snapshot=next_snapshot,
886
+ removed_ids=removed_ids,
887
+ removal_details=removals,
888
+ desired_slots=desired_slots,
889
+ next_views=next_views,
890
+ )
891
+
892
+ def prepare_release(
893
+ self,
894
+ remaining_views: tuple[WorkerGenerationDebugView, ...],
895
+ ) -> WorkerBreakpointPlan:
896
+ if (
897
+ type(remaining_views) is not tuple
898
+ or any(
899
+ type(view) is not WorkerGenerationDebugView
900
+ for view in remaining_views
901
+ )
902
+ or any(
903
+ all(view is not retained for retained in self._views)
904
+ for view in remaining_views
905
+ )
906
+ ):
907
+ raise TypeError("remaining Worker debug views are invalid")
908
+ with self._lock:
909
+ next_snapshot, desired_slots = self._build_snapshot(
910
+ self._definitions(self._snapshot),
911
+ self._snapshot.tombstone_ids,
912
+ self._snapshot.catalog_version + 1,
913
+ views=remaining_views,
914
+ assume_desired_installed=True,
915
+ )
916
+ return self._plan(
917
+ result_id=uuid4(),
918
+ snapshot=next_snapshot,
919
+ removed_ids=(),
920
+ desired_slots=desired_slots,
921
+ next_views=remaining_views,
922
+ )
923
+
924
+ def quarantine(self, plan: WorkerBreakpointPlan) -> None:
925
+ if type(plan) is not WorkerBreakpointPlan:
926
+ raise TypeError("worker breakpoint plan is required")
927
+ with self._lock:
928
+ if (
929
+ plan.owner is not self._owner
930
+ or plan.expected_catalog_version != self._snapshot.catalog_version
931
+ or plan.proposal_id in self._committed_proposals
932
+ ):
933
+ raise ProtocolError("Worker breakpoint proposal is stale")
934
+ self._quarantined_plans.append(plan)
935
+
936
+ def commit(
937
+ self,
938
+ plan: WorkerBreakpointPlan,
939
+ *,
940
+ workspace_confirmed: bool = False,
941
+ ) -> None:
942
+ if type(plan) is not WorkerBreakpointPlan:
943
+ raise TypeError("worker breakpoint plan is required")
944
+ if type(workspace_confirmed) is not bool:
945
+ raise TypeError("workspace confirmation must be bool")
946
+ with self._lock:
947
+ if (
948
+ plan.owner is not self._owner
949
+ or plan.expected_catalog_version != self._snapshot.catalog_version
950
+ or plan.proposal_id in self._committed_proposals
951
+ ):
952
+ raise ProtocolError("Worker breakpoint proposal is stale")
953
+ snapshot = plan.next_snapshot
954
+ if workspace_confirmed:
955
+ snapshot, desired_slots = self._build_snapshot(
956
+ self._definitions(plan.next_snapshot),
957
+ plan.next_snapshot.tombstone_ids,
958
+ plan.next_snapshot.catalog_version,
959
+ views=plan.next_views,
960
+ installed_slots=frozenset(plan.desired_slots),
961
+ )
962
+ if desired_slots != plan.desired_slots:
963
+ raise ProtocolError(
964
+ "Worker breakpoint proposal changed during commit"
965
+ )
966
+ self._snapshot = snapshot
967
+ self._views = plan.next_views
968
+ if workspace_confirmed:
969
+ self._installed_slots = frozenset(plan.desired_slots)
970
+ self._committed_proposals.add(plan.proposal_id)
971
+
972
+ def status(self, breakpoint_id: UUID) -> WorkerBreakpointStatus:
973
+ self._validate_breakpoint_id(breakpoint_id)
974
+ with self._lock:
975
+ for status in self._snapshot.statuses:
976
+ if status.breakpoint.id == breakpoint_id:
977
+ return status
978
+ raise ProtocolError("Worker breakpoint id is unknown to this session")
979
+
980
+ def list_statuses(self) -> tuple[WorkerBreakpointStatus, ...]:
981
+ with self._lock:
982
+ return self._snapshot.statuses
983
+
984
+ def snapshot(self) -> WorkerBreakpointCatalogSnapshot:
985
+ with self._lock:
986
+ return self._snapshot
987
+
988
+ def bindings_for_view(
989
+ self,
990
+ view: WorkerGenerationDebugView,
991
+ ) -> tuple[WorkerBreakpointBinding, ...]:
992
+ if type(view) is not WorkerGenerationDebugView:
993
+ raise TypeError("worker generation debug view is required")
994
+ with self._lock:
995
+ if all(view is not retained for retained in self._views):
996
+ raise ProtocolError("Worker generation debug view is not retained")
997
+ bindings: list[WorkerBreakpointBinding] = []
998
+ for status in self._snapshot.statuses:
999
+ if not status.enabled:
1000
+ continue
1001
+ breakpoint = status.breakpoint
1002
+ modules = tuple(
1003
+ module
1004
+ for module in view.modules
1005
+ if module.canonical_module == breakpoint.canonical_module
1006
+ and breakpoint.source_unit in module.source_units
1007
+ )
1008
+ if not modules:
1009
+ continue
1010
+ if len(modules) != 1:
1011
+ raise ProtocolError(
1012
+ "Worker generation contains duplicate module views"
1013
+ )
1014
+ mapping = resolve_source_line(
1015
+ modules[0],
1016
+ breakpoint.source_unit,
1017
+ breakpoint.line,
1018
+ )
1019
+ if mapping.generated_line is None:
1020
+ continue
1021
+ bindings.append(
1022
+ WorkerBreakpointBinding(
1023
+ breakpoint.id,
1024
+ view.handle,
1025
+ modules[0].registration.module_location(
1026
+ mapping.generated_line
1027
+ ),
1028
+ )
1029
+ )
1030
+ return tuple(
1031
+ sorted(
1032
+ bindings,
1033
+ key=lambda item: (
1034
+ _module_location_key(item.location),
1035
+ item.breakpoint_id.hex,
1036
+ ),
1037
+ )
1038
+ )
1039
+
1040
+ @staticmethod
1041
+ def _validate_request(
1042
+ source_unit: SourceUnitRef,
1043
+ canonical_module: str,
1044
+ line: int,
1045
+ *,
1046
+ enabled: bool,
1047
+ column: int | None,
1048
+ ) -> None:
1049
+ if not isinstance(source_unit, SourceUnitRef):
1050
+ raise TypeError("worker breakpoint source unit is required")
1051
+ if type(canonical_module) is not str or not canonical_module:
1052
+ raise ValueError("worker breakpoint module is required")
1053
+ if type(line) is not int or line < 1:
1054
+ raise ValueError("worker breakpoint line must be positive")
1055
+ if type(enabled) is not bool:
1056
+ raise TypeError("worker breakpoint enabled must be bool")
1057
+ if column is not None:
1058
+ raise ValueError("worker breakpoint columns are not supported")
1059
+
1060
+ @staticmethod
1061
+ def _validate_breakpoint_id(breakpoint_id: UUID) -> None:
1062
+ if type(breakpoint_id) is not UUID:
1063
+ raise TypeError("worker breakpoint id must be a UUID")
1064
+
1065
+ @staticmethod
1066
+ def _definitions(
1067
+ snapshot: WorkerBreakpointCatalogSnapshot,
1068
+ ) -> tuple[tuple[WorkerBreakpoint, bool], ...]:
1069
+ return tuple((status.breakpoint, status.enabled) for status in snapshot.statuses)
1070
+
1071
+ def _plan(
1072
+ self,
1073
+ *,
1074
+ result_id: UUID,
1075
+ snapshot: WorkerBreakpointCatalogSnapshot,
1076
+ removed_ids: tuple[UUID, ...],
1077
+ removal_details: tuple[WorkerBreakpointReloadRemoval, ...] = (),
1078
+ desired_slots: tuple[ModuleLocation, ...] | None = None,
1079
+ next_views: tuple[WorkerGenerationDebugView, ...] | None = None,
1080
+ ) -> WorkerBreakpointPlan:
1081
+ if next_views is None:
1082
+ next_views = self._views
1083
+ if desired_slots is None:
1084
+ _ignored, desired_slots = self._build_snapshot(
1085
+ self._definitions(snapshot),
1086
+ snapshot.tombstone_ids,
1087
+ snapshot.catalog_version,
1088
+ )
1089
+ return WorkerBreakpointPlan(
1090
+ self._owner,
1091
+ uuid4(),
1092
+ self._snapshot.catalog_version,
1093
+ result_id,
1094
+ desired_slots,
1095
+ removed_ids,
1096
+ removal_details,
1097
+ next_views,
1098
+ snapshot,
1099
+ )
1100
+
1101
+ def _build_snapshot(
1102
+ self,
1103
+ definitions: tuple[tuple[WorkerBreakpoint, bool], ...],
1104
+ tombstones: frozenset[UUID],
1105
+ version: int,
1106
+ *,
1107
+ views: tuple[WorkerGenerationDebugView, ...] | None = None,
1108
+ installed_slots: frozenset[ModuleLocation] | None = None,
1109
+ assume_desired_installed: bool = False,
1110
+ ) -> tuple[WorkerBreakpointCatalogSnapshot, tuple[ModuleLocation, ...]]:
1111
+ statuses: list[WorkerBreakpointStatus] = []
1112
+ desired: set[ModuleLocation] = set()
1113
+ active_views = self._views if views is None else views
1114
+ active_installed_slots = (
1115
+ self._installed_slots if installed_slots is None else installed_slots
1116
+ )
1117
+ for breakpoint, enabled in definitions:
1118
+ status, slots = self._resolve_breakpoint(
1119
+ breakpoint,
1120
+ enabled=enabled,
1121
+ version=version,
1122
+ views=active_views,
1123
+ installed_slots=active_installed_slots,
1124
+ assume_desired_installed=assume_desired_installed,
1125
+ )
1126
+ statuses.append(status)
1127
+ if enabled:
1128
+ desired.update(slots)
1129
+ desired_slots = tuple(sorted(desired, key=_module_location_key))
1130
+ return (
1131
+ WorkerBreakpointCatalogSnapshot(version, tuple(statuses), tombstones),
1132
+ desired_slots,
1133
+ )
1134
+
1135
+ def _resolve_breakpoint(
1136
+ self,
1137
+ breakpoint: WorkerBreakpoint,
1138
+ *,
1139
+ enabled: bool,
1140
+ version: int,
1141
+ views: tuple[WorkerGenerationDebugView, ...],
1142
+ installed_slots: frozenset[ModuleLocation],
1143
+ assume_desired_installed: bool,
1144
+ ) -> tuple[WorkerBreakpointStatus, tuple[ModuleLocation, ...]]:
1145
+ generation_statuses: list[WorkerBreakpointGenerationStatus] = []
1146
+ resolved_slots: set[ModuleLocation] = set()
1147
+ exact_rejections: list[WorkerBreakpointRejection] = []
1148
+ saw_module = False
1149
+ for generation_view in views:
1150
+ module_views = tuple(
1151
+ module
1152
+ for module in generation_view.modules
1153
+ if module.canonical_module == breakpoint.canonical_module
1154
+ )
1155
+ if len(module_views) > 1:
1156
+ raise ProtocolError("Worker generation contains duplicate module views")
1157
+ if not module_views:
1158
+ generation_statuses.append(
1159
+ WorkerBreakpointGenerationStatus(
1160
+ generation_view.handle,
1161
+ WorkerBreakpointResolution.PENDING,
1162
+ WorkerBreakpointRejection.NO_RETAINED_COMPATIBLE_ARTIFACT,
1163
+ )
1164
+ )
1165
+ continue
1166
+ saw_module = True
1167
+ module = module_views[0]
1168
+ if breakpoint.source_unit not in module.source_units:
1169
+ generation_statuses.append(
1170
+ WorkerBreakpointGenerationStatus(
1171
+ generation_view.handle,
1172
+ WorkerBreakpointResolution.PENDING,
1173
+ WorkerBreakpointRejection.SOURCE_REVISION_NOT_ADMITTED,
1174
+ )
1175
+ )
1176
+ continue
1177
+ mapping = resolve_source_line(
1178
+ module,
1179
+ breakpoint.source_unit,
1180
+ breakpoint.line,
1181
+ )
1182
+ if mapping.reason is not None:
1183
+ exact_rejections.append(mapping.reason)
1184
+ generation_statuses.append(
1185
+ WorkerBreakpointGenerationStatus(
1186
+ generation_view.handle,
1187
+ WorkerBreakpointResolution.REJECTED,
1188
+ mapping.reason,
1189
+ )
1190
+ )
1191
+ continue
1192
+ if mapping.generated_line is None:
1193
+ raise ProtocolError("Worker breakpoint resolution lost generated line")
1194
+ slot = module.registration.module_location(mapping.generated_line)
1195
+ resolved_slots.add(slot)
1196
+ generation_statuses.append(
1197
+ WorkerBreakpointGenerationStatus(
1198
+ generation_view.handle,
1199
+ WorkerBreakpointResolution.RESOLVED,
1200
+ None,
1201
+ )
1202
+ )
1203
+
1204
+ if resolved_slots:
1205
+ resolution = WorkerBreakpointResolution.RESOLVED
1206
+ reason = None
1207
+ elif exact_rejections:
1208
+ resolution = WorkerBreakpointResolution.REJECTED
1209
+ reason = (
1210
+ exact_rejections[0]
1211
+ if len(set(exact_rejections)) == 1
1212
+ else WorkerBreakpointRejection.AMBIGUOUS_LINE_MAPPING
1213
+ )
1214
+ else:
1215
+ resolution = WorkerBreakpointResolution.PENDING
1216
+ reason = (
1217
+ WorkerBreakpointRejection.SOURCE_REVISION_NOT_ADMITTED
1218
+ if saw_module
1219
+ else WorkerBreakpointRejection.NO_RETAINED_COMPATIBLE_ARTIFACT
1220
+ )
1221
+ installed_count = (
1222
+ (
1223
+ len(resolved_slots)
1224
+ if assume_desired_installed
1225
+ else len(resolved_slots & installed_slots)
1226
+ )
1227
+ if enabled and resolution is WorkerBreakpointResolution.RESOLVED
1228
+ else 0
1229
+ )
1230
+ status = WorkerBreakpointStatus(
1231
+ breakpoint,
1232
+ enabled,
1233
+ resolution,
1234
+ reason,
1235
+ tuple(generation_statuses),
1236
+ installed_count,
1237
+ version,
1238
+ )
1239
+ return status, tuple(sorted(resolved_slots, key=_module_location_key))
1240
+
1241
+ @staticmethod
1242
+ def _candidate_incompatibility(
1243
+ breakpoint: WorkerBreakpoint,
1244
+ candidate_view: WorkerGenerationDebugView,
1245
+ ) -> WorkerBreakpointReloadRemoval | None:
1246
+ modules = tuple(
1247
+ module
1248
+ for module in candidate_view.modules
1249
+ if module.canonical_module == breakpoint.canonical_module
1250
+ )
1251
+ if len(modules) > 1:
1252
+ raise ProtocolError("Worker generation contains duplicate module views")
1253
+ if not modules:
1254
+ reason = WorkerBreakpointRejection.MODULE_NOT_IN_CANDIDATE
1255
+ elif breakpoint.source_unit not in modules[0].source_units:
1256
+ reason = WorkerBreakpointRejection.SOURCE_IDENTITY_MISMATCH
1257
+ else:
1258
+ mapping = resolve_source_line(
1259
+ modules[0],
1260
+ breakpoint.source_unit,
1261
+ breakpoint.line,
1262
+ )
1263
+ reason = mapping.reason
1264
+ return (
1265
+ None
1266
+ if reason is None
1267
+ else WorkerBreakpointReloadRemoval(breakpoint.id, reason)
1268
+ )
1269
+
1270
+
1271
+ def require_exact_worker_module_or_native(
1272
+ location: ModuleLocation,
1273
+ view: WorkerGenerationDebugView,
1274
+ ) -> WorkerModuleDebugView | None:
1275
+ if type(location) is not ModuleLocation:
1276
+ raise TypeError("debug module location is required")
1277
+ if type(view) is not WorkerGenerationDebugView:
1278
+ raise TypeError("worker generation debug view is required")
1279
+ if type(location.line) is not int or location.line < 1:
1280
+ raise ProtocolError("Debug frame line is invalid")
1281
+ expected_locations = tuple(
1282
+ (module, module.registration.module_location(location.line))
1283
+ for module in view.modules
1284
+ )
1285
+ exact = tuple(
1286
+ module
1287
+ for module, expected in expected_locations
1288
+ if location == expected
1289
+ )
1290
+ if len(exact) == 1:
1291
+ return exact[0]
1292
+ if len(exact) > 1:
1293
+ raise ProtocolError("Worker physical module identity is ambiguous")
1294
+ for _module, expected in expected_locations:
1295
+ same_worker_shape = (
1296
+ location.module_type == expected.module_type
1297
+ and location.object_id == expected.object_id
1298
+ and location.property_id == expected.property_id
1299
+ and location.extension_name == expected.extension_name
1300
+ and location.ext_id == expected.ext_id
1301
+ )
1302
+ if location.url == expected.url or same_worker_shape:
1303
+ raise ProtocolError("Worker physical module identity is stale or forged")
1304
+ return None
1305
+
1306
+
1307
+ def mint_worker_frame(
1308
+ level: int,
1309
+ source: WorkerSourceLocation,
1310
+ view: WorkerGenerationDebugView,
1311
+ module: WorkerModuleDebugView,
1312
+ raw_frame: StackFrame,
1313
+ ) -> WorkerMappedFrame:
1314
+ if (
1315
+ type(level) is not int
1316
+ or level < 0
1317
+ or type(source) is not WorkerSourceLocation
1318
+ or type(view) is not WorkerGenerationDebugView
1319
+ or type(module) is not WorkerModuleDebugView
1320
+ or type(raw_frame) is not StackFrame
1321
+ or all(module is not candidate for candidate in view.modules)
1322
+ or raw_frame.level != level
1323
+ ):
1324
+ raise ProtocolError("Worker frame provenance is inconsistent")
1325
+ return WorkerMappedFrame(
1326
+ level,
1327
+ source,
1328
+ view.handle,
1329
+ module.artifact_sha256,
1330
+ module.source_map_sha256,
1331
+ WorkerFrameProvenance(
1332
+ _WORKER_FRAME_PROVENANCE_OWNER,
1333
+ raw_frame,
1334
+ module.registration.target_incarnation_id,
1335
+ ),
1336
+ )
1337
+
1338
+
1339
+ def _stop_stack_frames(event: StopEvent) -> tuple[StackFrame, ...]:
1340
+ if type(event) is not StopEvent or type(event.target_id) is not TargetId:
1341
+ raise TypeError("RDBG stop event is required")
1342
+ if event.stack_frames:
1343
+ frames = event.stack_frames
1344
+ levels = tuple(frame.level for frame in frames)
1345
+ if (
1346
+ any(type(frame) is not StackFrame for frame in frames)
1347
+ or any(frame.target_id != event.target_id for frame in frames)
1348
+ or any(type(level) is not int or level < 0 for level in levels)
1349
+ or len(set(levels)) != len(levels)
1350
+ ):
1351
+ raise ProtocolError("Worker debug stack is incoherent")
1352
+ frames = tuple(sorted(frames, key=lambda frame: frame.level))
1353
+ if frames[0].level != 0 or frames[0].location != event.location:
1354
+ raise ProtocolError("Worker debug stack top does not match the stop")
1355
+ if event.stack and tuple(frame.location for frame in frames) != event.stack:
1356
+ raise ProtocolError("Worker debug stack representations disagree")
1357
+ return frames
1358
+ locations = event.stack or (event.location,)
1359
+ if locations[0] != event.location:
1360
+ raise ProtocolError("Worker debug stack top does not match the stop")
1361
+ return tuple(
1362
+ StackFrame(event.target_id, level, location)
1363
+ for level, location in enumerate(locations)
1364
+ )
1365
+
1366
+
1367
+ def map_worker_stop(
1368
+ event: StopEvent,
1369
+ *,
1370
+ operation_id: int,
1371
+ view: WorkerGenerationDebugView,
1372
+ origin: str,
1373
+ bindings: tuple[WorkerBreakpointBinding, ...],
1374
+ reason: StopReason = StopReason.USER_BREAKPOINT,
1375
+ ) -> RuntimeDebugStop:
1376
+ if type(operation_id) is not int or operation_id < 0:
1377
+ raise ValueError("debug stop operation id is invalid")
1378
+ if type(view) is not WorkerGenerationDebugView:
1379
+ raise TypeError("worker generation debug view is required")
1380
+ if origin not in {"main", "capture_evaluation"}:
1381
+ raise ValueError("debug stop origin is invalid")
1382
+ if (
1383
+ type(bindings) is not tuple
1384
+ or any(type(binding) is not WorkerBreakpointBinding for binding in bindings)
1385
+ or any(binding.generation is not view.handle for binding in bindings)
1386
+ ):
1387
+ raise ProtocolError("Worker breakpoint bindings do not match generation")
1388
+ if type(reason) is not StopReason:
1389
+ raise TypeError("debug stop reason is required")
1390
+
1391
+ top_module = require_exact_worker_module_or_native(event.location, view)
1392
+ if top_module is None:
1393
+ public_location: WorkerSourceLocation | ModuleLocation = event.location
1394
+ else:
1395
+ mapped_top = map_generated_line(top_module, event.location.line)
1396
+ if mapped_top is None:
1397
+ raise ProtocolError("Worker stopped in synthetic generated source")
1398
+ public_location = mapped_top
1399
+
1400
+ visible_frames: list[WorkerMappedFrame | NativeDebugFrame] = []
1401
+ for raw_frame in _stop_stack_frames(event):
1402
+ module = require_exact_worker_module_or_native(raw_frame.location, view)
1403
+ if module is None:
1404
+ visible_frames.append(
1405
+ NativeDebugFrame(raw_frame.level, raw_frame.location)
1406
+ )
1407
+ continue
1408
+ source = map_generated_line(module, raw_frame.location.line)
1409
+ if source is None:
1410
+ continue
1411
+ visible_frames.append(
1412
+ mint_worker_frame(raw_frame.level, source, view, module, raw_frame)
1413
+ )
1414
+ breakpoint_ids = tuple(
1415
+ sorted(
1416
+ {
1417
+ binding.breakpoint_id
1418
+ for binding in bindings
1419
+ if binding.location == event.location
1420
+ },
1421
+ key=lambda item: item.hex,
1422
+ )
1423
+ )
1424
+ return RuntimeDebugStop(
1425
+ reason,
1426
+ origin,
1427
+ operation_id,
1428
+ public_location,
1429
+ tuple(visible_frames),
1430
+ breakpoint_ids,
1431
+ )