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,2644 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from hashlib import sha256
6
+ from math import isfinite
7
+ import json
8
+ import re
9
+ from collections.abc import Mapping, Sequence
10
+ from time import monotonic
11
+ from typing import Callable
12
+ from functools import lru_cache
13
+ from uuid import UUID, uuid4
14
+
15
+ from onec_runtime.bsl import (
16
+ DiagnosticStage,
17
+ LoweringMode,
18
+ MappedSource,
19
+ NormalizedDiagnostic,
20
+ SemanticNotebookLowerer,
21
+ SourceArtifactKind,
22
+ SourceSpan,
23
+ SourceTransformBuilder,
24
+ SourceUnitKind,
25
+ SourceUnitRef,
26
+ VisibleSourceContext,
27
+ mapped_visible_source,
28
+ parse_platform_diagnostic,
29
+ remap_platform_diagnostic,
30
+ source_sha256,
31
+ )
32
+ from onec_runtime.bsl.parser_target import PythonParserTarget
33
+ from onec_runtime.capture import (
34
+ build_capture_transfer_call,
35
+ build_live_capture_begin_call,
36
+ build_live_capture_end_call,
37
+ build_live_capture_root_transfer_call,
38
+ build_live_current_capture_call,
39
+ build_temporary_storage_value_expression,
40
+ )
41
+ from onec_runtime.breakpoint_workspace import (
42
+ BreakpointWorkspaceController,
43
+ BreakpointWorkspaceOutcomeUnknown,
44
+ WorkspaceInstallReceipt,
45
+ WorkspaceSnapshot,
46
+ )
47
+ from onec_runtime.errors import BslExecutionError, ProtocolError, RdbgTransportError
48
+ from onec_runtime.experiment import bsl_string_literal
49
+ from onec_runtime.fault_injection import FaultPoint
50
+ from onec_runtime.rdbg.models import (
51
+ EvaluationResult,
52
+ FrameVariable,
53
+ ModuleLocation,
54
+ PendingEvaluation,
55
+ StackFrame,
56
+ StopEvent,
57
+ TargetId,
58
+ )
59
+ from onec_runtime.rdbg.reconnect import ReconnectedSession
60
+ from onec_runtime.rdbg.session import RdbgSession
61
+ from onec_runtime.recovery import (
62
+ RecoveryCheckpoint,
63
+ RecoveryOutcome,
64
+ RecoveryPhase,
65
+ RecoveryResult,
66
+ RootWriteRecord,
67
+ SideEffectStatus,
68
+ validate_paused_identity,
69
+ )
70
+ from onec_runtime.recovery_journal import RecoveryJournal
71
+ from onec_runtime.stop_routing import (
72
+ BreakpointRegistry,
73
+ ClassifiedStop,
74
+ StopReason,
75
+ classify_stop,
76
+ )
77
+ from onec_runtime.table_value import evaluation_to_python
78
+
79
+
80
+ MAX_CAPTURE_PROJECTION_POSITION = 10_000_000
81
+ MAX_CAPTURE_SCHEMA_COLUMNS = 100
82
+
83
+
84
+ def _safe_platform_diagnostic(
85
+ message: str,
86
+ executed_source: MappedSource,
87
+ *,
88
+ visible_source_context: VisibleSourceContext | None,
89
+ ) -> NormalizedDiagnostic | None:
90
+ try:
91
+ return remap_platform_diagnostic(
92
+ parse_platform_diagnostic(message),
93
+ executed_source,
94
+ stage=DiagnosticStage.EXECUTION,
95
+ visible_source_context=visible_source_context,
96
+ )
97
+ except Exception:
98
+ return None
99
+
100
+
101
+ class OperationState(Enum):
102
+ IDLE = "idle"
103
+ MAIN_PENDING = "main_pending"
104
+ CAPTURED = "captured"
105
+ EVALUATING_CAPTURE = "evaluating_capture"
106
+ CAPTURE_DEBUG_STOPPED = "capture_debug_stopped"
107
+ DEBUG_STOPPED = "debug_stopped"
108
+ FLUSHING = "flushing"
109
+ PARTIAL_WRITEBACK_FAILURE = "partial_writeback_failure"
110
+ BREAKPOINT_RESTORE_FAILURE = "breakpoint_restore_failure"
111
+ RESUMING = "resuming"
112
+ RECOVERING = "recovering"
113
+ LOST = "lost"
114
+ COMPLETED = "completed"
115
+ FAILED = "failed"
116
+
117
+
118
+ class PartialWritebackError(ProtocolError):
119
+ """At least one staged capture root could not be written to the frame."""
120
+
121
+
122
+ class BreakpointRestoreError(ProtocolError):
123
+ """The full breakpoint workspace could not be restored while paused."""
124
+
125
+
126
+ class CompletionDecodeError(ProtocolError):
127
+ """A MAIN completion scalar could not be decoded without exposing its value."""
128
+
129
+ def __init__(self, phase: str, evaluation: EvaluationResult) -> None:
130
+ super().__init__(f"MAIN completion {phase} cannot be decoded")
131
+ self.phase = phase
132
+ self.evaluation_type = evaluation.type_name
133
+ self.exact_decimal_present = bool(evaluation.value_decimal)
134
+
135
+
136
+ @dataclass(frozen=True, slots=True)
137
+ class OperationHandle:
138
+ operation_id: int
139
+ visible_source: str = field(repr=False)
140
+ lowered_source: str = field(repr=False)
141
+ messages_intercepted: int = 0
142
+ message_collector_key: str = ""
143
+ executed_source: MappedSource | None = field(default=None, repr=False)
144
+ visible_source_context: VisibleSourceContext | None = field(
145
+ default=None,
146
+ repr=False,
147
+ )
148
+
149
+
150
+ @dataclass(frozen=True, slots=True)
151
+ class CapturedStop:
152
+ operation: OperationHandle
153
+ location: ModuleLocation
154
+ stop_sequence: int
155
+ variables: tuple[FrameVariable, ...]
156
+ observed_command_id: int | None = None
157
+
158
+
159
+ @dataclass(frozen=True, slots=True)
160
+ class DebugStop:
161
+ operation: OperationHandle
162
+ stop: StopEvent
163
+ reason: StopReason
164
+
165
+
166
+ @dataclass(frozen=True, slots=True)
167
+ class _PendingCaptureEvaluation:
168
+ pending: PendingEvaluation
169
+ operation: OperationHandle
170
+ visible_source: str = field(repr=False)
171
+ lowered_source: str = field(repr=False)
172
+ executed_source: MappedSource = field(repr=False)
173
+ visible_source_context: VisibleSourceContext | None = field(repr=False)
174
+ messages_intercepted: int
175
+ message_collector_key: str
176
+ cell_fields: Mapping[str, object] = field(repr=False)
177
+
178
+
179
+ @dataclass(frozen=True, slots=True)
180
+ class BreakpointWorkspaceEvent:
181
+ phase: str
182
+ locations: tuple[ModuleLocation, ...]
183
+
184
+
185
+ @dataclass(frozen=True, slots=True)
186
+ class ContinuationAttemptSpec:
187
+ """Opaque identity binding one continuation to its exact ordered roots."""
188
+
189
+ attempt_id: str
190
+ capture_generation: int
191
+ request_operation_id: str
192
+ dirty_roots: tuple[str, ...]
193
+
194
+ def __post_init__(self) -> None:
195
+ if not isinstance(self.attempt_id, str) or not self.attempt_id or len(self.attempt_id) > 256:
196
+ raise ValueError("continuation attempt_id is invalid")
197
+ if type(self.capture_generation) is not int or self.capture_generation <= 0:
198
+ raise ValueError("continuation capture_generation must be positive")
199
+ if (
200
+ not isinstance(self.request_operation_id, str)
201
+ or not self.request_operation_id
202
+ or len(self.request_operation_id) > 256
203
+ ):
204
+ raise ValueError("continuation request_operation_id is invalid")
205
+ roots = tuple(self.dirty_roots)
206
+ if len(roots) > 100:
207
+ raise ValueError("continuation dirty_roots exceeds 100")
208
+ if any(
209
+ not isinstance(root, str)
210
+ or not root
211
+ or len(root) > 256
212
+ or not root.isidentifier()
213
+ for root in roots
214
+ ):
215
+ raise ValueError("continuation dirty_roots are invalid")
216
+ if len({root.casefold() for root in roots}) != len(roots):
217
+ raise ValueError("continuation dirty_roots must be unique")
218
+ object.__setattr__(self, "dirty_roots", roots)
219
+
220
+
221
+ @dataclass(frozen=True, slots=True)
222
+ class ContinuationAttemptEvidence:
223
+ root_statuses: tuple[tuple[str, str], ...]
224
+ continue_state: str
225
+
226
+
227
+ @dataclass(slots=True)
228
+ class _ContinuationAttemptState:
229
+ spec: ContinuationAttemptSpec
230
+ root_statuses: dict[str, str]
231
+ continue_state: str = "unattempted"
232
+ irreversible_roots: set[str] = field(default_factory=set)
233
+
234
+
235
+ class _ControllerContinuationAdmission:
236
+ """Exact rollback fence for one paused successor preparation."""
237
+
238
+ def __init__(
239
+ self,
240
+ controller: "PrototypeRuntimeController",
241
+ *,
242
+ registry: BreakpointRegistry,
243
+ capture_points: tuple[ModuleLocation, ...],
244
+ workspace: tuple[ModuleLocation, ...],
245
+ state: OperationState,
246
+ attempt_id: str,
247
+ ) -> None:
248
+ self._controller = controller
249
+ self._registry = registry
250
+ self._capture_points = capture_points
251
+ self._workspace = workspace
252
+ self._state = state
253
+ self.attempt_id = attempt_id
254
+ self._closed = False
255
+
256
+ def commit(self) -> None:
257
+ self._closed = True
258
+
259
+ def rollback(self) -> None:
260
+ if self._closed:
261
+ return
262
+ attempt = self._controller._continuation_attempts[self.attempt_id]
263
+ unsafe = (
264
+ bool(attempt.irreversible_roots)
265
+ or attempt.continue_state != "unattempted"
266
+ or self._controller.state is OperationState.RECOVERING
267
+ )
268
+ if unsafe:
269
+ self.quarantine()
270
+ raise ProtocolError("continuation admission has irreversible evidence")
271
+ try:
272
+ self._controller._set_workspace(
273
+ "capture_successor_restore", self._workspace
274
+ )
275
+ except BaseException as error:
276
+ self.quarantine()
277
+ raise ProtocolError(
278
+ "capture successor breakpoint rollback is uncertain"
279
+ ) from error
280
+ self._controller.registry = self._registry
281
+ self._controller.capture_points = self._capture_points
282
+ self._controller.state = self._state
283
+ self._closed = True
284
+
285
+ def quarantine(self) -> None:
286
+ self._controller._clear_capture_inspection()
287
+ self._controller.state = OperationState.RECOVERING
288
+ self._closed = True
289
+
290
+
291
+ @dataclass(frozen=True, slots=True)
292
+ class CaptureCellResult:
293
+ operation_id: int
294
+ visible_source: str = field(repr=False)
295
+ lowered_source: str = field(repr=False)
296
+ result: object
297
+ messages: tuple[str, ...] = ()
298
+
299
+
300
+ @dataclass(frozen=True, slots=True)
301
+ class MainCompletion:
302
+ operation: OperationHandle
303
+ result: object
304
+ error: str
305
+ succeeded: bool
306
+ messages: tuple[str, ...] = ()
307
+ diagnostic: NormalizedDiagnostic | None = None
308
+
309
+
310
+ @lru_cache(maxsize=1)
311
+ def _semantic_parser_target() -> PythonParserTarget:
312
+ return PythonParserTarget.from_generated()
313
+
314
+
315
+ class PrototypeRuntimeController:
316
+ """Single-writer in-process facade for the PrototypeProfile."""
317
+
318
+ def __init__(
319
+ self,
320
+ session: RdbgSession,
321
+ service_location: ModuleLocation,
322
+ *,
323
+ kernel_location: ModuleLocation | None = None,
324
+ command_timeout_s: float = 60.0,
325
+ runtime_generation: int = 1,
326
+ journal: RecoveryJournal | None = None,
327
+ fault_hook: Callable[[FaultPoint], None] | None = None,
328
+ on_generation_lost: Callable[[RecoveryCheckpoint], None] | None = None,
329
+ ) -> None:
330
+ self.session = session
331
+ self.service_location = service_location
332
+ self.kernel_location = kernel_location or service_location
333
+ self.command_timeout_s = command_timeout_s
334
+ self.runtime_generation = runtime_generation
335
+ self.journal = journal or RecoveryJournal()
336
+ self.fault_hook = fault_hook
337
+ self.on_generation_lost = on_generation_lost
338
+ self.lowerer = SemanticNotebookLowerer(_semantic_parser_target().new_instance())
339
+ self.state = OperationState.IDLE
340
+ self.operation_id = 0
341
+ self.active_operation: OperationHandle | None = None
342
+ self.capture_points: tuple[ModuleLocation, ...] = ()
343
+ self.registry = BreakpointRegistry(service_location)
344
+ self.stop_sequence = 0
345
+ self.last_capture_location: ModuleLocation | None = None
346
+ self.capture_kernel_stack_level: int | None = None
347
+ self.capture_frame_stack_level: int | None = None
348
+ self._capture_frame_variables: tuple[FrameVariable, ...] = ()
349
+ self._capture_stack_frames: tuple[StackFrame, ...] = ()
350
+ self._capture_target_id: TargetId | None = None
351
+ self._capture_manager_paths: dict[str, str] = {}
352
+ self._capture_value_paths: dict[str, str] = {}
353
+ self._capture_metadata_handles: set[str] = set()
354
+ self.checkpoint_sequence = 0
355
+ self.recovery_checkpoint: RecoveryCheckpoint | None = None
356
+ self.continue_sent = False
357
+ self._generation_lost_notified = False
358
+ self.cell_sequence = 0
359
+ self.side_effect_sequence = 0
360
+ self.write_journal: list[RootWriteRecord] = []
361
+ self.stop_history: list[ClassifiedStop] = []
362
+ self.last_debug_stop: DebugStop | None = None
363
+ self.pending_capture_evaluation: _PendingCaptureEvaluation | None = None
364
+ self.breakpoint_workspaces: list[BreakpointWorkspaceEvent] = []
365
+ self._breakpoint_workspace = self.registry.full_locations
366
+ self.breakpoint_workspace_owner = BreakpointWorkspaceController(
367
+ session,
368
+ WorkspaceSnapshot(0, service_location, (), (), (), False),
369
+ )
370
+ self._continuation_attempts: dict[str, _ContinuationAttemptState] = {}
371
+ self._active_continuation_attempt_id: str | None = None
372
+
373
+ @staticmethod
374
+ def _capture_identifier(value: object, *, name: str) -> str:
375
+ if (
376
+ not isinstance(value, str)
377
+ or not value
378
+ or len(value) > 256
379
+ or re.fullmatch(r"[^\W\d]\w*", value, re.UNICODE) is None
380
+ ):
381
+ raise ProtocolError(f"capture {name} is invalid")
382
+ return value
383
+
384
+ def _require_capture_frame(self) -> int:
385
+ self._require_state(OperationState.CAPTURED)
386
+ if self.capture_frame_stack_level is None:
387
+ raise ProtocolError("capture frame is not initialized")
388
+ return self.capture_frame_stack_level
389
+
390
+ def _clear_capture_inspection(self) -> None:
391
+ self.capture_frame_stack_level = None
392
+ self._capture_frame_variables = ()
393
+ self._capture_stack_frames = ()
394
+ self._capture_target_id = None
395
+ self._capture_manager_paths.clear()
396
+ self._capture_value_paths.clear()
397
+ self._capture_metadata_handles.clear()
398
+
399
+ def invalidate_capture_inspection(self) -> None:
400
+ """Revoke captured-frame handles without resuming the suspended target."""
401
+ self._clear_capture_inspection()
402
+
403
+ def _capture_command_deadline(self, timeout_s: float | None) -> float:
404
+ selected = self.command_timeout_s if timeout_s is None else timeout_s
405
+ for value in (selected, self.command_timeout_s):
406
+ if (
407
+ isinstance(value, bool)
408
+ or not isinstance(value, (int, float))
409
+ or not isfinite(float(value))
410
+ or float(value) <= 0
411
+ ):
412
+ raise ProtocolError(
413
+ "capture inspection timeout must be finite and positive"
414
+ )
415
+ return monotonic() + min(float(selected), float(self.command_timeout_s))
416
+
417
+ @staticmethod
418
+ def _capture_remaining_timeout(deadline: float) -> float:
419
+ remaining = deadline - monotonic()
420
+ if remaining <= 0:
421
+ raise ProtocolError("capture inspection deadline exceeded")
422
+ return remaining
423
+
424
+ def _record(self, stream: str, event: str, **fields: object) -> None:
425
+ self.journal.record(stream, event, **fields)
426
+
427
+ def _flush_journal(self) -> None:
428
+ self.journal.flush()
429
+
430
+ def _checkpoint(self, phase: RecoveryPhase) -> RecoveryCheckpoint:
431
+ if self.active_operation is None:
432
+ raise ProtocolError("Recovery checkpoint has no active operation")
433
+ target = self.session.target
434
+ if target is None:
435
+ raise ProtocolError("Recovery checkpoint has no selected target")
436
+ self.checkpoint_sequence += 1
437
+ checkpoint = RecoveryCheckpoint(
438
+ sequence=self.checkpoint_sequence,
439
+ runtime_generation=self.runtime_generation,
440
+ operation_id=self.active_operation.operation_id,
441
+ phase=phase,
442
+ target=target,
443
+ frame_location=self.last_capture_location,
444
+ stop_sequence=self.stop_sequence,
445
+ breakpoint_workspace=(
446
+ self.breakpoint_workspace_owner.confirmed_snapshot.effective_locations
447
+ ),
448
+ write_journal=tuple(self.write_journal),
449
+ continue_sent=self.continue_sent,
450
+ )
451
+ self.recovery_checkpoint = checkpoint
452
+ self._record(
453
+ "recovery-checkpoints.jsonl",
454
+ "checkpoint",
455
+ checkpoint=checkpoint,
456
+ )
457
+ self._flush_journal()
458
+ return checkpoint
459
+
460
+ def _inject(self, point: FaultPoint, phase: RecoveryPhase) -> None:
461
+ checkpoint = self._checkpoint(phase)
462
+ if self.fault_hook is None:
463
+ return
464
+ try:
465
+ self.fault_hook(point)
466
+ except RdbgTransportError as error:
467
+ self.state = OperationState.RECOVERING
468
+ self._record(
469
+ "recovery-transitions.jsonl",
470
+ "transport_lost",
471
+ checkpoint_sequence=checkpoint.sequence,
472
+ phase=phase.value,
473
+ fault_point=point.value,
474
+ error=str(error),
475
+ )
476
+ self._flush_journal()
477
+ raise
478
+
479
+ def _set_root_status(
480
+ self,
481
+ root: str,
482
+ expression_sha256: str,
483
+ status: SideEffectStatus,
484
+ *,
485
+ result_id: UUID | None = None,
486
+ error: str = "",
487
+ ) -> RootWriteRecord:
488
+ existing_index = next(
489
+ (
490
+ index
491
+ for index, entry in enumerate(self.write_journal)
492
+ if entry.root == root
493
+ ),
494
+ None,
495
+ )
496
+ if existing_index is None:
497
+ self.side_effect_sequence += 1
498
+ sequence = self.side_effect_sequence
499
+ else:
500
+ sequence = self.write_journal[existing_index].sequence
501
+ record = RootWriteRecord(
502
+ sequence,
503
+ root,
504
+ expression_sha256,
505
+ status,
506
+ result_id,
507
+ error,
508
+ )
509
+ if existing_index is None:
510
+ self.write_journal.append(record)
511
+ else:
512
+ self.write_journal[existing_index] = record
513
+ return record
514
+
515
+ def _require_state(self, *states: OperationState) -> None:
516
+ if self.state not in states:
517
+ expected = ", ".join(state.value for state in states)
518
+ raise ProtocolError(
519
+ f"Runtime operation requires state {expected}; current state is "
520
+ f"{self.state.value}"
521
+ )
522
+
523
+ @property
524
+ def _allowed_locations(self) -> tuple[ModuleLocation, ...]:
525
+ return self.registry.full_locations
526
+
527
+ @staticmethod
528
+ def _checked_modify(result: object, variable: str) -> None:
529
+ if getattr(result, "error_occurred", False):
530
+ raise ProtocolError(
531
+ f"Failed to modify {variable}: {getattr(result, 'error_text', '')}"
532
+ )
533
+
534
+ def _set_workspace(
535
+ self,
536
+ phase: str,
537
+ locations: tuple[ModuleLocation, ...],
538
+ ) -> None:
539
+ if not locations or locations[0] != self.service_location:
540
+ raise ProtocolError("Breakpoint workspace must retain the service point")
541
+ if phase == "capture-evaluation":
542
+ captures = self.registry.captures
543
+ ordinary_users = self.registry.users
544
+ shielded = True
545
+ else:
546
+ ordinary_users = tuple(
547
+ location
548
+ for location in locations[1:]
549
+ if location in self.registry.users
550
+ )
551
+ captures = tuple(
552
+ location
553
+ for location in locations[1:]
554
+ if location not in ordinary_users
555
+ )
556
+ shielded = False
557
+ try:
558
+ confirmed = self.breakpoint_workspace_owner.confirmed_snapshot
559
+ desired = self.breakpoint_workspace_owner.prepare(
560
+ captures=captures,
561
+ ordinary_users=ordinary_users,
562
+ worker_slots=confirmed.worker_slots,
563
+ shielded=shielded,
564
+ )
565
+ self.breakpoint_workspace_owner.install(desired)
566
+ except BreakpointWorkspaceOutcomeUnknown:
567
+ self.state = OperationState.RECOVERING
568
+ raise
569
+ self._breakpoint_workspace = desired.effective_locations
570
+ self.breakpoint_workspaces.append(
571
+ BreakpointWorkspaceEvent(phase, desired.effective_locations)
572
+ )
573
+
574
+ def install_worker_workspace(
575
+ self,
576
+ snapshot: WorkspaceSnapshot,
577
+ ) -> WorkspaceInstallReceipt:
578
+ try:
579
+ receipt = self.breakpoint_workspace_owner.install(snapshot)
580
+ except BreakpointWorkspaceOutcomeUnknown:
581
+ self.state = OperationState.RECOVERING
582
+ raise
583
+ self._breakpoint_workspace = snapshot.effective_locations
584
+ self.breakpoint_workspaces.append(
585
+ BreakpointWorkspaceEvent("worker", snapshot.effective_locations)
586
+ )
587
+ return receipt
588
+
589
+ def require_debug_workspace_ready(self) -> None:
590
+ try:
591
+ self.breakpoint_workspace_owner.require_confirmed()
592
+ except BreakpointWorkspaceOutcomeUnknown:
593
+ self.state = OperationState.RECOVERING
594
+ raise
595
+
596
+ def rearm_capture_successor(
597
+ self, capture_points: tuple[ModuleLocation, ...]
598
+ ) -> None:
599
+ """Replace only capture breakpoints while the business frame is paused.
600
+
601
+ The debugger registry and physical workspace are one transaction: a
602
+ failed install restores the exact prior registry/workspace before the
603
+ caller may decide that no Continue has been sent.
604
+ """
605
+ self._require_state(OperationState.CAPTURED)
606
+ points = tuple(capture_points)
607
+ previous_registry = self.registry
608
+ previous_points = self.capture_points
609
+ candidate = BreakpointRegistry(
610
+ self.service_location, points, previous_registry.users
611
+ )
612
+ try:
613
+ self._set_workspace("capture_successor", candidate.full_locations)
614
+ except BaseException:
615
+ try:
616
+ self._set_workspace("capture_successor_restore", previous_registry.full_locations)
617
+ except BaseException as rollback_error:
618
+ self.state = OperationState.RECOVERING
619
+ raise ProtocolError("capture successor breakpoint rollback is uncertain") from rollback_error
620
+ raise
621
+ self.registry = candidate
622
+ self.capture_points = points
623
+
624
+ def begin_continuation_admission(
625
+ self,
626
+ attempt: ContinuationAttemptSpec,
627
+ capture_points: tuple[ModuleLocation, ...],
628
+ ) -> _ControllerContinuationAdmission:
629
+ """Begin one durable successor attempt before any root resolver runs."""
630
+ self._require_state(OperationState.CAPTURED)
631
+ if not isinstance(attempt, ContinuationAttemptSpec):
632
+ raise TypeError("attempt must be a ContinuationAttemptSpec")
633
+ if attempt.attempt_id in self._continuation_attempts:
634
+ raise ProtocolError("continuation attempt_id was already used")
635
+ snapshot = _ControllerContinuationAdmission(
636
+ self,
637
+ registry=self.registry,
638
+ capture_points=self.capture_points,
639
+ workspace=self._breakpoint_workspace,
640
+ state=self.state,
641
+ attempt_id=attempt.attempt_id,
642
+ )
643
+ state = _ContinuationAttemptState(
644
+ attempt,
645
+ {root: "unattempted" for root in attempt.dirty_roots},
646
+ )
647
+ self._continuation_attempts[attempt.attempt_id] = state
648
+ self._active_continuation_attempt_id = attempt.attempt_id
649
+ self._record(
650
+ "write-journal.jsonl",
651
+ "continuation_attempt_started",
652
+ attempt_id=attempt.attempt_id,
653
+ runtime_generation=self.runtime_generation,
654
+ operation_id=self.active_operation.operation_id if self.active_operation else 0,
655
+ capture_generation=attempt.capture_generation,
656
+ request_operation_id=attempt.request_operation_id,
657
+ dirty_roots=list(attempt.dirty_roots),
658
+ )
659
+ for root in attempt.dirty_roots:
660
+ self._record(
661
+ "write-journal.jsonl",
662
+ "root_write_planned",
663
+ attempt_id=attempt.attempt_id,
664
+ runtime_generation=self.runtime_generation,
665
+ operation_id=self.active_operation.operation_id if self.active_operation else 0,
666
+ root=root,
667
+ )
668
+ try:
669
+ self._flush_journal()
670
+ except BaseException:
671
+ snapshot.quarantine()
672
+ raise
673
+ points = tuple(capture_points)
674
+ candidate = BreakpointRegistry(
675
+ self.service_location, points, self.registry.users
676
+ )
677
+ try:
678
+ self._set_workspace(
679
+ "capture_successor", candidate.full_locations
680
+ )
681
+ self.registry = candidate
682
+ self.capture_points = points
683
+ except BaseException as error:
684
+ if isinstance(error, RdbgTransportError):
685
+ snapshot.quarantine()
686
+ raise
687
+ try:
688
+ snapshot.rollback()
689
+ except BaseException:
690
+ pass
691
+ raise
692
+ return snapshot
693
+
694
+ def continuation_attempt_evidence(
695
+ self, attempt_id: str
696
+ ) -> ContinuationAttemptEvidence:
697
+ try:
698
+ attempt = self._continuation_attempts[attempt_id]
699
+ except KeyError as error:
700
+ raise ProtocolError("continuation attempt is unknown") from error
701
+ return ContinuationAttemptEvidence(
702
+ tuple(
703
+ (root, attempt.root_statuses[root])
704
+ for root in attempt.spec.dirty_roots
705
+ ),
706
+ attempt.continue_state,
707
+ )
708
+
709
+ def _continuation_attempt(
710
+ self,
711
+ dirty_roots: tuple[str, ...],
712
+ attempt_id: str | None,
713
+ ) -> _ContinuationAttemptState:
714
+ if attempt_id is None:
715
+ spec = ContinuationAttemptSpec(
716
+ f"internal-{uuid4().hex}",
717
+ max(1, self.stop_sequence),
718
+ f"controller-{self.operation_id}",
719
+ dirty_roots,
720
+ )
721
+ state = _ContinuationAttemptState(
722
+ spec, {root: "unattempted" for root in dirty_roots}
723
+ )
724
+ self._continuation_attempts[spec.attempt_id] = state
725
+ self._active_continuation_attempt_id = spec.attempt_id
726
+ self._record(
727
+ "write-journal.jsonl",
728
+ "continuation_attempt_started",
729
+ attempt_id=spec.attempt_id,
730
+ runtime_generation=self.runtime_generation,
731
+ operation_id=self.operation_id,
732
+ capture_generation=spec.capture_generation,
733
+ request_operation_id=spec.request_operation_id,
734
+ dirty_roots=list(dirty_roots),
735
+ )
736
+ for root in dirty_roots:
737
+ self._record(
738
+ "write-journal.jsonl",
739
+ "root_write_planned",
740
+ attempt_id=spec.attempt_id,
741
+ runtime_generation=self.runtime_generation,
742
+ operation_id=self.operation_id,
743
+ root=root,
744
+ )
745
+ self._flush_journal()
746
+ return state
747
+ try:
748
+ state = self._continuation_attempts[attempt_id]
749
+ except KeyError as error:
750
+ raise ProtocolError("continuation attempt is unknown") from error
751
+ if state.spec.dirty_roots != dirty_roots:
752
+ raise ProtocolError("continuation attempt dirty_roots changed")
753
+ return state
754
+
755
+ def _mark_continuation_root(
756
+ self,
757
+ attempt: _ContinuationAttemptState,
758
+ root: str,
759
+ status: str,
760
+ *,
761
+ error: str = "",
762
+ ) -> None:
763
+ attempt.root_statuses[root] = status
764
+ if status in {"sent", "succeeded", "outcome_unknown"}:
765
+ attempt.irreversible_roots.add(root)
766
+ self._record(
767
+ "write-journal.jsonl",
768
+ f"root_write_{status}",
769
+ attempt_id=attempt.spec.attempt_id,
770
+ runtime_generation=self.runtime_generation,
771
+ operation_id=self.operation_id,
772
+ root=root,
773
+ error=error,
774
+ )
775
+ self._flush_journal()
776
+
777
+ def _mark_later_roots_unattempted(
778
+ self, attempt: _ContinuationAttemptState, failed_root: str
779
+ ) -> None:
780
+ start = attempt.spec.dirty_roots.index(failed_root) + 1
781
+ for root in attempt.spec.dirty_roots[start:]:
782
+ if attempt.root_statuses[root] == "unattempted":
783
+ self._record(
784
+ "write-journal.jsonl",
785
+ "root_write_unattempted",
786
+ attempt_id=attempt.spec.attempt_id,
787
+ runtime_generation=self.runtime_generation,
788
+ operation_id=self.operation_id,
789
+ root=root,
790
+ )
791
+ self._flush_journal()
792
+
793
+ def execute_main(
794
+ self,
795
+ source: str,
796
+ *,
797
+ capture_points: tuple[ModuleLocation, ...] = (),
798
+ user_breakpoints: tuple[ModuleLocation, ...] = (),
799
+ on_transport_dispatch: Callable[[], None] | None = None,
800
+ ) -> MainCompletion | CapturedStop | DebugStop:
801
+ return self._execute_main(
802
+ source,
803
+ None,
804
+ capture_points=capture_points,
805
+ user_breakpoints=user_breakpoints,
806
+ on_transport_dispatch=on_transport_dispatch,
807
+ )
808
+
809
+ def execute_lowered_main(
810
+ self,
811
+ visible_source: str,
812
+ lowered_source: str,
813
+ *,
814
+ messages_intercepted: int = 0,
815
+ message_collector_key: str = "",
816
+ capture_points: tuple[ModuleLocation, ...] = (),
817
+ user_breakpoints: tuple[ModuleLocation, ...] = (),
818
+ on_transport_dispatch: Callable[[], None] | None = None,
819
+ ) -> MainCompletion | CapturedStop | DebugStop:
820
+ return self._execute_main(
821
+ visible_source,
822
+ lowered_source,
823
+ messages_intercepted=messages_intercepted,
824
+ message_collector_key=message_collector_key,
825
+ capture_points=capture_points,
826
+ user_breakpoints=user_breakpoints,
827
+ on_transport_dispatch=on_transport_dispatch,
828
+ )
829
+
830
+ def execute_mapped_main(
831
+ self,
832
+ visible_source: str,
833
+ lowered_source: MappedSource,
834
+ *,
835
+ visible_source_context: VisibleSourceContext,
836
+ messages_intercepted: int = 0,
837
+ message_collector_key: str = "",
838
+ worker_messages: bool = False,
839
+ worker_globals: tuple[str, ...] = (),
840
+ capture_points: tuple[ModuleLocation, ...] = (),
841
+ user_breakpoints: tuple[ModuleLocation, ...] = (),
842
+ on_transport_dispatch: Callable[[], None] | None = None,
843
+ ) -> MainCompletion | CapturedStop | DebugStop:
844
+ return self._execute_main(
845
+ visible_source,
846
+ lowered_source,
847
+ visible_source_context=visible_source_context,
848
+ messages_intercepted=messages_intercepted,
849
+ message_collector_key=message_collector_key,
850
+ worker_messages=worker_messages,
851
+ worker_globals=worker_globals,
852
+ capture_points=capture_points,
853
+ user_breakpoints=user_breakpoints,
854
+ on_transport_dispatch=on_transport_dispatch,
855
+ )
856
+
857
+ def execute_system_main(self, source: str) -> MainCompletion:
858
+ mapped = self._compatibility_mapped_source(source, "system-main")
859
+ result = self._execute_main(
860
+ source,
861
+ mapped,
862
+ visible_source_context=self._visible_context(mapped, source),
863
+ )
864
+ if not isinstance(result, MainCompletion):
865
+ raise ProtocolError("System MAIN stopped outside the service boundary")
866
+ return result
867
+
868
+ def _execute_main(
869
+ self,
870
+ visible_source: str,
871
+ lowered_source: str | MappedSource | None,
872
+ *,
873
+ visible_source_context: VisibleSourceContext | None = None,
874
+ messages_intercepted: int | None = None,
875
+ message_collector_key: str = "",
876
+ worker_messages: bool = False,
877
+ worker_globals: tuple[str, ...] = (),
878
+ capture_points: tuple[ModuleLocation, ...] = (),
879
+ user_breakpoints: tuple[ModuleLocation, ...] = (),
880
+ on_transport_dispatch: Callable[[], None] | None = None,
881
+ ) -> MainCompletion | CapturedStop | DebugStop:
882
+ self._require_state(
883
+ OperationState.IDLE,
884
+ OperationState.COMPLETED,
885
+ OperationState.FAILED,
886
+ )
887
+ if lowered_source is None:
888
+ message_collector_key = self.message_collector_key(LoweringMode.MAIN)
889
+ lowering = self.lowerer.lower(
890
+ visible_source,
891
+ mode=LoweringMode.MAIN,
892
+ message_collector_key=message_collector_key,
893
+ )
894
+ mapped_lowered = lowering.mapped_source
895
+ messages_intercepted = lowering.messages_intercepted
896
+ elif isinstance(lowered_source, MappedSource):
897
+ mapped_lowered = lowered_source
898
+ if messages_intercepted is None:
899
+ messages_intercepted = 0
900
+ else:
901
+ mapped_lowered = self._compatibility_mapped_source(
902
+ lowered_source,
903
+ "lowered-main",
904
+ )
905
+ if messages_intercepted is None:
906
+ messages_intercepted = 0
907
+ messages_intercepted = max(messages_intercepted, int(worker_messages))
908
+ bound_source = self._with_worker_globals_mapped(
909
+ mapped_lowered, worker_globals
910
+ )
911
+ wrapped_source = self._with_message_collector_mapped(
912
+ bound_source,
913
+ messages_intercepted,
914
+ message_collector_key,
915
+ worker_messages=worker_messages,
916
+ )
917
+ executed_source = self._as_executed_source(
918
+ wrapped_source,
919
+ mode=LoweringMode.MAIN,
920
+ )
921
+ lowered_text = executed_source.text
922
+ if visible_source_context is None:
923
+ visible_source_context = self._visible_context(
924
+ executed_source,
925
+ visible_source,
926
+ )
927
+ self.operation_id += 1
928
+ operation = OperationHandle(
929
+ self.operation_id,
930
+ visible_source,
931
+ lowered_text,
932
+ messages_intercepted,
933
+ message_collector_key,
934
+ executed_source,
935
+ visible_source_context,
936
+ )
937
+ self.active_operation = operation
938
+ self._record(
939
+ "write-journal.jsonl",
940
+ "main_started",
941
+ runtime_generation=self.runtime_generation,
942
+ operation_id=operation.operation_id,
943
+ state_before=self.state.value,
944
+ visible_sha256=sha256(visible_source.encode("utf-8")).hexdigest(),
945
+ lowered_sha256=executed_source.artifact.source_sha256,
946
+ )
947
+ self._flush_journal()
948
+ try:
949
+ self.capture_points = tuple(capture_points)
950
+ self.registry = BreakpointRegistry(
951
+ self.service_location,
952
+ self.capture_points,
953
+ tuple(user_breakpoints),
954
+ )
955
+ self.stop_sequence = 0
956
+ self.last_capture_location = None
957
+ self.recovery_checkpoint = None
958
+ self.continue_sent = False
959
+ self._generation_lost_notified = False
960
+ self.write_journal.clear()
961
+ self.stop_history.clear()
962
+ self.last_debug_stop = None
963
+ self.breakpoint_workspaces.clear()
964
+ self._set_workspace("full", self._allowed_locations)
965
+ instruction = self.session.modify(
966
+ "ТекущаяИнструкция", bsl_string_literal(lowered_text)
967
+ )
968
+ self._checked_modify(instruction, "ТекущаяИнструкция")
969
+ command = self.session.modify(
970
+ "ИдентификаторКоманды", str(operation.operation_id)
971
+ )
972
+ self._checked_modify(command, "ИдентификаторКоманды")
973
+ self.state = OperationState.MAIN_PENDING
974
+ if on_transport_dispatch is not None:
975
+ on_transport_dispatch()
976
+ self.require_debug_workspace_ready()
977
+ self.session.continue_()
978
+ stop = self.session.wait_for_any_stop(timeout_s=self.command_timeout_s)
979
+ return self._route_stop(stop)
980
+ except BaseException as error:
981
+ if self.state not in {
982
+ OperationState.CAPTURED,
983
+ OperationState.DEBUG_STOPPED,
984
+ OperationState.PARTIAL_WRITEBACK_FAILURE,
985
+ OperationState.BREAKPOINT_RESTORE_FAILURE,
986
+ OperationState.RECOVERING,
987
+ }:
988
+ self.state = OperationState.FAILED
989
+ failure_fields: dict[str, object] = {}
990
+ if isinstance(error, CompletionDecodeError):
991
+ failure_fields = {
992
+ "failure_phase": error.phase,
993
+ "evaluation_type": error.evaluation_type,
994
+ "exact_decimal_present": error.exact_decimal_present,
995
+ }
996
+ self._record(
997
+ "write-journal.jsonl",
998
+ "main_failed",
999
+ runtime_generation=self.runtime_generation,
1000
+ operation_id=operation.operation_id,
1001
+ state_after=self.state.value,
1002
+ visible_sha256=sha256(visible_source.encode("utf-8")).hexdigest(),
1003
+ lowered_sha256=executed_source.artifact.source_sha256,
1004
+ error_type=type(error).__name__,
1005
+ **failure_fields,
1006
+ )
1007
+ self._flush_journal()
1008
+ raise
1009
+
1010
+ def _route_stop(
1011
+ self,
1012
+ stop: StopEvent,
1013
+ ) -> MainCompletion | CapturedStop | DebugStop:
1014
+ classified = classify_stop(
1015
+ stop,
1016
+ self.registry,
1017
+ worker_locations=(
1018
+ self.breakpoint_workspace_owner.confirmed_snapshot.worker_slots
1019
+ ),
1020
+ )
1021
+ self.stop_history.append(classified)
1022
+ if classified.reason is StopReason.MAIN_SERVICE:
1023
+ return self._complete_main()
1024
+ if classified.reason is StopReason.CAPTURE:
1025
+ return self._begin_capture(stop)
1026
+ if self.active_operation is None:
1027
+ self.state = OperationState.FAILED
1028
+ raise ProtocolError("Debug stop has no active MAIN operation")
1029
+ debug_stop = DebugStop(self.active_operation, stop, classified.reason)
1030
+ self.last_debug_stop = debug_stop
1031
+ self.state = OperationState.DEBUG_STOPPED
1032
+ return debug_stop
1033
+
1034
+ def resume_debug_stop(
1035
+ self,
1036
+ *,
1037
+ on_transport_dispatch: Callable[[], None] | None = None,
1038
+ ) -> MainCompletion | CapturedStop | CaptureCellResult | DebugStop:
1039
+ self._require_state(
1040
+ OperationState.DEBUG_STOPPED,
1041
+ OperationState.CAPTURE_DEBUG_STOPPED,
1042
+ )
1043
+ if (
1044
+ self.last_debug_stop is None
1045
+ or self.last_debug_stop.reason is not StopReason.USER_BREAKPOINT
1046
+ ):
1047
+ raise ProtocolError("Only an explicit user breakpoint can be resumed")
1048
+ if self.state is OperationState.CAPTURE_DEBUG_STOPPED:
1049
+ capture = self.pending_capture_evaluation
1050
+ if capture is None:
1051
+ raise ProtocolError("CAPTURE debug stop has no pending evaluation")
1052
+ if on_transport_dispatch is not None:
1053
+ on_transport_dispatch()
1054
+ self.session.continue_evaluation(
1055
+ capture.pending,
1056
+ self.last_debug_stop.stop,
1057
+ )
1058
+ self.state = OperationState.EVALUATING_CAPTURE
1059
+ event = self.session.wait_evaluation_event(
1060
+ capture.pending,
1061
+ timeout_s=self.command_timeout_s,
1062
+ )
1063
+ return self._handle_capture_evaluation_event(capture, event)
1064
+ self.state = OperationState.MAIN_PENDING
1065
+ if on_transport_dispatch is not None:
1066
+ on_transport_dispatch()
1067
+ self.require_debug_workspace_ready()
1068
+ self.session.continue_()
1069
+ stop = self.session.wait_for_any_stop(timeout_s=self.command_timeout_s)
1070
+ return self._route_stop(stop)
1071
+
1072
+ def _begin_capture(self, stop: StopEvent) -> CapturedStop:
1073
+ if self.active_operation is None:
1074
+ self.state = OperationState.FAILED
1075
+ raise ProtocolError("Capture stop has no active MAIN operation")
1076
+ local_result = self.session.local_variables(stack_level=0)
1077
+ if local_result.error_occurred:
1078
+ self.state = OperationState.FAILED
1079
+ raise ProtocolError(local_result.error_text)
1080
+ transfer = self.session.evaluate(
1081
+ build_capture_transfer_call(
1082
+ variable.name for variable in local_result.variables
1083
+ )
1084
+ )
1085
+ if transfer.error_occurred:
1086
+ self.state = OperationState.FAILED
1087
+ raise BslExecutionError(transfer.error_text)
1088
+ address = evaluation_to_python(transfer)
1089
+ if not isinstance(address, str) or not address:
1090
+ self.state = OperationState.FAILED
1091
+ raise ProtocolError("Capture temporary-storage address is invalid")
1092
+ stack_level = self._locate_kernel_context_frame(stop)
1093
+ command_evidence = self.session.evaluate(
1094
+ "ИдентификаторКоманды", stack_level=stack_level
1095
+ )
1096
+ if command_evidence.error_occurred:
1097
+ self.state = OperationState.FAILED
1098
+ raise BslExecutionError(command_evidence.error_text)
1099
+ observed_command_id = self._decode_main_completion_value(
1100
+ command_evidence,
1101
+ phase="capture_command",
1102
+ )
1103
+ if observed_command_id != self.active_operation.operation_id:
1104
+ self.state = OperationState.FAILED
1105
+ raise ProtocolError(
1106
+ f"Captured command {observed_command_id!r} does not match active "
1107
+ f"operation {self.active_operation.operation_id}"
1108
+ )
1109
+ begin = self.session.evaluate(
1110
+ build_live_capture_begin_call(address),
1111
+ stack_level=stack_level,
1112
+ )
1113
+ if begin.error_occurred:
1114
+ self.state = OperationState.FAILED
1115
+ raise BslExecutionError(begin.error_text)
1116
+ self.capture_kernel_stack_level = stack_level
1117
+ # RDBG level zero is the exact suspended business frame used to build
1118
+ # the capture context. The default metadata index reuses these locals;
1119
+ # other frames and named value details are read only on explicit request.
1120
+ self.capture_frame_stack_level = 0
1121
+ self._capture_frame_variables = tuple(local_result.variables)
1122
+ self._capture_stack_frames = tuple(stop.stack_frames)
1123
+ self._capture_target_id = stop.target_id
1124
+ self._capture_manager_paths.clear()
1125
+ self._capture_value_paths.clear()
1126
+ self._capture_metadata_handles.clear()
1127
+ self.stop_sequence += 1
1128
+ self.last_capture_location = stop.location
1129
+ self.state = OperationState.CAPTURED
1130
+ captured = CapturedStop(
1131
+ self.active_operation,
1132
+ stop.location,
1133
+ self.stop_sequence,
1134
+ local_result.variables,
1135
+ observed_command_id,
1136
+ )
1137
+ self._inject(
1138
+ FaultPoint.AFTER_CAPTURE_CHECKPOINT,
1139
+ RecoveryPhase.CAPTURED,
1140
+ )
1141
+ return captured
1142
+
1143
+ def _locate_kernel_context_frame(self, stop: StopEvent) -> int:
1144
+ required = {
1145
+ "контекст",
1146
+ "текущаяинструкция",
1147
+ "идентификаторкоманды",
1148
+ }
1149
+ stack = stop.stack
1150
+ frames = stop.stack_frames
1151
+ if not stack or not frames:
1152
+ self.state = OperationState.FAILED
1153
+ raise ProtocolError("Capture stop has no exact stack mapping for runtime kernel")
1154
+ if (
1155
+ len(frames) != len(stack)
1156
+ or tuple(frame.location for frame in frames) != stack
1157
+ or any(frame.level < 0 for frame in frames)
1158
+ or len({frame.level for frame in frames}) != len(frames)
1159
+ ):
1160
+ self.state = OperationState.FAILED
1161
+ raise ProtocolError("Capture stop stack mapping is incoherent")
1162
+ for frame in frames:
1163
+ stack_level = frame.level
1164
+ if stack_level <= 0 or stack_level >= 8:
1165
+ continue
1166
+ frame_location = frame.location
1167
+ if not self._same_kernel_module(frame_location):
1168
+ continue
1169
+ local_result = self.session.local_variables(stack_level=stack_level)
1170
+ if local_result.error_occurred:
1171
+ continue
1172
+ names = {variable.name.casefold() for variable in local_result.variables}
1173
+ if not required.issubset(names):
1174
+ continue
1175
+ return stack_level
1176
+ self.state = OperationState.FAILED
1177
+ raise ProtocolError("Runtime kernel context frame was not found")
1178
+
1179
+ def _same_kernel_module(self, location: ModuleLocation) -> bool:
1180
+ kernel = self.kernel_location
1181
+ return (
1182
+ location.module_type == kernel.module_type
1183
+ and location.url == kernel.url
1184
+ and location.object_id == kernel.object_id
1185
+ and location.property_id == kernel.property_id
1186
+ and location.extension_name == kernel.extension_name
1187
+ and location.ext_id == kernel.ext_id
1188
+ )
1189
+
1190
+ def _required_capture_kernel_stack_level(self) -> int:
1191
+ if self.capture_kernel_stack_level is None:
1192
+ raise ProtocolError("Capture kernel context frame is not initialized")
1193
+ return self.capture_kernel_stack_level
1194
+
1195
+ def execute_capture(
1196
+ self,
1197
+ source: str,
1198
+ *,
1199
+ on_transport_dispatch: Callable[[], None] | None = None,
1200
+ ) -> CaptureCellResult | DebugStop:
1201
+ self._require_state(OperationState.CAPTURED)
1202
+ message_collector_key = self.message_collector_key(LoweringMode.CAPTURE)
1203
+ lowering = self.lowerer.lower(
1204
+ source,
1205
+ mode=LoweringMode.CAPTURE,
1206
+ message_collector_key=message_collector_key,
1207
+ )
1208
+ return self._execute_capture(
1209
+ source,
1210
+ lowering.mapped_source,
1211
+ visible_source_context=self._visible_context(
1212
+ lowering.mapped_source,
1213
+ source,
1214
+ ),
1215
+ messages_intercepted=lowering.messages_intercepted,
1216
+ message_collector_key=message_collector_key,
1217
+ dirty_roots=lowering.dirty_roots,
1218
+ on_transport_dispatch=on_transport_dispatch,
1219
+ )
1220
+
1221
+ def execute_lowered_capture(
1222
+ self,
1223
+ visible_source: str,
1224
+ lowered_source: str,
1225
+ *,
1226
+ messages_intercepted: int = 0,
1227
+ message_collector_key: str = "",
1228
+ dirty_roots: tuple[str, ...] = (),
1229
+ on_transport_dispatch: Callable[[], None] | None = None,
1230
+ ) -> CaptureCellResult | DebugStop:
1231
+ return self._execute_capture(
1232
+ visible_source,
1233
+ self._compatibility_mapped_source(
1234
+ lowered_source,
1235
+ "lowered-capture",
1236
+ ),
1237
+ messages_intercepted=messages_intercepted,
1238
+ message_collector_key=message_collector_key,
1239
+ dirty_roots=dirty_roots,
1240
+ on_transport_dispatch=on_transport_dispatch,
1241
+ )
1242
+
1243
+ def execute_mapped_capture(
1244
+ self,
1245
+ visible_source: str,
1246
+ lowered_source: MappedSource,
1247
+ *,
1248
+ visible_source_context: VisibleSourceContext,
1249
+ messages_intercepted: int = 0,
1250
+ message_collector_key: str = "",
1251
+ worker_messages: bool = False,
1252
+ worker_globals: tuple[str, ...] = (),
1253
+ dirty_roots: tuple[str, ...] = (),
1254
+ on_transport_dispatch: Callable[[], None] | None = None,
1255
+ ) -> CaptureCellResult | DebugStop:
1256
+ return self._execute_capture(
1257
+ visible_source,
1258
+ lowered_source,
1259
+ visible_source_context=visible_source_context,
1260
+ messages_intercepted=messages_intercepted,
1261
+ message_collector_key=message_collector_key,
1262
+ worker_messages=worker_messages,
1263
+ worker_globals=worker_globals,
1264
+ dirty_roots=dirty_roots,
1265
+ on_transport_dispatch=on_transport_dispatch,
1266
+ )
1267
+
1268
+ def capture_frame_variables(
1269
+ self, *, filters: Mapping[str, object], cursor: int, limit: int,
1270
+ timeout_s: float | None = None,
1271
+ ) -> Mapping[str, object]:
1272
+ """Return a page of captured-frame metadata without reading values."""
1273
+ deadline = self._capture_command_deadline(timeout_s)
1274
+ self._require_capture_frame()
1275
+ if not isinstance(filters, Mapping) or set(filters) - {"name", "role", "type"}:
1276
+ raise ProtocolError("capture frame filters are invalid")
1277
+ if type(cursor) is not int or cursor < 0 or type(limit) is not int or not 1 <= limit <= 100:
1278
+ raise ProtocolError("capture frame page is invalid")
1279
+ expected = {
1280
+ key: value.casefold()
1281
+ for key, value in filters.items()
1282
+ if isinstance(value, str) and value
1283
+ }
1284
+ if len(expected) != len(filters):
1285
+ raise ProtocolError("capture frame filters must be non-empty strings")
1286
+ items = tuple(
1287
+ variable
1288
+ for variable in self._capture_frame_variables
1289
+ if (
1290
+ ("name" not in expected or expected["name"] in variable.name.casefold())
1291
+ and ("type" not in expected or expected["type"] in variable.type_name.casefold())
1292
+ and ("role" not in expected or expected["role"] == "local")
1293
+ )
1294
+ )
1295
+ if cursor > len(items):
1296
+ raise ProtocolError("capture frame cursor exceeds metadata")
1297
+ self._capture_remaining_timeout(deadline)
1298
+ page = items[cursor : cursor + limit]
1299
+ next_cursor = cursor + len(page)
1300
+ return {
1301
+ "items": tuple(
1302
+ {
1303
+ "name": variable.name,
1304
+ "type_name": variable.type_name,
1305
+ "role": "local",
1306
+ # Registry-private path into the captured Context structure.
1307
+ "handle": "Контекст.КонтекстОтладки." + variable.name,
1308
+ }
1309
+ for variable in page
1310
+ ),
1311
+ "total": len(items),
1312
+ "next_cursor": next_cursor if next_cursor < len(items) else None,
1313
+ }
1314
+
1315
+ def _capture_stack_frame_wire(self, frame: StackFrame) -> Mapping[str, object]:
1316
+ location = frame.location
1317
+ if self._same_kernel_module(location):
1318
+ # The caller needs the stack level, not the extension's private
1319
+ # RDBG URL, source line, or module identity.
1320
+ return {
1321
+ "level": frame.level,
1322
+ "runtime_kernel": True,
1323
+ "module_type": None,
1324
+ "object_id": None,
1325
+ "property_id": None,
1326
+ "line": None,
1327
+ "extension_name": None,
1328
+ }
1329
+ return {
1330
+ "level": frame.level,
1331
+ "runtime_kernel": False,
1332
+ "module_type": location.module_type,
1333
+ "object_id": str(location.object_id) if location.object_id is not None else None,
1334
+ "property_id": str(location.property_id) if location.property_id is not None else None,
1335
+ "line": location.line,
1336
+ "extension_name": location.extension_name,
1337
+ }
1338
+
1339
+ def _require_capture_stack(self) -> tuple[StackFrame, ...]:
1340
+ self._require_capture_frame()
1341
+ if not self._capture_stack_frames or self._capture_target_id is None:
1342
+ raise ProtocolError("capture stack is unavailable")
1343
+ target = self.session.target
1344
+ if target is None or target.target_id != self._capture_target_id:
1345
+ raise ProtocolError("capture target has changed")
1346
+ return self._capture_stack_frames
1347
+
1348
+ def capture_stack(
1349
+ self, *, cursor: int, limit: int, timeout_s: float | None = None,
1350
+ ) -> Mapping[str, object]:
1351
+ """Page already-observed stack locations only when requested."""
1352
+ deadline = self._capture_command_deadline(timeout_s)
1353
+ if type(cursor) is not int or cursor < 0 or type(limit) is not int or not 1 <= limit <= 100:
1354
+ raise ProtocolError("capture stack page is invalid")
1355
+ frames = self._require_capture_stack()
1356
+ if cursor > len(frames):
1357
+ raise ProtocolError("capture stack cursor exceeds frames")
1358
+ self._capture_remaining_timeout(deadline)
1359
+ page = frames[cursor : cursor + limit]
1360
+ next_cursor = cursor + len(page)
1361
+ return {
1362
+ "frames": tuple(self._capture_stack_frame_wire(frame) for frame in page),
1363
+ "total": len(frames),
1364
+ "next_cursor": next_cursor if next_cursor < len(frames) else None,
1365
+ }
1366
+
1367
+ def capture_frame(
1368
+ self, *, level: int, cursor: int, limit: int,
1369
+ name: str | None = None, timeout_s: float | None = None,
1370
+ ) -> Mapping[str, object]:
1371
+ """Read one frame on demand; evaluate only an explicitly named local."""
1372
+ deadline = self._capture_command_deadline(timeout_s)
1373
+ if type(level) is not int or level < 0:
1374
+ raise ProtocolError("capture frame level is invalid")
1375
+ if type(cursor) is not int or cursor < 0 or type(limit) is not int or not 1 <= limit <= 100:
1376
+ raise ProtocolError("capture frame page is invalid")
1377
+ if name is not None:
1378
+ self._capture_identifier(name, name="variable name")
1379
+ if cursor != 0:
1380
+ raise ProtocolError("named capture variable cursor must be zero")
1381
+ frames = self._require_capture_stack()
1382
+ frame = next((item for item in frames if item.level == level), None)
1383
+ if frame is None:
1384
+ raise ProtocolError("capture frame level is unavailable")
1385
+ if self._same_kernel_module(frame.location):
1386
+ raise ProtocolError("runtime kernel frame variables are unavailable")
1387
+ if level == self.capture_frame_stack_level and name is None:
1388
+ variables = self._capture_frame_variables
1389
+ else:
1390
+ result = self.session.local_variables(
1391
+ stack_level=level,
1392
+ timeout_s=self._capture_remaining_timeout(deadline),
1393
+ max_text_size=512,
1394
+ )
1395
+ if result.error_occurred:
1396
+ raise ProtocolError("capture frame variables are unavailable")
1397
+ variables = result.variables
1398
+ if name is not None:
1399
+ variables = tuple(item for item in variables if item.name.casefold() == name.casefold())
1400
+ if not variables:
1401
+ raise ProtocolError("capture variable is unavailable")
1402
+ if cursor > len(variables):
1403
+ raise ProtocolError("capture frame cursor exceeds variables")
1404
+ self._capture_remaining_timeout(deadline)
1405
+ page = variables[cursor : cursor + limit]
1406
+ if name is None:
1407
+ items: tuple[Mapping[str, object], ...] = tuple(
1408
+ {"name": item.name, "type_name": item.type_name} for item in page
1409
+ )
1410
+ else:
1411
+ item = page[0]
1412
+ items = ({
1413
+ "name": item.name,
1414
+ "type_name": item.type_name,
1415
+ "presentation": item.presentation[:512],
1416
+ "collection_size": item.collection_size,
1417
+ },)
1418
+ next_cursor = cursor + len(page)
1419
+ return {
1420
+ "frame": self._capture_stack_frame_wire(frame),
1421
+ "variables": items,
1422
+ "total": len(variables),
1423
+ "next_cursor": next_cursor if next_cursor < len(variables) else None,
1424
+ }
1425
+
1426
+ def resolve_capture_manager_origin(
1427
+ self, root: str, fields: tuple[str, ...], *,
1428
+ timeout_s: float | None = None,
1429
+ ) -> Mapping[str, object]:
1430
+ deadline = self._capture_command_deadline(timeout_s)
1431
+ frame_stack_level = self._require_capture_frame()
1432
+ root = self._capture_identifier(root, name="manager root")
1433
+ if isinstance(fields, str) or not isinstance(fields, tuple):
1434
+ raise ProtocolError("capture manager fields are invalid")
1435
+ checked_fields = tuple(
1436
+ self._capture_identifier(field, name="manager field") for field in fields
1437
+ )
1438
+ frame_roots = {
1439
+ item.name.casefold(): item.name
1440
+ for item in self._capture_frame_variables
1441
+ }
1442
+ canonical_root = frame_roots.get(root.casefold())
1443
+ if canonical_root is None:
1444
+ raise ProtocolError("capture manager root is unavailable")
1445
+ physical_path = ".".join((canonical_root, *checked_fields))
1446
+ native_path = "Контекст.КонтекстОтладки." + physical_path
1447
+ existing = next(
1448
+ (
1449
+ key
1450
+ for key, value in self._capture_manager_paths.items()
1451
+ if value.casefold() == native_path.casefold()
1452
+ ),
1453
+ None,
1454
+ )
1455
+ if existing is not None:
1456
+ self._capture_remaining_timeout(deadline)
1457
+ return {
1458
+ "key": existing,
1459
+ "handle": existing,
1460
+ "type_name": "МенеджерВременныхТаблиц",
1461
+ }
1462
+ expression = (
1463
+ f"ТипЗнч({physical_path}) = "
1464
+ 'Тип("МенеджерВременныхТаблиц")'
1465
+ )
1466
+ try:
1467
+ proof = self.session.evaluate(
1468
+ expression,
1469
+ stack_level=frame_stack_level,
1470
+ timeout_s=self._capture_remaining_timeout(deadline),
1471
+ )
1472
+ except BaseException:
1473
+ raise ProtocolError("capture manager origin is unavailable") from None
1474
+ self._capture_remaining_timeout(deadline)
1475
+ if (
1476
+ proof.error_occurred
1477
+ or proof.type_name != "Булево"
1478
+ or evaluation_to_python(proof) is not True
1479
+ ):
1480
+ raise ProtocolError("capture manager origin is unavailable")
1481
+ handle = existing or "capture_manager_" + uuid4().hex
1482
+ self._capture_manager_paths[handle] = native_path
1483
+ return {"key": handle, "handle": handle, "type_name": "МенеджерВременныхТаблиц"}
1484
+
1485
+ def capture_temporary_tables(
1486
+ self,
1487
+ manager_handle: str,
1488
+ *,
1489
+ names: tuple[str, ...] | None,
1490
+ cursor: int,
1491
+ limit: int,
1492
+ selection: Mapping[str, object] | None,
1493
+ timeout_s: float | None = None,
1494
+ ) -> Mapping[str, object]:
1495
+ deadline = self._capture_command_deadline(timeout_s)
1496
+ self._require_capture_frame()
1497
+ if not isinstance(manager_handle, str) or manager_handle not in self._capture_manager_paths:
1498
+ raise ProtocolError("capture manager handle is stale or invalid")
1499
+ if names is None or isinstance(names, str) or len(names) != 1:
1500
+ raise ProtocolError("capture table inspection requires one named table")
1501
+ if type(cursor) is not int or cursor != 0 or type(limit) is not int or not 1 <= limit <= 100:
1502
+ raise ProtocolError("capture table page is invalid")
1503
+ name = self._capture_identifier(names[0], name="table name")
1504
+ offset, row_limit, columns = self._capture_table_selection(selection)
1505
+ manager_path = self._capture_manager_paths[manager_handle]
1506
+ if selection is None:
1507
+ schema = self._capture_temporary_table_schema(
1508
+ manager_path, name, deadline=deadline
1509
+ )
1510
+ handle = "capture_table_metadata_" + uuid4().hex
1511
+ self._capture_metadata_handles.add(handle)
1512
+ return {
1513
+ "items": ({"name": name, "schema": schema, "handle": handle},),
1514
+ "total": 1,
1515
+ "next_cursor": None,
1516
+ }
1517
+ context_key = "__onec_capture_table_" + uuid4().hex
1518
+ columns_expression = (
1519
+ "Новый Массив"
1520
+ if not columns
1521
+ else "СтрРазделить(" + bsl_string_literal(",".join(columns)) + ", \",\")"
1522
+ )
1523
+ expression = (
1524
+ "RuntimeKernelServer.СохранитьВременнуюТаблицуОтладки(Контекст, "
1525
+ + manager_path
1526
+ + ", " + bsl_string_literal(name)
1527
+ + ", " + bsl_string_literal(context_key)
1528
+ + f", {offset}, {row_limit}, " + columns_expression + ")"
1529
+ )
1530
+ result = self.session.evaluate(
1531
+ expression,
1532
+ stack_level=self._required_capture_kernel_stack_level(),
1533
+ timeout_s=self._capture_remaining_timeout(deadline),
1534
+ )
1535
+ self._capture_remaining_timeout(deadline)
1536
+ if result.error_occurred:
1537
+ raise BslExecutionError(result.error_text)
1538
+ native_path = "Контекст." + context_key
1539
+ schema_result = self.inspect_declared_table_schema(
1540
+ native_path,
1541
+ timeout_s=self._capture_remaining_timeout(deadline),
1542
+ )
1543
+ self._capture_remaining_timeout(deadline)
1544
+ schema = self._capture_schema_names(schema_result.collection_rows)
1545
+ handle = "capture_table_" + uuid4().hex
1546
+ self._capture_value_paths[handle] = native_path
1547
+ return {
1548
+ "items": ({"name": name, "schema": schema, "handle": handle},),
1549
+ "total": 1,
1550
+ "next_cursor": None,
1551
+ }
1552
+
1553
+ def _capture_temporary_table_schema(
1554
+ self, manager_path: str, name: str, *, deadline: float
1555
+ ) -> tuple[str, ...]:
1556
+ expression = (
1557
+ "RuntimeKernelServer.ПолучитьСхемуВременнойТаблицыОтладки("
1558
+ + manager_path
1559
+ + ", "
1560
+ + bsl_string_literal(name)
1561
+ + ")"
1562
+ )
1563
+ rows: list[object] = []
1564
+ start_index = 0
1565
+ while len(rows) <= MAX_CAPTURE_SCHEMA_COLUMNS:
1566
+ page_size = min(64, MAX_CAPTURE_SCHEMA_COLUMNS + 1 - len(rows))
1567
+ result = self.session.evaluate_collection(
1568
+ expression,
1569
+ start_index=start_index,
1570
+ page_size=page_size,
1571
+ timeout_s=self._capture_remaining_timeout(deadline),
1572
+ max_text_size=4096,
1573
+ stack_level=self._required_capture_kernel_stack_level(),
1574
+ )
1575
+ self._capture_remaining_timeout(deadline)
1576
+ if result.error_occurred:
1577
+ raise BslExecutionError(result.error_text)
1578
+ page = result.collection_rows
1579
+ rows.extend(page)
1580
+ if len(rows) > MAX_CAPTURE_SCHEMA_COLUMNS:
1581
+ raise ProtocolError("capture table schema exceeds bounded metadata")
1582
+ if result.collection_size is not None:
1583
+ if result.collection_size > MAX_CAPTURE_SCHEMA_COLUMNS:
1584
+ raise ProtocolError("capture table schema exceeds bounded metadata")
1585
+ if len(rows) >= result.collection_size:
1586
+ break
1587
+ if len(page) < page_size:
1588
+ break
1589
+ start_index += len(page)
1590
+ return self._capture_schema_names(rows)
1591
+
1592
+ @classmethod
1593
+ def _capture_table_selection(
1594
+ cls, selection: Mapping[str, object] | None
1595
+ ) -> tuple[int, int, tuple[str, ...]]:
1596
+ if selection is None:
1597
+ return 0, 0, ()
1598
+ if not isinstance(selection, Mapping) or set(selection) != {"offset", "limit", "columns"}:
1599
+ raise ProtocolError("capture table selection is invalid")
1600
+ offset, limit, columns = selection["offset"], selection["limit"], selection["columns"]
1601
+ if (
1602
+ type(offset) is not int
1603
+ or offset < 0
1604
+ or offset > MAX_CAPTURE_PROJECTION_POSITION
1605
+ or type(limit) is not int
1606
+ or not 1 <= limit <= 100
1607
+ or offset + limit > MAX_CAPTURE_PROJECTION_POSITION
1608
+ ):
1609
+ raise ProtocolError("capture table selection bounds are invalid")
1610
+ if isinstance(columns, str) or not isinstance(columns, Sequence) or len(columns) > 100:
1611
+ raise ProtocolError("capture table selection columns are invalid")
1612
+ return offset, limit, tuple(cls._capture_identifier(column, name="table column") for column in columns)
1613
+
1614
+ @staticmethod
1615
+ def _capture_schema_names(rows: Sequence[object]) -> tuple[str, ...]:
1616
+ names: list[str] = []
1617
+ for row in rows:
1618
+ cells = getattr(row, "cells", ())
1619
+ for cell in cells:
1620
+ if getattr(cell, "name", "") == "Имя":
1621
+ name = getattr(cell, "value_string", "") or getattr(cell, "presentation", "")
1622
+ if isinstance(name, str) and name:
1623
+ names.append(name.strip('"'))
1624
+ return tuple(names)
1625
+
1626
+ def capture_value_handle(self, handle: str) -> str:
1627
+ self._require_capture_frame()
1628
+ if handle in self._capture_manager_paths:
1629
+ return self._capture_manager_paths[handle]
1630
+ if handle in self._capture_metadata_handles:
1631
+ raise ProtocolError(
1632
+ "capture table inventory is metadata-only; request a bounded row selection"
1633
+ )
1634
+ try:
1635
+ return self._capture_value_paths[handle]
1636
+ except KeyError as error:
1637
+ raise ProtocolError("capture value handle is stale or invalid") from error
1638
+
1639
+ def is_capture_metadata_handle(self, handle: str) -> bool:
1640
+ """Prove an opaque inventory handle belongs to the active frame."""
1641
+ self._require_capture_frame()
1642
+ return isinstance(handle, str) and handle in self._capture_metadata_handles
1643
+
1644
+ @classmethod
1645
+ def _with_message_collector(
1646
+ cls,
1647
+ source: str,
1648
+ messages_intercepted: int,
1649
+ message_collector_key: str,
1650
+ ) -> str:
1651
+ mapped = cls._compatibility_mapped_source(source, "collector-compatibility")
1652
+ return cls._with_message_collector_mapped(
1653
+ mapped,
1654
+ messages_intercepted,
1655
+ message_collector_key,
1656
+ ).text
1657
+
1658
+ @staticmethod
1659
+ def _with_worker_globals_mapped(
1660
+ source: MappedSource,
1661
+ names: tuple[str, ...],
1662
+ ) -> MappedSource:
1663
+ """Expose only referenced notebook values for one Worker invocation."""
1664
+ if not names:
1665
+ return source
1666
+ worker = "__OnecNotebookGlobalWorker"
1667
+ previous = "__OnecNotebookPreviousGlobals"
1668
+ globals_name = "__OnecNotebookBoundGlobals"
1669
+ initialization = (
1670
+ f'{worker} = Контекст.RuntimeWorkerActiveGeneration.Modules.Получить("Worker");\n'
1671
+ f"{previous} = {worker}.__OnecNotebookGlobals;\n"
1672
+ f"{globals_name} = Новый Структура;\n"
1673
+ )
1674
+ for name in names:
1675
+ initialization += (
1676
+ f"{globals_name}.Вставить({bsl_string_literal(name)}, "
1677
+ f"Контекст.{name});\n"
1678
+ )
1679
+ initialization += f"{worker}.__OnecNotebookGlobals = {globals_name};\n"
1680
+ restore = f"{worker}.__OnecNotebookGlobals = {previous};\n"
1681
+ start = SourceSpan(0, 0)
1682
+ end = SourceSpan(len(source.text), len(source.text))
1683
+ builder = SourceTransformBuilder(source)
1684
+ builder.synthetic(initialization, start, "worker_globals_initialize")
1685
+ builder.synthetic("Попытка\n", start, "worker_globals_try")
1686
+ builder.copy(SourceSpan(0, len(source.text)))
1687
+ builder.synthetic("\n" + restore, end, "worker_globals_finalize")
1688
+ builder.synthetic("Исключение\n", end, "worker_globals_exception")
1689
+ builder.synthetic(restore, end, "worker_globals_finalize")
1690
+ builder.synthetic("ВызватьИсключение;\n", end, "worker_globals_rethrow")
1691
+ builder.synthetic("КонецПопытки;", end, "worker_globals_end")
1692
+ return builder.build(
1693
+ SourceArtifactKind.COLLECTOR_WRAPPER,
1694
+ wrapper_semantic_version="notebook-globals-v1",
1695
+ mode=source.artifact.mode,
1696
+ )
1697
+
1698
+ @staticmethod
1699
+ def _with_message_collector_mapped(
1700
+ source: MappedSource,
1701
+ messages_intercepted: int,
1702
+ message_collector_key: str,
1703
+ *,
1704
+ worker_messages: bool = False,
1705
+ ) -> MappedSource:
1706
+ if not isinstance(source, MappedSource):
1707
+ raise ValueError("message collector source must be mapped")
1708
+ if not messages_intercepted:
1709
+ return source
1710
+ worker_object = "__OnecPinnedWorkerGenerationMessageObject"
1711
+ previous_sink = "__OnecPinnedWorkerGenerationPreviousMessageSink"
1712
+ finalize = (
1713
+ (
1714
+ f"{worker_object}.__OnecWorkerMessageSink = "
1715
+ f"{previous_sink};\n"
1716
+ if worker_messages else ""
1717
+ )
1718
+ + 'Контекст.Вставить("__onec_cell_messages_result_key", '
1719
+ + bsl_string_literal(message_collector_key)
1720
+ + ");\n"
1721
+ + 'Контекст.Вставить("__onec_cell_messages_result", Контекст.'
1722
+ + message_collector_key
1723
+ + ");\n"
1724
+ + "Контекст.Удалить("
1725
+ + bsl_string_literal(message_collector_key)
1726
+ + ");"
1727
+ )
1728
+ start = SourceSpan(0, 0)
1729
+ end = SourceSpan(len(source.text), len(source.text))
1730
+ builder = SourceTransformBuilder(source)
1731
+ initialize = f'Контекст.Вставить("{message_collector_key}", Новый Массив);\n'
1732
+ if worker_messages:
1733
+ initialize += (
1734
+ f"{worker_object} = "
1735
+ 'Контекст.RuntimeWorkerActiveGeneration.Modules.Получить("Worker");\n'
1736
+ f"{previous_sink} = "
1737
+ f"{worker_object}.__OnecWorkerMessageSink;\n"
1738
+ f"{worker_object}.__OnecWorkerMessageSink = "
1739
+ f"Контекст.{message_collector_key};\n"
1740
+ )
1741
+ builder.synthetic(
1742
+ initialize,
1743
+ start,
1744
+ "message_collector_initialize",
1745
+ )
1746
+ builder.synthetic("Попытка\n", start, "message_collector_try")
1747
+ builder.copy(SourceSpan(0, len(source.text)))
1748
+ builder.synthetic(
1749
+ "\n" + finalize + "\n",
1750
+ end,
1751
+ "message_collector_finalize",
1752
+ )
1753
+ builder.synthetic(
1754
+ "Исключение\n",
1755
+ end,
1756
+ "message_collector_exception",
1757
+ )
1758
+ builder.synthetic(
1759
+ finalize + "\n",
1760
+ end,
1761
+ "message_collector_finalize",
1762
+ )
1763
+ builder.synthetic(
1764
+ "ВызватьИсключение;\n",
1765
+ end,
1766
+ "message_collector_rethrow",
1767
+ )
1768
+ builder.synthetic("КонецПопытки;", end, "message_collector_end")
1769
+ return builder.build(
1770
+ SourceArtifactKind.COLLECTOR_WRAPPER,
1771
+ wrapper_semantic_version="message-collector-v1",
1772
+ mode=source.artifact.mode,
1773
+ )
1774
+
1775
+ @staticmethod
1776
+ def _as_executed_source(
1777
+ source: MappedSource,
1778
+ *,
1779
+ mode: LoweringMode,
1780
+ ) -> MappedSource:
1781
+ builder = SourceTransformBuilder(source)
1782
+ builder.copy(SourceSpan(0, len(source.text)))
1783
+ return builder.build(
1784
+ SourceArtifactKind.EXECUTED_BSL,
1785
+ wrapper_semantic_version="runtime-execution-v1",
1786
+ mode=mode.value,
1787
+ )
1788
+
1789
+ @staticmethod
1790
+ def _compatibility_mapped_source(source: str, purpose: str) -> MappedSource:
1791
+ digest = source_sha256(source)
1792
+ return mapped_visible_source(
1793
+ source,
1794
+ SourceUnitRef(
1795
+ SourceUnitKind.NOTEBOOK_CELL,
1796
+ f"runtime-{purpose}:{digest}",
1797
+ 0,
1798
+ digest,
1799
+ ),
1800
+ )
1801
+
1802
+ @staticmethod
1803
+ def _visible_context(
1804
+ mapped: MappedSource,
1805
+ visible_source: str,
1806
+ ) -> VisibleSourceContext | None:
1807
+ digest = source_sha256(visible_source)
1808
+ units = {
1809
+ reference
1810
+ for segment in mapped.source_map.segments
1811
+ for reference in (segment.origin_ref, segment.anchor_ref)
1812
+ if isinstance(reference, SourceUnitRef)
1813
+ and reference.source_sha256 == digest
1814
+ }
1815
+ if not units:
1816
+ return None
1817
+ return VisibleSourceContext({unit: visible_source for unit in units})
1818
+
1819
+ def message_collector_key(self, mode: LoweringMode) -> str:
1820
+ operation_id = (
1821
+ self.operation_id + 1
1822
+ if mode is LoweringMode.MAIN
1823
+ else self.active_operation.operation_id if self.active_operation else 0
1824
+ )
1825
+ cell_sequence = 0 if mode is LoweringMode.MAIN else self.cell_sequence + 1
1826
+ return (
1827
+ f"__onec_cell_messages_{self.runtime_generation}_{operation_id}_{cell_sequence}"
1828
+ )
1829
+
1830
+ def execute_system_capture(self, source: str) -> CaptureCellResult | DebugStop:
1831
+ """Execute trusted runtime BSL verbatim in the current capture frame."""
1832
+ mapped = self._compatibility_mapped_source(source, "system-capture")
1833
+ return self._execute_capture(
1834
+ source,
1835
+ mapped,
1836
+ visible_source_context=self._visible_context(mapped, source),
1837
+ )
1838
+
1839
+ def install_capture_worker_generation_pin(
1840
+ self,
1841
+ manifest_sha256: str,
1842
+ ) -> None:
1843
+ """Bind the host-fenced Worker root into the ephemeral CAPTURE slot."""
1844
+ self._require_state(OperationState.CAPTURED)
1845
+ if (
1846
+ not isinstance(manifest_sha256, str)
1847
+ or re.fullmatch(r"[0-9a-f]{64}", manifest_sha256) is None
1848
+ ):
1849
+ raise ProtocolError("CAPTURE Worker manifest identity is invalid")
1850
+ result = self.session.evaluate(
1851
+ "RuntimeKernelServer.УстановитьПинПоколенияWorker(Контекст, "
1852
+ f"{bsl_string_literal(manifest_sha256)})",
1853
+ stack_level=self._required_capture_kernel_stack_level(),
1854
+ )
1855
+ if result.error_occurred or evaluation_to_python(result) is not True:
1856
+ raise ProtocolError("CAPTURE Worker pin installation failed")
1857
+
1858
+ def clear_capture_worker_generation_pin(self) -> None:
1859
+ """Remove only the reserved ephemeral CAPTURE Worker slot."""
1860
+ self._require_state(OperationState.CAPTURED)
1861
+ result = self.session.evaluate(
1862
+ "RuntimeKernelServer.ОчиститьПинПоколенияWorker(Контекст)",
1863
+ stack_level=self._required_capture_kernel_stack_level(),
1864
+ )
1865
+ if result.error_occurred or evaluation_to_python(result) is not True:
1866
+ raise ProtocolError("CAPTURE Worker pin cleanup failed")
1867
+
1868
+ def _execute_capture(
1869
+ self,
1870
+ visible_source: str,
1871
+ lowered_source: str | MappedSource,
1872
+ *,
1873
+ visible_source_context: VisibleSourceContext | None = None,
1874
+ messages_intercepted: int = 0,
1875
+ message_collector_key: str = "",
1876
+ worker_messages: bool = False,
1877
+ worker_globals: tuple[str, ...] = (),
1878
+ dirty_roots: tuple[str, ...] = (),
1879
+ on_transport_dispatch: Callable[[], None] | None = None,
1880
+ ) -> CaptureCellResult | DebugStop:
1881
+ self._require_state(OperationState.CAPTURED)
1882
+ if self.active_operation is None:
1883
+ raise ProtocolError("Capture cell has no active MAIN operation")
1884
+ mapped_lowered = (
1885
+ lowered_source
1886
+ if isinstance(lowered_source, MappedSource)
1887
+ else self._compatibility_mapped_source(lowered_source, "capture-cell")
1888
+ )
1889
+ messages_intercepted = max(messages_intercepted, int(worker_messages))
1890
+ bound_source = self._with_worker_globals_mapped(
1891
+ mapped_lowered, worker_globals
1892
+ )
1893
+ wrapped_source = self._with_message_collector_mapped(
1894
+ bound_source,
1895
+ messages_intercepted,
1896
+ message_collector_key,
1897
+ worker_messages=worker_messages,
1898
+ )
1899
+ executed_source = self._as_executed_source(
1900
+ wrapped_source,
1901
+ mode=LoweringMode.CAPTURE,
1902
+ )
1903
+ lowered_text = executed_source.text
1904
+ if visible_source_context is None:
1905
+ visible_source_context = self._visible_context(
1906
+ executed_source,
1907
+ visible_source,
1908
+ )
1909
+ self.cell_sequence += 1
1910
+ cell_fields = {
1911
+ "runtime_generation": self.runtime_generation,
1912
+ "operation_id": self.active_operation.operation_id,
1913
+ "capture_stop_sequence": self.stop_sequence,
1914
+ "cell_sequence": self.cell_sequence,
1915
+ }
1916
+ self._record(
1917
+ "write-journal.jsonl",
1918
+ "cell_started",
1919
+ **cell_fields,
1920
+ visible_sha256=sha256(visible_source.encode("utf-8")).hexdigest(),
1921
+ lowered_sha256=executed_source.artifact.source_sha256,
1922
+ state_before=OperationState.CAPTURED.value,
1923
+ dirty_roots=list(dirty_roots),
1924
+ immediate_object_mutation_possible=True,
1925
+ )
1926
+ try:
1927
+ self.state = OperationState.EVALUATING_CAPTURE
1928
+ try:
1929
+ self._set_workspace(
1930
+ "capture-evaluation",
1931
+ self.registry.evaluation_locations,
1932
+ )
1933
+ except BaseException as error:
1934
+ if not isinstance(error, BreakpointWorkspaceOutcomeUnknown):
1935
+ self.state = OperationState.CAPTURED
1936
+ raise
1937
+ if on_transport_dispatch is not None:
1938
+ on_transport_dispatch()
1939
+ pending = self.session.start_evaluation(
1940
+ build_live_current_capture_call(lowered_text),
1941
+ stack_level=self._required_capture_kernel_stack_level(),
1942
+ )
1943
+ capture = _PendingCaptureEvaluation(
1944
+ pending,
1945
+ self.active_operation,
1946
+ visible_source,
1947
+ lowered_text,
1948
+ executed_source,
1949
+ visible_source_context,
1950
+ messages_intercepted,
1951
+ message_collector_key,
1952
+ cell_fields,
1953
+ )
1954
+ self.pending_capture_evaluation = capture
1955
+ event = self.session.wait_evaluation_event(
1956
+ pending,
1957
+ timeout_s=self.command_timeout_s,
1958
+ )
1959
+ except BaseException as error:
1960
+ self._record(
1961
+ "write-journal.jsonl",
1962
+ "cell_failed",
1963
+ **cell_fields,
1964
+ state_after=self.state.value,
1965
+ error_type=type(error).__name__,
1966
+ )
1967
+ self._flush_journal()
1968
+ raise
1969
+ return self._handle_capture_evaluation_event(capture, event)
1970
+
1971
+ def _handle_capture_evaluation_event(
1972
+ self,
1973
+ capture: _PendingCaptureEvaluation,
1974
+ event: EvaluationResult | StopEvent,
1975
+ ) -> CaptureCellResult | DebugStop:
1976
+ if self.pending_capture_evaluation is not capture:
1977
+ raise ProtocolError("Pending CAPTURE evaluation identity changed")
1978
+ if isinstance(event, StopEvent):
1979
+ classified = classify_stop(
1980
+ event,
1981
+ self.registry,
1982
+ worker_locations=(
1983
+ self.breakpoint_workspace_owner.confirmed_snapshot.worker_slots
1984
+ ),
1985
+ )
1986
+ self.stop_history.append(classified)
1987
+ debug_stop = DebugStop(capture.operation, event, classified.reason)
1988
+ self.last_debug_stop = debug_stop
1989
+ self.state = OperationState.CAPTURE_DEBUG_STOPPED
1990
+ return debug_stop
1991
+ try:
1992
+ try:
1993
+ self._set_workspace("full-restore", self.registry.full_locations)
1994
+ except BaseException as restore_error:
1995
+ self.state = OperationState.BREAKPOINT_RESTORE_FAILURE
1996
+ raise BreakpointRestoreError(
1997
+ "Failed to restore full breakpoint workspace"
1998
+ ) from restore_error
1999
+ self.state = OperationState.CAPTURED
2000
+ self.pending_capture_evaluation = None
2001
+ self.last_debug_stop = None
2002
+ if event.error_occurred:
2003
+ messages = self._take_capture_cell_messages(
2004
+ capture.message_collector_key
2005
+ if capture.messages_intercepted
2006
+ else ""
2007
+ )
2008
+ diagnostic = _safe_platform_diagnostic(
2009
+ event.error_text,
2010
+ capture.executed_source,
2011
+ visible_source_context=capture.visible_source_context,
2012
+ )
2013
+ raise BslExecutionError(
2014
+ event.error_text,
2015
+ messages=messages,
2016
+ diagnostic=diagnostic,
2017
+ )
2018
+ value = evaluation_to_python(event)
2019
+ messages = self._take_capture_cell_messages(
2020
+ capture.message_collector_key
2021
+ if capture.messages_intercepted
2022
+ else ""
2023
+ )
2024
+ cell = CaptureCellResult(
2025
+ capture.operation.operation_id,
2026
+ capture.visible_source,
2027
+ capture.lowered_source,
2028
+ value,
2029
+ messages,
2030
+ )
2031
+ except BaseException as error:
2032
+ self._record(
2033
+ "write-journal.jsonl",
2034
+ "cell_failed",
2035
+ **capture.cell_fields,
2036
+ state_after=self.state.value,
2037
+ error_type=type(error).__name__,
2038
+ )
2039
+ self._flush_journal()
2040
+ raise
2041
+ self._record(
2042
+ "write-journal.jsonl",
2043
+ "cell_completed",
2044
+ **capture.cell_fields,
2045
+ state_after=self.state.value,
2046
+ )
2047
+ self._flush_journal()
2048
+ return cell
2049
+
2050
+ def resume(
2051
+ self,
2052
+ *,
2053
+ dirty_roots: tuple[str, ...] = (),
2054
+ continuation_attempt_id: str | None = None,
2055
+ on_transport_dispatch: Callable[[], None] | None = None,
2056
+ ) -> MainCompletion | CapturedStop | DebugStop:
2057
+ self._require_state(OperationState.CAPTURED)
2058
+ dirty_roots = tuple(dirty_roots)
2059
+ attempt = self._continuation_attempt(
2060
+ dirty_roots, continuation_attempt_id
2061
+ )
2062
+ self.state = OperationState.FLUSHING
2063
+ for root in dirty_roots:
2064
+ try:
2065
+ transfer = self.session.evaluate(
2066
+ build_live_capture_root_transfer_call(root),
2067
+ stack_level=self._required_capture_kernel_stack_level(),
2068
+ )
2069
+ except BaseException as error:
2070
+ self._mark_continuation_root(
2071
+ attempt, root, "failed", error=type(error).__name__
2072
+ )
2073
+ self._mark_later_roots_unattempted(attempt, root)
2074
+ self.state = (
2075
+ OperationState.RECOVERING
2076
+ if isinstance(error, RdbgTransportError)
2077
+ else OperationState.PARTIAL_WRITEBACK_FAILURE
2078
+ )
2079
+ raise
2080
+ if transfer.error_occurred:
2081
+ self._mark_continuation_root(
2082
+ attempt, root, "failed", error=transfer.error_text
2083
+ )
2084
+ self._mark_later_roots_unattempted(attempt, root)
2085
+ self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
2086
+ raise PartialWritebackError(
2087
+ f"Capture root {root} transfer failed: {transfer.error_text}"
2088
+ )
2089
+ address = evaluation_to_python(transfer)
2090
+ if not isinstance(address, str) or not address:
2091
+ self._mark_continuation_root(
2092
+ attempt,
2093
+ root,
2094
+ "failed",
2095
+ error="invalid temporary-storage address",
2096
+ )
2097
+ self._mark_later_roots_unattempted(attempt, root)
2098
+ self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
2099
+ raise PartialWritebackError(
2100
+ f"Capture root {root} temporary-storage address is invalid"
2101
+ )
2102
+ expression = build_temporary_storage_value_expression(address)
2103
+ expression_sha256 = sha256(expression.encode("utf-8")).hexdigest()
2104
+ self._set_root_status(root, expression_sha256, SideEffectStatus.PLANNED)
2105
+ self._set_root_status(root, expression_sha256, SideEffectStatus.SENT)
2106
+ self._mark_continuation_root(attempt, root, "sent")
2107
+ try:
2108
+ result = self.session.modify(root, expression)
2109
+ except RdbgTransportError as error:
2110
+ self._set_root_status(
2111
+ root,
2112
+ expression_sha256,
2113
+ SideEffectStatus.OUTCOME_UNKNOWN,
2114
+ error=str(error),
2115
+ )
2116
+ self._mark_continuation_root(
2117
+ attempt, root, "outcome_unknown", error=str(error)
2118
+ )
2119
+ self._mark_later_roots_unattempted(attempt, root)
2120
+ raise
2121
+ self._set_root_status(
2122
+ root,
2123
+ expression_sha256,
2124
+ SideEffectStatus.ACKNOWLEDGED,
2125
+ result_id=result.result_id,
2126
+ )
2127
+ self._record(
2128
+ "write-journal.jsonl",
2129
+ "root_write_acknowledged",
2130
+ runtime_generation=self.runtime_generation,
2131
+ operation_id=self.active_operation.operation_id,
2132
+ root=root,
2133
+ result_id=str(result.result_id),
2134
+ )
2135
+ if result.error_occurred:
2136
+ self._set_root_status(
2137
+ root,
2138
+ expression_sha256,
2139
+ SideEffectStatus.FAILED,
2140
+ result_id=result.result_id,
2141
+ error=result.error_text,
2142
+ )
2143
+ self._mark_continuation_root(
2144
+ attempt, root, "failed", error=result.error_text
2145
+ )
2146
+ self._mark_later_roots_unattempted(attempt, root)
2147
+ self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
2148
+ raise PartialWritebackError(
2149
+ f"Capture root {root} failed: {result.error_text}"
2150
+ )
2151
+ self._set_root_status(
2152
+ root,
2153
+ expression_sha256,
2154
+ SideEffectStatus.SUCCEEDED,
2155
+ result_id=result.result_id,
2156
+ )
2157
+ self._mark_continuation_root(attempt, root, "succeeded")
2158
+ if sum(entry.succeeded for entry in self.write_journal) == 1:
2159
+ self._inject(
2160
+ FaultPoint.AFTER_FIRST_ROOT_WRITE,
2161
+ RecoveryPhase.FLUSHING,
2162
+ )
2163
+ self._record(
2164
+ "write-journal.jsonl",
2165
+ "capture_cleanup_planned",
2166
+ runtime_generation=self.runtime_generation,
2167
+ operation_id=self.active_operation.operation_id,
2168
+ )
2169
+ self._flush_journal()
2170
+ try:
2171
+ close = self.session.evaluate(
2172
+ build_live_capture_end_call(),
2173
+ stack_level=self._required_capture_kernel_stack_level(),
2174
+ )
2175
+ except RdbgTransportError as error:
2176
+ self._record(
2177
+ "write-journal.jsonl",
2178
+ "capture_cleanup_outcome_unknown",
2179
+ runtime_generation=self.runtime_generation,
2180
+ operation_id=self.active_operation.operation_id,
2181
+ error=str(error),
2182
+ )
2183
+ self._flush_journal()
2184
+ raise
2185
+ if close.error_occurred:
2186
+ self._record(
2187
+ "write-journal.jsonl",
2188
+ "capture_cleanup_failed",
2189
+ runtime_generation=self.runtime_generation,
2190
+ operation_id=self.active_operation.operation_id,
2191
+ error=close.error_text,
2192
+ )
2193
+ self._flush_journal()
2194
+ self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
2195
+ raise PartialWritebackError(
2196
+ f"Capture cleanup failed after write-back: {close.error_text}"
2197
+ )
2198
+ self._record(
2199
+ "write-journal.jsonl",
2200
+ "capture_cleanup_completed",
2201
+ runtime_generation=self.runtime_generation,
2202
+ operation_id=self.active_operation.operation_id,
2203
+ )
2204
+ self._flush_journal()
2205
+ self.state = OperationState.RESUMING
2206
+ attempt.continue_state = "planned"
2207
+ self._record(
2208
+ "write-journal.jsonl",
2209
+ "continue_planned",
2210
+ attempt_id=attempt.spec.attempt_id,
2211
+ runtime_generation=self.runtime_generation,
2212
+ operation_id=self.active_operation.operation_id,
2213
+ )
2214
+ self._flush_journal()
2215
+ attempt.continue_state = "sent"
2216
+ self._record(
2217
+ "write-journal.jsonl",
2218
+ "continue_sent",
2219
+ attempt_id=attempt.spec.attempt_id,
2220
+ runtime_generation=self.runtime_generation,
2221
+ operation_id=self.active_operation.operation_id,
2222
+ )
2223
+ self._flush_journal()
2224
+ try:
2225
+ if on_transport_dispatch is not None:
2226
+ on_transport_dispatch()
2227
+ self.require_debug_workspace_ready()
2228
+ self.session.continue_()
2229
+ except RdbgTransportError as error:
2230
+ # The transport gives no pre-send acknowledgement. Treat this as
2231
+ # an ambiguous continuation: the paused frame may already be gone.
2232
+ self._clear_capture_inspection()
2233
+ self.state = OperationState.RECOVERING
2234
+ attempt.continue_state = "outcome_unknown"
2235
+ self._record(
2236
+ "write-journal.jsonl",
2237
+ "continue_outcome_unknown",
2238
+ attempt_id=attempt.spec.attempt_id,
2239
+ runtime_generation=self.runtime_generation,
2240
+ operation_id=self.active_operation.operation_id,
2241
+ error=str(error),
2242
+ )
2243
+ self._flush_journal()
2244
+ raise
2245
+ # The debugger has accepted Continue; no frame-backed resolver may
2246
+ # survive into the next stop, even if the subsequent wait is unknown.
2247
+ self._clear_capture_inspection()
2248
+ attempt.continue_state = "acknowledged"
2249
+ self._record(
2250
+ "write-journal.jsonl",
2251
+ "continue_acknowledged",
2252
+ attempt_id=attempt.spec.attempt_id,
2253
+ runtime_generation=self.runtime_generation,
2254
+ operation_id=self.active_operation.operation_id,
2255
+ )
2256
+ self.continue_sent = True
2257
+ self._flush_journal()
2258
+ self._inject(
2259
+ FaultPoint.AFTER_CONTINUE_ACK,
2260
+ RecoveryPhase.RESUMING,
2261
+ )
2262
+ stop = self.session.wait_for_any_stop(timeout_s=self.command_timeout_s)
2263
+ return self._route_stop(stop)
2264
+
2265
+ def _lose_generation(
2266
+ self,
2267
+ checkpoint: RecoveryCheckpoint,
2268
+ reason: str,
2269
+ ) -> RecoveryResult:
2270
+ self.state = OperationState.LOST
2271
+ self._record(
2272
+ "recovery-transitions.jsonl",
2273
+ "lost",
2274
+ checkpoint_sequence=checkpoint.sequence,
2275
+ phase=checkpoint.phase.value,
2276
+ reason=reason,
2277
+ )
2278
+ self._flush_journal()
2279
+ self.session.invalidate()
2280
+ if not self._generation_lost_notified:
2281
+ self._generation_lost_notified = True
2282
+ if self.on_generation_lost is not None:
2283
+ self.on_generation_lost(checkpoint)
2284
+ return RecoveryResult(
2285
+ RecoveryOutcome.LOST,
2286
+ checkpoint.phase,
2287
+ checkpoint,
2288
+ reason,
2289
+ )
2290
+
2291
+ @staticmethod
2292
+ def _validate_target_identity(
2293
+ checkpoint: RecoveryCheckpoint,
2294
+ reconnected: ReconnectedSession,
2295
+ ) -> None:
2296
+ target = reconnected.session.target
2297
+ if target is None:
2298
+ raise ProtocolError("Recovered session has no selected target")
2299
+ if target.target_id != checkpoint.target.target_id:
2300
+ raise ProtocolError("Recovered target identity changed")
2301
+ if target.target_type != checkpoint.target.target_type:
2302
+ raise ProtocolError("Recovered target type changed")
2303
+
2304
+ def recover_transport(
2305
+ self,
2306
+ reconnector: Callable[[RecoveryCheckpoint, bool], ReconnectedSession],
2307
+ ) -> RecoveryResult:
2308
+ self._require_state(OperationState.RECOVERING)
2309
+ checkpoint = self.recovery_checkpoint
2310
+ if checkpoint is None:
2311
+ raise ProtocolError("RECOVERING state has no checkpoint")
2312
+ self._record(
2313
+ "recovery-transitions.jsonl",
2314
+ "recovering",
2315
+ checkpoint_sequence=checkpoint.sequence,
2316
+ phase=checkpoint.phase.value,
2317
+ )
2318
+ self._flush_journal()
2319
+ if checkpoint.phase is RecoveryPhase.FLUSHING:
2320
+ return self._lose_generation(
2321
+ checkpoint,
2322
+ "partial write-back cannot be replayed",
2323
+ )
2324
+
2325
+ reconnected: ReconnectedSession | None = None
2326
+ try:
2327
+ reconnected = reconnector(
2328
+ checkpoint,
2329
+ checkpoint.phase is RecoveryPhase.RESUMING,
2330
+ )
2331
+ self._validate_target_identity(checkpoint, reconnected)
2332
+ if checkpoint.phase is RecoveryPhase.CAPTURED:
2333
+ if reconnected.evidence is None:
2334
+ raise ProtocolError("Paused recovery has no frame evidence")
2335
+ validate_paused_identity(checkpoint, reconnected.evidence)
2336
+ old_session = self.session
2337
+ self.session = reconnected.session
2338
+ self.breakpoint_workspace_owner._adopt_confirmed_session(
2339
+ reconnected.session,
2340
+ checkpoint.breakpoint_workspace,
2341
+ )
2342
+ old_session.invalidate()
2343
+ self.state = OperationState.CAPTURED
2344
+ self._record(
2345
+ "recovery-transitions.jsonl",
2346
+ "recovered",
2347
+ checkpoint_sequence=checkpoint.sequence,
2348
+ phase=checkpoint.phase.value,
2349
+ )
2350
+ self._flush_journal()
2351
+ return RecoveryResult(
2352
+ RecoveryOutcome.RECOVERED,
2353
+ checkpoint.phase,
2354
+ checkpoint,
2355
+ "exact paused identity recovered",
2356
+ )
2357
+
2358
+ stop = (
2359
+ reconnected.evidence.stop
2360
+ if reconnected.evidence is not None
2361
+ else reconnected.session.wait_for_any_stop(
2362
+ timeout_s=self.command_timeout_s
2363
+ )
2364
+ )
2365
+ if stop.target_id != checkpoint.target.target_id:
2366
+ raise ProtocolError("Recovered stop belongs to another target")
2367
+ old_session = self.session
2368
+ self.session = reconnected.session
2369
+ self.breakpoint_workspace_owner._adopt_confirmed_session(
2370
+ reconnected.session,
2371
+ checkpoint.breakpoint_workspace,
2372
+ )
2373
+ old_session.invalidate()
2374
+ self.state = OperationState.RESUMING
2375
+ continuation = self._route_stop(stop)
2376
+ self._record(
2377
+ "recovery-transitions.jsonl",
2378
+ "recovered",
2379
+ checkpoint_sequence=checkpoint.sequence,
2380
+ phase=checkpoint.phase.value,
2381
+ )
2382
+ self._flush_journal()
2383
+ return RecoveryResult(
2384
+ RecoveryOutcome.RECOVERED,
2385
+ checkpoint.phase,
2386
+ checkpoint,
2387
+ "continued operation observed without replay",
2388
+ continuation,
2389
+ )
2390
+ except Exception as error:
2391
+ if reconnected is not None and reconnected.session is not self.session:
2392
+ reconnected.session.invalidate()
2393
+ return self._lose_generation(checkpoint, str(error))
2394
+
2395
+ def _complete_main(self) -> MainCompletion:
2396
+ if self.active_operation is None:
2397
+ self.state = OperationState.FAILED
2398
+ raise ProtocolError("MAIN completion has no active operation")
2399
+ completed_result = self.session.evaluate("ЗавершеннаяКоманда")
2400
+ if completed_result.error_occurred:
2401
+ self.state = OperationState.FAILED
2402
+ raise BslExecutionError(completed_result.error_text)
2403
+ completed_id = self._decode_main_completion_value(
2404
+ completed_result,
2405
+ phase="completed_command",
2406
+ )
2407
+ if completed_id != self.active_operation.operation_id:
2408
+ self.state = OperationState.FAILED
2409
+ raise ProtocolError(
2410
+ f"Completed command {completed_id!r} does not match active "
2411
+ f"operation {self.active_operation.operation_id}"
2412
+ )
2413
+ result_evaluation = self.session.evaluate("Результат")
2414
+ error_evaluation = self.session.evaluate("Ошибка")
2415
+ if result_evaluation.error_occurred:
2416
+ self.state = OperationState.FAILED
2417
+ raise BslExecutionError(result_evaluation.error_text)
2418
+ if error_evaluation.error_occurred:
2419
+ self.state = OperationState.FAILED
2420
+ raise BslExecutionError(error_evaluation.error_text)
2421
+ error_value = self._decode_main_completion_value(
2422
+ error_evaluation,
2423
+ phase="error",
2424
+ )
2425
+ error = "" if error_value is None else str(error_value)
2426
+ diagnostic = None
2427
+ if error and self.active_operation.executed_source is not None:
2428
+ diagnostic = _safe_platform_diagnostic(
2429
+ error,
2430
+ self.active_operation.executed_source,
2431
+ visible_source_context=self.active_operation.visible_source_context,
2432
+ )
2433
+ self.state = OperationState.FAILED if error else OperationState.COMPLETED
2434
+ result_value = self._decode_main_completion_value(
2435
+ result_evaluation,
2436
+ phase="result",
2437
+ )
2438
+ messages = self._take_kernel_cell_messages(
2439
+ self.active_operation.message_collector_key
2440
+ if self.active_operation.messages_intercepted
2441
+ else ""
2442
+ )
2443
+ completion = MainCompletion(
2444
+ self.active_operation,
2445
+ result_value,
2446
+ error,
2447
+ not error,
2448
+ messages,
2449
+ diagnostic,
2450
+ )
2451
+ self._record(
2452
+ "write-journal.jsonl",
2453
+ "main_completed" if completion.succeeded else "main_failed",
2454
+ runtime_generation=self.runtime_generation,
2455
+ operation_id=self.active_operation.operation_id,
2456
+ state_after=self.state.value,
2457
+ visible_sha256=sha256(
2458
+ self.active_operation.visible_source.encode("utf-8")
2459
+ ).hexdigest(),
2460
+ lowered_sha256=sha256(
2461
+ self.active_operation.lowered_source.encode("utf-8")
2462
+ ).hexdigest(),
2463
+ )
2464
+ self._flush_journal()
2465
+ return completion
2466
+
2467
+ @staticmethod
2468
+ def _decode_main_completion_value(
2469
+ evaluation: EvaluationResult,
2470
+ *,
2471
+ phase: str,
2472
+ ) -> object:
2473
+ try:
2474
+ return evaluation_to_python(evaluation)
2475
+ except Exception as error:
2476
+ raise CompletionDecodeError(phase, evaluation) from error
2477
+
2478
+ def _take_capture_cell_messages(
2479
+ self, message_collector_key: str
2480
+ ) -> tuple[str, ...]:
2481
+ return self._take_context_cell_messages(
2482
+ message_collector_key,
2483
+ stack_level=self._required_capture_kernel_stack_level(),
2484
+ )
2485
+
2486
+ def _take_kernel_cell_messages(
2487
+ self, message_collector_key: str
2488
+ ) -> tuple[str, ...]:
2489
+ return self._take_context_cell_messages(message_collector_key, stack_level=0)
2490
+
2491
+ def _take_context_cell_messages(
2492
+ self, message_collector_key: str, *, stack_level: int
2493
+ ) -> tuple[str, ...]:
2494
+ if not message_collector_key:
2495
+ return ()
2496
+ result = self.session.evaluate(
2497
+ "RuntimeKernelServer.ЗабратьСообщенияЯчейкиИзКонтекста(Контекст, "
2498
+ + bsl_string_literal(message_collector_key)
2499
+ + ")",
2500
+ stack_level=stack_level,
2501
+ )
2502
+ if result.error_occurred:
2503
+ raise BslExecutionError(result.error_text)
2504
+ payload = evaluation_to_python(result)
2505
+ if not isinstance(payload, str):
2506
+ raise ProtocolError("Cell message payload is not JSON text")
2507
+ try:
2508
+ messages = json.loads(payload)
2509
+ except json.JSONDecodeError as error:
2510
+ raise ProtocolError("Cell message payload is invalid JSON") from error
2511
+ if not isinstance(messages, list) or any(
2512
+ not isinstance(message, str) for message in messages
2513
+ ):
2514
+ raise ProtocolError("Cell message payload is not a text array")
2515
+ return tuple(messages)
2516
+
2517
+ def take_context_string(self, key: str, *, max_text_size: int) -> str:
2518
+ if not re.fullmatch(
2519
+ r"__(?:onec_compact_table|onec_value)_[0-9a-f]{32}", key
2520
+ ):
2521
+ raise ProtocolError("materialization context key is invalid")
2522
+ if type(max_text_size) is not int or max_text_size <= 0:
2523
+ raise ProtocolError("compact table maximum text size is invalid")
2524
+ stack_level = (
2525
+ self._required_capture_kernel_stack_level()
2526
+ if self.state is OperationState.CAPTURED
2527
+ else 0
2528
+ )
2529
+ result = self.session.evaluate(
2530
+ "RuntimeKernelServer."
2531
+ "ЗабратьКомпактнуюМатериализациюИзКонтекста(Контекст, "
2532
+ + bsl_string_literal(key)
2533
+ + ")",
2534
+ timeout_s=self.command_timeout_s,
2535
+ max_text_size=max_text_size,
2536
+ stack_level=stack_level,
2537
+ )
2538
+ if result.error_occurred:
2539
+ raise BslExecutionError(result.error_text)
2540
+ value = evaluation_to_python(result)
2541
+ if not isinstance(value, str):
2542
+ raise ProtocolError("compact table payload is not a string")
2543
+ return value
2544
+
2545
+ def drop_context_value(self, key: str) -> None:
2546
+ if not re.fullmatch(
2547
+ r"__(?:onec_compact_table|onec_value|onec_projection)_[0-9a-f]{32}",
2548
+ key,
2549
+ ):
2550
+ raise ProtocolError("materialization context key is invalid")
2551
+ stack_level = (
2552
+ self._required_capture_kernel_stack_level()
2553
+ if self.state is OperationState.CAPTURED
2554
+ else 0
2555
+ )
2556
+ result = self.session.evaluate(
2557
+ "RuntimeKernelServer."
2558
+ "УдалитьМатериализациюИзКонтекста(Контекст, "
2559
+ + bsl_string_literal(key)
2560
+ + ")",
2561
+ timeout_s=self.command_timeout_s,
2562
+ stack_level=stack_level,
2563
+ )
2564
+ if result.error_occurred:
2565
+ raise BslExecutionError(result.error_text)
2566
+
2567
+ def inspect_table_sample(
2568
+ self,
2569
+ handle: str,
2570
+ *,
2571
+ page_size: int = 16,
2572
+ ) -> EvaluationResult:
2573
+ if not re.fullmatch(
2574
+ r"Контекст\.[^\W\d]\w*(?:\.[^\W\d]\w*)*", handle, re.UNICODE
2575
+ ):
2576
+ raise ProtocolError(
2577
+ "table handle must be one direct or dotted persistent Context path"
2578
+ )
2579
+ if type(page_size) is not int or not 1 <= page_size <= 64:
2580
+ raise ProtocolError("table schema sample size is invalid")
2581
+ stack_level = (
2582
+ self._required_capture_kernel_stack_level()
2583
+ if self.state is OperationState.CAPTURED
2584
+ else 0
2585
+ )
2586
+ result = self.session.evaluate_collection(
2587
+ handle,
2588
+ start_index=0,
2589
+ page_size=page_size,
2590
+ timeout_s=self.command_timeout_s,
2591
+ max_text_size=4096,
2592
+ stack_level=stack_level,
2593
+ )
2594
+ if result.error_occurred:
2595
+ raise BslExecutionError(result.error_text)
2596
+ return result
2597
+
2598
+ def inspect_completion_fields(self, handle: str, *, table_row: bool) -> EvaluationResult:
2599
+ """Inspect bounded column/key names, without serializing field values."""
2600
+ if (not isinstance(handle, str) or len(handle) > 512
2601
+ or not re.fullmatch(r"Контекст\.[^\W\d]\w*(?:\.[^\W\d]\w*){0,7}", handle)
2602
+ or type(table_row) is not bool):
2603
+ raise ProtocolError("Completion requires a direct or dotted Context path")
2604
+ stack_level = (
2605
+ self._required_capture_kernel_stack_level()
2606
+ if self.state is OperationState.CAPTURED else 0
2607
+ )
2608
+ result = self.session.evaluate_collection(
2609
+ "RuntimeValueTransferServer.ПолучитьИменаСвойствДляПодсказки("
2610
+ + handle + (", Истина)" if table_row else ", Ложь)"),
2611
+ start_index=0, page_size=128, max_text_size=512,
2612
+ timeout_s=self.command_timeout_s, stack_level=stack_level,
2613
+ )
2614
+ if result.error_occurred:
2615
+ raise BslExecutionError(result.error_text)
2616
+ return result
2617
+
2618
+ def inspect_declared_table_schema(
2619
+ self, handle: str, *, timeout_s: float | None = None
2620
+ ) -> EvaluationResult:
2621
+ deadline = self._capture_command_deadline(timeout_s)
2622
+ if not re.fullmatch(
2623
+ r"Контекст\.[^\W\d]\w*(?:\.[^\W\d]\w*)*", handle, re.UNICODE
2624
+ ):
2625
+ raise ProtocolError(
2626
+ "table handle must be one direct or dotted persistent Context path"
2627
+ )
2628
+ stack_level = (
2629
+ self._required_capture_kernel_stack_level()
2630
+ if self.state is OperationState.CAPTURED
2631
+ else 0
2632
+ )
2633
+ result = self.session.evaluate_collection(
2634
+ "RuntimeTableTransferServer.ПолучитьКомпактнуюСхему(" + handle + ")",
2635
+ start_index=0,
2636
+ page_size=64,
2637
+ timeout_s=self._capture_remaining_timeout(deadline),
2638
+ max_text_size=4096,
2639
+ stack_level=stack_level,
2640
+ )
2641
+ self._capture_remaining_timeout(deadline)
2642
+ if result.error_occurred:
2643
+ raise BslExecutionError(result.error_text)
2644
+ return result