ithrl 0.1.0__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 (108) hide show
  1. ithrl/__init__.py +1 -0
  2. ithrl/__main__.py +6 -0
  3. ithrl/adapters/__init__.py +49 -0
  4. ithrl/adapters/_jsonl.py +266 -0
  5. ithrl/adapters/codex/__init__.py +5 -0
  6. ithrl/adapters/codex/context.py +204 -0
  7. ithrl/adapters/codex/decisions.py +224 -0
  8. ithrl/adapters/codex/discovery.py +60 -0
  9. ithrl/adapters/codex/parser.py +191 -0
  10. ithrl/adapters/contracts.py +106 -0
  11. ithrl/adapters/copilot/__init__.py +5 -0
  12. ithrl/adapters/copilot/context.py +182 -0
  13. ithrl/adapters/copilot/decisions.py +294 -0
  14. ithrl/adapters/copilot/discovery.py +28 -0
  15. ithrl/adapters/copilot/parser.py +283 -0
  16. ithrl/adapters/discovery.py +220 -0
  17. ithrl/adapters/errors.py +52 -0
  18. ithrl/adapters/registry.py +433 -0
  19. ithrl/behavior.py +138 -0
  20. ithrl/calibration.py +434 -0
  21. ithrl/case/__init__.py +33 -0
  22. ithrl/case/contracts.py +88 -0
  23. ithrl/case/evidence.py +169 -0
  24. ithrl/case/filesystem.py +340 -0
  25. ithrl/case/source.py +208 -0
  26. ithrl/case/source_context.py +119 -0
  27. ithrl/case/source_storage.py +234 -0
  28. ithrl/case/storage.py +377 -0
  29. ithrl/cli/__init__.py +968 -0
  30. ithrl/cli/extract.py +478 -0
  31. ithrl/cli/native.py +484 -0
  32. ithrl/cli/output.py +786 -0
  33. ithrl/cli/paths.py +24 -0
  34. ithrl/cli/sampled.py +485 -0
  35. ithrl/cli/sampled_output.py +472 -0
  36. ithrl/cli/sampled_reduction.py +1036 -0
  37. ithrl/cli/sampled_session.py +342 -0
  38. ithrl/cli/shrink.py +523 -0
  39. ithrl/context.py +465 -0
  40. ithrl/decisions.py +271 -0
  41. ithrl/examples/synthetic-trace.json +46 -0
  42. ithrl/execution/__init__.py +68 -0
  43. ithrl/execution/budget.py +146 -0
  44. ithrl/execution/contracts.py +426 -0
  45. ithrl/execution/preflight.py +346 -0
  46. ithrl/experiment.py +453 -0
  47. ithrl/extraction.py +206 -0
  48. ithrl/measurement.py +315 -0
  49. ithrl/observations.py +1148 -0
  50. ithrl/path_safety.py +100 -0
  51. ithrl/reduce/__init__.py +53 -0
  52. ithrl/reduce/contracts.py +508 -0
  53. ithrl/reduce/engine.py +901 -0
  54. ithrl/reduce/search.py +178 -0
  55. ithrl/results/__init__.py +49 -0
  56. ithrl/results/models.py +294 -0
  57. ithrl/results/storage.py +485 -0
  58. ithrl/results/summary.py +420 -0
  59. ithrl/results/validation.py +611 -0
  60. ithrl/run_state.py +1135 -0
  61. ithrl/runner/__init__.py +44 -0
  62. ithrl/runner/codex/__init__.py +17 -0
  63. ithrl/runner/codex/capability.py +708 -0
  64. ithrl/runner/codex/configuration.py +529 -0
  65. ithrl/runner/codex/isolation.py +396 -0
  66. ithrl/runner/codex/materialize.py +358 -0
  67. ithrl/runner/codex/policy.py +67 -0
  68. ithrl/runner/codex/protocol.py +649 -0
  69. ithrl/runner/codex/protocol_contracts.py +171 -0
  70. ithrl/runner/codex/runner.py +533 -0
  71. ithrl/runner/codex/runtime_configuration.py +264 -0
  72. ithrl/runner/codex/schema.py +527 -0
  73. ithrl/runner/codex/transcript.py +130 -0
  74. ithrl/runner/codex/worker.py +175 -0
  75. ithrl/runner/contracts.py +419 -0
  76. ithrl/runner/copilot/__init__.py +18 -0
  77. ithrl/runner/copilot/capability.py +825 -0
  78. ithrl/runner/copilot/configuration.py +201 -0
  79. ithrl/runner/copilot/isolation.py +374 -0
  80. ithrl/runner/copilot/materialize.py +484 -0
  81. ithrl/runner/copilot/policy.py +110 -0
  82. ithrl/runner/copilot/protocol/__init__.py +18 -0
  83. ithrl/runner/copilot/protocol/contracts.py +74 -0
  84. ithrl/runner/copilot/protocol/decoding.py +93 -0
  85. ithrl/runner/copilot/protocol/parser.py +418 -0
  86. ithrl/runner/copilot/protocol/state.py +130 -0
  87. ithrl/runner/copilot/runner.py +425 -0
  88. ithrl/runner/copilot/worker.py +351 -0
  89. ithrl/runner/evidence.py +345 -0
  90. ithrl/runner/fake.py +107 -0
  91. ithrl/runner/registry.py +90 -0
  92. ithrl/runner/runtime/__init__.py +50 -0
  93. ithrl/runner/runtime/contracts.py +157 -0
  94. ithrl/runner/runtime/output.py +184 -0
  95. ithrl/runner/runtime/process.py +954 -0
  96. ithrl/runner/runtime/protection.py +562 -0
  97. ithrl/runner/runtime/state.py +282 -0
  98. ithrl/runner/validation.py +376 -0
  99. ithrl/sampling.py +412 -0
  100. ithrl/statistics.py +276 -0
  101. ithrl/synthetic.py +511 -0
  102. ithrl/timestamps.py +26 -0
  103. ithrl/workflow.py +570 -0
  104. ithrl-0.1.0.dist-info/METADATA +118 -0
  105. ithrl-0.1.0.dist-info/RECORD +108 -0
  106. ithrl-0.1.0.dist-info/WHEEL +4 -0
  107. ithrl-0.1.0.dist-info/entry_points.txt +2 -0
  108. ithrl-0.1.0.dist-info/licenses/LICENSE +201 -0
ithrl/run_state.py ADDED
@@ -0,0 +1,1135 @@
1
+ """Atomic mutable state for an in-progress reduction run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import stat
8
+ import tempfile
9
+ import threading
10
+ from collections.abc import Generator
11
+ from contextlib import contextmanager
12
+ from dataclasses import dataclass
13
+ from datetime import UTC, datetime
14
+ from pathlib import Path
15
+ from typing import Annotated, Literal
16
+
17
+ from filelock import Timeout
18
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator, model_validator
19
+
20
+ from ithrl.case.contracts import CaseError
21
+ from ithrl.case.filesystem import read_safe_bytes
22
+ from ithrl.context import content_digest
23
+ from ithrl.observations import (
24
+ Observation,
25
+ ObservationArm,
26
+ ObservationIdentity,
27
+ ObservationRole,
28
+ ObservationStore,
29
+ ObservationStoreError,
30
+ SamplingArm,
31
+ SamplingAttempt,
32
+ SamplingBlockEvidence,
33
+ SamplingBlockPlan,
34
+ SamplingBudgetAllowance,
35
+ )
36
+ from ithrl.path_safety import UnsafePathError, private_file_lock
37
+ from ithrl.reduce.contracts import ReductionCheckpoint, SampledReductionCheckpoint
38
+ from ithrl.runner import ProviderAcceptanceState
39
+
40
+ _MAX_RUN_STATE_BYTES = 64 * 1024 * 1024
41
+
42
+
43
+ class RunStateError(ValueError):
44
+ """An in-progress run state is unavailable, unsafe, or inconsistent."""
45
+
46
+
47
+ class RunStateBusyError(RunStateError):
48
+ """Another process holds the exclusive lock for an in-progress run."""
49
+
50
+
51
+ class RunState(BaseModel):
52
+ """The complete state needed to continue an interrupted process."""
53
+
54
+ model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
55
+
56
+ schema_version: Literal[1] = 1
57
+ run_id: Annotated[str, Field(min_length=1, max_length=128)]
58
+ source_case: Annotated[str, Field(min_length=1, max_length=4096)]
59
+ observation_store: Annotated[str, Field(min_length=1, max_length=4096)]
60
+ checkpoint: ReductionCheckpoint
61
+ status: Literal["active", "completed"] = "active"
62
+ result_path: Annotated[str | None, Field(min_length=1)] = None
63
+
64
+ @model_validator(mode="after")
65
+ def validate_completion(self) -> RunState:
66
+ if (self.status == "completed") != (self.result_path is not None):
67
+ raise ValueError("only completed run state may reference a result")
68
+ return self
69
+
70
+
71
+ class RunStateFile:
72
+ """A private, atomically replaced state file outside immutable results."""
73
+
74
+ def __init__(self, path: Path) -> None:
75
+ self.path = path if path.is_absolute() else Path.cwd() / path
76
+
77
+ @classmethod
78
+ def create(
79
+ cls,
80
+ *,
81
+ case_root: Path,
82
+ run_id: str,
83
+ observation_store: Path,
84
+ checkpoint: ReductionCheckpoint,
85
+ ) -> RunStateFile:
86
+ runs_root = case_root / ".runs"
87
+ _reject_unsafe_path(runs_root)
88
+ runs_root.mkdir(mode=0o700, exist_ok=True)
89
+ if os.name == "posix":
90
+ runs_root.chmod(0o700)
91
+ root = runs_root / run_id
92
+ _reject_unsafe_path(root)
93
+ root.mkdir(mode=0o700, exist_ok=False)
94
+ if os.name == "posix":
95
+ root.chmod(0o700)
96
+ state_file = cls(root / "state.json")
97
+ state_file._write(
98
+ RunState(
99
+ run_id=run_id,
100
+ source_case=str(case_root.absolute()),
101
+ observation_store=str(observation_store.absolute()),
102
+ checkpoint=checkpoint,
103
+ )
104
+ )
105
+ return state_file
106
+
107
+ @classmethod
108
+ def open(cls, source: Path) -> RunStateFile:
109
+ path = source / "state.json" if source.is_dir() else source
110
+ return cls(path)
111
+
112
+ def load(self) -> RunState:
113
+ """Load and validate one private state file without accepting a symbolic link."""
114
+
115
+ _reject_unsafe_path(self.path)
116
+ try:
117
+ metadata = self.path.lstat()
118
+ except OSError as error:
119
+ raise RunStateError(f"run state is unavailable: {self.path}") from error
120
+ if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
121
+ raise RunStateError("run state must be a regular file, not a link")
122
+ if os.name == "posix" and stat.S_IMODE(metadata.st_mode) != 0o600:
123
+ raise RunStateError("run state must have mode 0600")
124
+ if metadata.st_size > _MAX_RUN_STATE_BYTES:
125
+ raise RunStateError(f"run state exceeds the {_MAX_RUN_STATE_BYTES}-byte limit")
126
+ if os.name == "posix":
127
+ parent_mode = stat.S_IMODE(self.path.parent.stat().st_mode)
128
+ if parent_mode != 0o700:
129
+ raise RunStateError("run-state directory must have mode 0700")
130
+ try:
131
+ source = self.path.read_text(encoding="utf-8")
132
+ except (OSError, UnicodeError) as error:
133
+ raise RunStateError(f"run state is unavailable: {self.path}") from error
134
+
135
+ def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
136
+ result: dict[str, object] = {}
137
+ for key, value in pairs:
138
+ if key in result:
139
+ raise RunStateError(f"run state contains duplicate key {key!r}")
140
+ result[key] = value
141
+ return result
142
+
143
+ try:
144
+ payload = json.loads(source, object_pairs_hook=reject_duplicate_keys)
145
+ return RunState.model_validate(payload)
146
+ except (json.JSONDecodeError, RecursionError, ValidationError) as error:
147
+ raise RunStateError(f"invalid run state: {error}") from error
148
+
149
+ def save_checkpoint(self, checkpoint: ReductionCheckpoint) -> None:
150
+ current = self.load()
151
+ if current.status == "completed":
152
+ raise RunStateError(f"run already completed: {current.result_path}")
153
+ self._write(current.model_copy(update={"checkpoint": checkpoint}))
154
+
155
+ @contextmanager
156
+ def acquire_lock(self) -> Generator[None]:
157
+ """Prevent concurrent execution against the active run."""
158
+
159
+ lock_path = self.path.with_name("run.lock")
160
+ _reject_unsafe_path(lock_path)
161
+ try:
162
+ lock = private_file_lock(lock_path)
163
+ except UnsafePathError as error:
164
+ raise RunStateError("run lock file is unsafe") from error
165
+ try:
166
+ with lock:
167
+ yield
168
+ except Timeout as error:
169
+ raise RunStateBusyError("another process is already using the active run") from error
170
+
171
+ def complete(self, result_path: Path) -> None:
172
+ current = self.load()
173
+ self._write(
174
+ current.model_copy(
175
+ update={"status": "completed", "result_path": str(result_path.absolute())}
176
+ )
177
+ )
178
+
179
+ def _write(self, state: RunState) -> None:
180
+ _reject_unsafe_path(self.path.parent)
181
+ temporary = self.path.with_name(f".{self.path.name}.tmp")
182
+ if temporary.exists() or temporary.is_symlink():
183
+ temporary.unlink()
184
+ rendered = json.dumps(
185
+ state.model_dump(mode="json"),
186
+ allow_nan=False,
187
+ ensure_ascii=False,
188
+ indent=2,
189
+ sort_keys=True,
190
+ )
191
+ if len(rendered.encode("utf-8")) + 1 > _MAX_RUN_STATE_BYTES:
192
+ raise RunStateError(f"run state exceeds the {_MAX_RUN_STATE_BYTES}-byte limit")
193
+ try:
194
+ with temporary.open("x", encoding="utf-8", newline="\n") as stream:
195
+ stream.write(f"{rendered}\n")
196
+ stream.flush()
197
+ os.fsync(stream.fileno())
198
+ if os.name == "posix":
199
+ temporary.chmod(0o600)
200
+ temporary.replace(self.path)
201
+ _sync_directory(self.path.parent)
202
+ except BaseException:
203
+ temporary.unlink(missing_ok=True)
204
+ raise
205
+
206
+
207
+ def _reject_unsafe_path(path: Path) -> None:
208
+ if ".." in path.parts:
209
+ raise RunStateError("run-state path cannot contain '..'")
210
+ absolute = path if path.is_absolute() else Path.cwd() / path
211
+ current = Path(absolute.anchor)
212
+ for part in absolute.parts[1:]:
213
+ current /= part
214
+ if os.path.lexists(current) and current.is_symlink():
215
+ raise RunStateError(f"run-state path contains a symbolic link: {current}")
216
+
217
+
218
+ def _sync_directory(path: Path) -> None:
219
+ if os.name != "posix":
220
+ return
221
+ descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
222
+ try:
223
+ os.fsync(descriptor)
224
+ finally:
225
+ os.close(descriptor)
226
+
227
+
228
+ type _StateDigest = Annotated[str, Field(pattern=r"^[0-9a-f]{64}$")]
229
+ type _PrivateRunId = Annotated[str, Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$")]
230
+
231
+
232
+ class _SampledStateContract(BaseModel):
233
+ model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
234
+
235
+
236
+ class SamplingObservationReference(_SampledStateContract):
237
+ """Membership and prepared-attempt binding; observation facts remain in SQLite."""
238
+
239
+ observation_id: Annotated[str, Field(min_length=1, max_length=128)]
240
+ attempt_id: Annotated[str, Field(pattern=r"^attempt-[0-9a-f]{32}$")]
241
+ attempt_digest: _StateDigest
242
+ schedule_position: Annotated[int, Field(ge=0)]
243
+ independence_evidence_digest: _StateDigest | None = None
244
+
245
+
246
+ class SamplingBlockProgress(_SampledStateContract):
247
+ """One immutable admitted plan and its append-only ordered observation references."""
248
+
249
+ plan: SamplingBlockPlan
250
+ observations: Annotated[tuple[SamplingObservationReference, ...], Field(max_length=20_000)] = ()
251
+ archived: bool = False
252
+
253
+ @model_validator(mode="after")
254
+ def validate_positions(self) -> SamplingBlockProgress:
255
+ positions = tuple(item.schedule_position for item in self.observations)
256
+ if positions != tuple(sorted(set(positions))):
257
+ raise ValueError("sampling references must have unique increasing positions")
258
+ if positions and positions[-1] >= len(self.plan.schedule):
259
+ raise ValueError("sampling reference exceeds the frozen schedule")
260
+ return self
261
+
262
+
263
+ class SamplingBudgetApproval(_SampledStateContract):
264
+ """Recorded administrative allowance; this reference grants no execution authority."""
265
+
266
+ approval_id: _PrivateRunId
267
+ approved_at: datetime
268
+ allowance: SamplingBudgetAllowance
269
+
270
+ @field_validator("approved_at")
271
+ @classmethod
272
+ def normalize_time(cls, value: datetime) -> datetime:
273
+ return _sampled_time(value)
274
+
275
+
276
+ class SamplingDispatchReservation(_SampledStateContract):
277
+ """Durable charge and exact reconciliation key, published before native dispatch."""
278
+
279
+ block_id: Annotated[str, Field(min_length=1, max_length=128)]
280
+ block_digest: _StateDigest
281
+ schedule_position: Annotated[int, Field(ge=0)]
282
+ arm: SamplingArm
283
+ attempt: Annotated[int, Field(ge=1)]
284
+ attempt_id: Annotated[str, Field(pattern=r"^attempt-[0-9a-f]{32}$")]
285
+ attempt_digest: _StateDigest
286
+ reserved_at: datetime
287
+ provider_calls: Annotated[int, Field(ge=1, le=2)]
288
+ runner_seconds: Annotated[int, Field(ge=1)]
289
+
290
+ @field_validator("reserved_at")
291
+ @classmethod
292
+ def normalize_time(cls, value: datetime) -> datetime:
293
+ return _sampled_time(value)
294
+
295
+
296
+ class SampledRunUsage(_SampledStateContract):
297
+ """Known call lower bounds, conservative upper bounds and separately charged dispatch.
298
+
299
+ Missing provider accounting remains explicit, including accepted attempts whose evidence
300
+ proves at least one call but cannot establish the complete count.
301
+ """
302
+
303
+ actual_provider_calls: int
304
+ possible_provider_calls: int
305
+ unknown_provider_attempts: int
306
+ pending_provider_calls: int
307
+ runner_milliseconds: int
308
+ unknown_runner_attempts: int
309
+ known_tokens: int
310
+ unknown_token_attempts: int
311
+ wall_time_seconds: float
312
+ pending_runner_seconds: int
313
+
314
+ @property
315
+ def charged_provider_calls(self) -> int:
316
+ return (
317
+ self.actual_provider_calls + self.possible_provider_calls + self.pending_provider_calls
318
+ )
319
+
320
+
321
+ class SampledFinalSelection(_SampledStateContract):
322
+ """One selected artifact and holdout plan, frozen before any confirmation dispatch."""
323
+
324
+ checkpoint_digest: _StateDigest
325
+ selected_at: datetime
326
+ plan: SamplingBlockPlan
327
+ search_stop_reason: (
328
+ Literal[
329
+ "candidate-limit",
330
+ "trial-limit",
331
+ "provider-call-budget",
332
+ "wall-time-budget",
333
+ "freshness-budget",
334
+ "attempt-limit",
335
+ ]
336
+ | None
337
+ ) = None
338
+
339
+ @field_validator("selected_at")
340
+ @classmethod
341
+ def normalize_time(cls, value: datetime) -> datetime:
342
+ return _sampled_time(value)
343
+
344
+ @model_validator(mode="after")
345
+ def validate_plan(self) -> SampledFinalSelection:
346
+ if self.plan.role != "confirmation" or self.plan.frozen_at <= self.selected_at:
347
+ raise ValueError("a fresh confirmation plan must follow selection")
348
+ return self
349
+
350
+
351
+ class SampledRunState(_SampledStateContract):
352
+ """Private sampled progress, with administrative history outside frozen execution identities."""
353
+
354
+ run_id: _PrivateRunId
355
+ source_case: Annotated[str, Field(min_length=1, max_length=4096)]
356
+ observation_store: Annotated[str, Field(min_length=1, max_length=4096)]
357
+ started_at: datetime
358
+ original_identity: ObservationIdentity
359
+ original_candidate_digest: _StateDigest
360
+ provider_calls_per_attempt: Annotated[int, Field(ge=1, le=2)]
361
+ approvals: Annotated[tuple[SamplingBudgetApproval, ...], Field(min_length=1, max_length=10_000)]
362
+ calibration_observation_ids: Annotated[tuple[str, ...], Field(max_length=16)] = ()
363
+ blocks: Annotated[tuple[SamplingBlockProgress, ...], Field(max_length=10_000)] = ()
364
+ pending: SamplingDispatchReservation | None = None
365
+ checkpoint: SampledReductionCheckpoint | None = None
366
+ selection: SampledFinalSelection | None = None
367
+
368
+ @field_validator("started_at")
369
+ @classmethod
370
+ def normalize_time(cls, value: datetime) -> datetime:
371
+ return _sampled_time(value)
372
+
373
+ @model_validator(mode="after")
374
+ def validate_history(self) -> SampledRunState:
375
+ if any(item.allowance.run_id != self.run_id for item in self.approvals):
376
+ raise ValueError("budget approvals must belong to this run")
377
+ if len({item.approval_id for item in self.approvals}) != len(self.approvals):
378
+ raise ValueError("budget approval IDs must be unique")
379
+ if self.approvals[0].approved_at > self.started_at:
380
+ raise ValueError("initial allowance must precede run start")
381
+ for earlier, later in zip(self.approvals, self.approvals[1:], strict=False):
382
+ _validate_budget_extension(earlier, later)
383
+ if len({item.plan.block_id for item in self.blocks}) != len(self.blocks):
384
+ raise ValueError("block IDs must be unique")
385
+ previous_time = self.started_at
386
+ for index, progress in enumerate(self.blocks):
387
+ plan = progress.plan
388
+ if plan.original_identity != self.original_identity:
389
+ raise ValueError("block original identity must match the run")
390
+ if plan.original_candidate_digest != self.original_candidate_digest:
391
+ raise ValueError("block original materialization must match the run")
392
+ if plan.provider_calls_per_attempt != self.provider_calls_per_attempt:
393
+ raise ValueError("blocks must preserve the exact run profile call bound")
394
+ if plan.frozen_at < previous_time:
395
+ raise ValueError("blocks must follow run chronology")
396
+ previous_time = plan.frozen_at
397
+ if index + 1 < len(self.blocks) and not progress.archived:
398
+ raise ValueError("only the final block may remain active")
399
+ if self.blocks and plan.reduction != self.blocks[0].plan.reduction:
400
+ raise ValueError("blocks must preserve the frozen reduction plan")
401
+ references = tuple(item for block in self.blocks for item in block.observations)
402
+ observation_ids = (
403
+ *self.calibration_observation_ids,
404
+ *(r.observation_id for r in references),
405
+ )
406
+ if len(set(observation_ids)) != len(observation_ids):
407
+ raise ValueError("observation references must be unique across the run")
408
+ if len({item.attempt_id for item in references}) != len(references):
409
+ raise ValueError("prepared attempts must be unique across blocks")
410
+ if self.pending is not None:
411
+ reservation = self.pending
412
+ if not self.blocks or self.blocks[-1].archived:
413
+ raise ValueError("dispatch requires an active block")
414
+ plan = self.blocks[-1].plan
415
+ if reservation.block_id != plan.block_id or reservation.block_digest != plan.digest:
416
+ raise ValueError("dispatch must reference the exact frozen block")
417
+ if reservation.schedule_position >= len(plan.schedule):
418
+ raise ValueError("dispatch exceeds the frozen schedule")
419
+ if reservation.arm != plan.schedule[reservation.schedule_position]:
420
+ raise ValueError("dispatch arm must match the frozen schedule")
421
+ if reservation.provider_calls != plan.provider_calls_per_attempt:
422
+ raise ValueError("dispatch charge must match the exact profile bound")
423
+ if not plan.frozen_at <= reservation.reserved_at < plan.expires_at:
424
+ raise ValueError("dispatch must be reserved within frozen freshness")
425
+ if reservation.attempt_id in {item.attempt_id for item in references}:
426
+ raise ValueError("dispatch cannot repeat a published prepared attempt")
427
+ if self.selection is not None:
428
+ selection, checkpoint = self.selection, self.checkpoint
429
+ plan = selection.plan
430
+ holdout_id = content_digest({"run_id": self.run_id, "role": "confirmation"})
431
+ if plan.block_id != f"confirmation-{holdout_id}" or plan.schedule_seed != int(
432
+ holdout_id[:16], 16
433
+ ):
434
+ raise ValueError("confirmation must use the single frozen run holdout")
435
+ if checkpoint is None or selection.checkpoint_digest != content_digest(
436
+ checkpoint.model_dump(mode="json")
437
+ ):
438
+ raise ValueError("selected search checkpoint cannot change")
439
+ if (
440
+ plan.reduction != checkpoint.reduction
441
+ or plan.original_identity != checkpoint.original_identity
442
+ or plan.original_candidate_digest != checkpoint.original_candidate_digest
443
+ or plan.candidate_identity != checkpoint.best_candidate_identity
444
+ or plan.candidate_digest != checkpoint.best_candidate_digest
445
+ ):
446
+ raise ValueError("confirmation must bind the selected candidate and original")
447
+ confirmation = tuple(b for b in self.blocks if b.plan.role == "confirmation")
448
+ if len(confirmation) > 1 or (
449
+ confirmation
450
+ and (self.blocks[-1] != confirmation[0] or confirmation[0].plan != plan)
451
+ ):
452
+ raise ValueError("selection permits only its single final confirmation block")
453
+ return self
454
+
455
+ @property
456
+ def allowance(self) -> SamplingBudgetAllowance:
457
+ return self.approvals[-1].allowance
458
+
459
+ def validate_observations(self, store: ObservationStore) -> None:
460
+ """Resolve immutable references and validate any derived checkpoint against SQLite."""
461
+
462
+ if Path(self.observation_store) != store.path.absolute():
463
+ raise RunStateError("sampled run observation-store identity does not match")
464
+ SampledRunStateFile._validate_authority(self, store)
465
+
466
+
467
+ def _sampled_time(value: datetime) -> datetime:
468
+ if value.tzinfo is None or value.utcoffset() is None:
469
+ raise ValueError("sampled state times must include a UTC offset")
470
+ return value.astimezone(UTC)
471
+
472
+
473
+ def _validate_budget_extension(
474
+ earlier: SamplingBudgetApproval, later: SamplingBudgetApproval
475
+ ) -> None:
476
+ if later.approved_at < earlier.approved_at:
477
+ raise ValueError("budget approvals must follow chronological order")
478
+ original, extended = earlier.allowance, later.allowance
479
+ limits = ("max_provider_calls", "max_wall_time_seconds", "max_candidate_comparisons")
480
+ if any(getattr(extended, name) < getattr(original, name) for name in limits):
481
+ raise ValueError("administrative budget limits cannot decrease")
482
+ if original.max_tokens is None:
483
+ if extended.max_tokens is not None:
484
+ raise ValueError("an unbounded token allowance cannot become smaller")
485
+ elif extended.max_tokens is not None and extended.max_tokens < original.max_tokens:
486
+ raise ValueError("token allowance cannot decrease")
487
+ fixed = (
488
+ "run_id",
489
+ "confirmation_provider_calls",
490
+ "confirmation_runner_seconds",
491
+ "inter_attempt_delay_seconds",
492
+ "token_accounting",
493
+ )
494
+ if any(getattr(extended, name) != getattr(original, name) for name in fixed):
495
+ raise ValueError("budget extensions must preserve confirmation reserves and accounting")
496
+
497
+
498
+ @dataclass(frozen=True, slots=True, repr=False)
499
+ class _SampledRunSnapshot:
500
+ """Private in-memory evidence resolved by one fresh owned state read."""
501
+
502
+ state: SampledRunState
503
+ calibration: tuple[Observation, ...]
504
+ blocks: tuple[SamplingBlockEvidence, ...]
505
+ usage: SampledRunUsage
506
+
507
+
508
+ class SampledRunStateFile:
509
+ """Owned, atomic sampled progress; authoritative observations are never replayed."""
510
+
511
+ def __init__(self, path: Path) -> None:
512
+ self.path = path.absolute()
513
+ self._owner: tuple[int, int] | None = None
514
+ self._store_path: Path | None = None
515
+
516
+ @classmethod
517
+ def create(
518
+ cls,
519
+ *,
520
+ case_root: Path,
521
+ run_id: str,
522
+ store: ObservationStore,
523
+ started_at: datetime,
524
+ original_identity: ObservationIdentity,
525
+ original_candidate_digest: str,
526
+ provider_calls_per_attempt: int,
527
+ approval: SamplingBudgetApproval,
528
+ calibration_observation_ids: tuple[str, ...] = (),
529
+ ) -> SampledRunStateFile:
530
+ try:
531
+ state = SampledRunState(
532
+ run_id=run_id,
533
+ source_case=str(case_root.absolute()),
534
+ observation_store=str(store.path.absolute()),
535
+ started_at=started_at,
536
+ original_identity=original_identity,
537
+ original_candidate_digest=original_candidate_digest,
538
+ provider_calls_per_attempt=provider_calls_per_attempt,
539
+ approvals=(approval,),
540
+ calibration_observation_ids=calibration_observation_ids,
541
+ )
542
+ except ValidationError as error:
543
+ raise RunStateError("invalid sampled run initialization") from error
544
+ runs_root = case_root / ".runs"
545
+ _reject_sampled_path(runs_root)
546
+ runs_root.mkdir(mode=0o700, exist_ok=True)
547
+ _private_directory(runs_root)
548
+ root = runs_root / state.run_id
549
+ root.mkdir(mode=0o700, exist_ok=False)
550
+ state_file = cls(root / "sampled-state.json")
551
+ with state_file.acquire_lock(store):
552
+ state_file._validate_authority(state, store)
553
+ state_file._write(state)
554
+ return state_file
555
+
556
+ @classmethod
557
+ def open(cls, source: Path) -> SampledRunStateFile:
558
+ _reject_sampled_path(source)
559
+ return cls(source / "sampled-state.json" if source.is_dir() else source)
560
+
561
+ @contextmanager
562
+ def acquire_lock(self, store: ObservationStore) -> Generator[None]:
563
+ """Own this run and sampled store from reconciliation through durable publication."""
564
+
565
+ if self._owner is not None:
566
+ raise RunStateBusyError("sampled run ownership is already active")
567
+ _reject_sampled_path(self.path.parent)
568
+ _private_directory(self.path.parent)
569
+ lock_path = self.path.with_name("run.lock")
570
+ _reject_sampled_path(lock_path)
571
+ try:
572
+ lock = private_file_lock(lock_path)
573
+ except UnsafePathError as error:
574
+ raise RunStateError("sampled run lock file is unsafe") from error
575
+ try:
576
+ lock.acquire()
577
+ except Timeout as error:
578
+ raise RunStateBusyError("another process owns sampled run progress") from error
579
+ try:
580
+ with store.acquire_sampling_lock():
581
+ self._owner = (os.getpid(), threading.get_ident())
582
+ self._store_path = store.path.absolute()
583
+ try:
584
+ yield
585
+ finally:
586
+ self._owner = None
587
+ self._store_path = None
588
+ finally:
589
+ lock.release()
590
+
591
+ def _require_lock(self, store: ObservationStore) -> None:
592
+ if (
593
+ self._owner != (os.getpid(), threading.get_ident())
594
+ or self._store_path != store.path.absolute()
595
+ ):
596
+ raise RunStateError("sampled progress requires owned run and observation-store locks")
597
+
598
+ def load(self, store: ObservationStore) -> SampledRunState:
599
+ """Read bounded private state and cross-check every cached fact against SQLite."""
600
+
601
+ state, _, _ = self._read_validated(store)
602
+ return state
603
+
604
+ def _read_validated(
605
+ self, store: ObservationStore
606
+ ) -> tuple[SampledRunState, tuple[Observation, ...], tuple[SamplingBlockEvidence, ...]]:
607
+ self._require_lock(store)
608
+ _reject_sampled_path(self.path)
609
+ _private_directory(self.path.parent)
610
+ _private_regular_file(self.path)
611
+ try:
612
+ raw = read_safe_bytes(
613
+ self.path, max_bytes=_MAX_RUN_STATE_BYTES, label="sampled run state"
614
+ )
615
+ _private_regular_file(self.path)
616
+ # Validate JSON directly to preserve strict tuple/datetime JSON decoding.
617
+ json.loads(raw, object_pairs_hook=_sampled_unique_keys)
618
+ state = SampledRunState.model_validate_json(raw)
619
+ except (
620
+ OSError,
621
+ UnicodeError,
622
+ json.JSONDecodeError,
623
+ RecursionError,
624
+ ValidationError,
625
+ CaseError,
626
+ ) as error:
627
+ raise RunStateError("invalid or unavailable sampled run state") from error
628
+ if Path(state.observation_store) != store.path.absolute():
629
+ raise RunStateError("sampled run observation-store identity does not match")
630
+ calibration, blocks = self._validate_authority(state, store)
631
+ return state, calibration, blocks
632
+
633
+ def evidence(self, store: ObservationStore) -> tuple[SamplingBlockEvidence, ...]:
634
+ _, _, blocks = self._read_validated(store)
635
+ return blocks
636
+
637
+ def snapshot(self, store: ObservationStore, *, now: datetime) -> _SampledRunSnapshot:
638
+ """Resolve fresh authority and its accounting once under the owned locks."""
639
+
640
+ state, calibration, blocks = self._read_validated(store)
641
+ return _SampledRunSnapshot(
642
+ state=state,
643
+ calibration=calibration,
644
+ blocks=blocks,
645
+ usage=self._usage(state, calibration, blocks, now=now),
646
+ )
647
+
648
+ @staticmethod
649
+ def _hydrate(progress: SamplingBlockProgress, store: ObservationStore) -> SamplingBlockEvidence:
650
+ attempts: list[SamplingAttempt] = []
651
+ try:
652
+ resolved = store.get_many(item.observation_id for item in progress.observations)
653
+ except (ObservationStoreError, ValueError) as error:
654
+ raise RunStateError(
655
+ "sampled progress references unavailable observation evidence"
656
+ ) from error
657
+ for reference, observation in zip(progress.observations, resolved, strict=True):
658
+ if (
659
+ observation.attempt_evidence.attempt_id != reference.attempt_id
660
+ or observation.attempt_evidence.attempt_digest != reference.attempt_digest
661
+ ):
662
+ raise RunStateError("sampled progress prepared-attempt identity does not match")
663
+ attempts.append(
664
+ SamplingAttempt(
665
+ schedule_position=reference.schedule_position,
666
+ observation=observation,
667
+ independence_evidence_digest=reference.independence_evidence_digest,
668
+ )
669
+ )
670
+ try:
671
+ return SamplingBlockEvidence(plan=progress.plan, attempts=tuple(attempts))
672
+ except ValidationError as error:
673
+ raise RunStateError("sampled progress has inconsistent block membership") from error
674
+
675
+ @staticmethod
676
+ def _validate_authority(
677
+ state: SampledRunState, store: ObservationStore
678
+ ) -> tuple[tuple[Observation, ...], tuple[SamplingBlockEvidence, ...]]:
679
+ observations = SampledRunStateFile._calibration(state, store)
680
+ blocks = tuple(SampledRunStateFile._hydrate(progress, store) for progress in state.blocks)
681
+ attempt_ids = [item.attempt_evidence.attempt_id for item in observations]
682
+ attempt_ids.extend(attempt_id for block in blocks for attempt_id in block.attempt_ids)
683
+ if len(set(attempt_ids)) != len(attempt_ids):
684
+ raise RunStateError("sampled run contains reused prepared attempts")
685
+ if state.pending is not None and state.pending.attempt_id in attempt_ids:
686
+ raise RunStateError("pending dispatch duplicates a published attempt")
687
+ if state.pending is not None and (
688
+ state.pending.schedule_position != SampledRunStateFile._next_position(blocks[-1])
689
+ ):
690
+ raise RunStateError("pending dispatch does not follow the unfinished frozen schedule")
691
+ checkpoint = state.checkpoint
692
+ if checkpoint is not None:
693
+ by_digest = {block.plan.digest: block for block in blocks}
694
+ if (
695
+ checkpoint.original_identity != state.original_identity
696
+ or checkpoint.original_candidate_digest != state.original_candidate_digest
697
+ or (state.blocks and checkpoint.reduction != state.blocks[0].plan.reduction)
698
+ or any(by_digest.get(block.plan.digest) != block for block in checkpoint.blocks)
699
+ ):
700
+ raise RunStateError("sampled checkpoint differs from authoritative block progress")
701
+ if state.selection is not None:
702
+ selection = state.selection
703
+ search = tuple(block for block in blocks if block.plan.role == "search")
704
+ if checkpoint is None or checkpoint.blocks != search:
705
+ raise RunStateError(
706
+ "selected checkpoint must include all authoritative search evidence"
707
+ )
708
+ if any(
709
+ selection.selected_at < block.plan.frozen_at
710
+ or any(selection.selected_at < a.observation.recorded_at for a in block.attempts)
711
+ for block in search
712
+ ) or any(selection.selected_at < o.recorded_at for o in observations):
713
+ raise RunStateError("selection must follow calibration and search evidence")
714
+ return observations, blocks
715
+
716
+ @staticmethod
717
+ def _calibration(state: SampledRunState, store: ObservationStore) -> tuple[Observation, ...]:
718
+ observations: list[Observation] = []
719
+ try:
720
+ resolved = store.get_many(state.calibration_observation_ids)
721
+ except (ObservationStoreError, ValueError) as error:
722
+ raise RunStateError("calibration accounting references unavailable evidence") from error
723
+ for observation in resolved:
724
+ if (
725
+ observation.role is not ObservationRole.CALIBRATION
726
+ or observation.identity != state.original_identity
727
+ or observation.candidate_digest != state.original_candidate_digest
728
+ or observation.arm is not ObservationArm.BASELINE
729
+ ):
730
+ raise RunStateError("calibration accounting does not match original run identity")
731
+ observations.append(observation)
732
+ return tuple(observations)
733
+
734
+ def usage(self, store: ObservationStore, *, now: datetime) -> SampledRunUsage:
735
+ state, calibration, blocks = self._read_validated(store)
736
+ return self._usage(state, calibration, blocks, now=now)
737
+
738
+ @staticmethod
739
+ def _usage(
740
+ state: SampledRunState,
741
+ calibration: tuple[Observation, ...],
742
+ blocks: tuple[SamplingBlockEvidence, ...],
743
+ *,
744
+ now: datetime,
745
+ ) -> SampledRunUsage:
746
+ now = _sampled_time(now)
747
+ if now < state.started_at:
748
+ raise RunStateError("run clock precedes its recorded start")
749
+ observations = list(calibration)
750
+ observations.extend(attempt.observation for block in blocks for attempt in block.attempts)
751
+ actual = possible = missing_provider = runner = missing_runner = tokens = missing_tokens = 0
752
+ for observation in observations:
753
+ evidence = observation.attempt_evidence
754
+ accounting = evidence.accounting
755
+ known = (
756
+ accounting.provider_calls_started
757
+ if accounting is not None
758
+ else int(evidence.provider_call_started)
759
+ )
760
+ actual += known
761
+ upper = max(
762
+ known,
763
+ evidence.provider_calls_possible,
764
+ state.provider_calls_per_attempt if evidence.provider_call_started else 0,
765
+ )
766
+ possible += upper - known
767
+ if (
768
+ accounting is None
769
+ and evidence.provider_acceptance_state is not ProviderAcceptanceState.NOT_STARTED
770
+ ):
771
+ missing_provider += 1
772
+ if accounting is None:
773
+ missing_runner += 1
774
+ else:
775
+ runner += accounting.elapsed_milliseconds
776
+ if evidence.usage is not None:
777
+ tokens += evidence.usage.input_tokens + evidence.usage.output_tokens
778
+ elif evidence.provider_acceptance_state is not ProviderAcceptanceState.NOT_STARTED:
779
+ missing_tokens += 1
780
+ pending = state.pending
781
+ return SampledRunUsage(
782
+ actual_provider_calls=actual,
783
+ possible_provider_calls=possible,
784
+ unknown_provider_attempts=missing_provider,
785
+ pending_provider_calls=0 if pending is None else pending.provider_calls,
786
+ runner_milliseconds=runner,
787
+ unknown_runner_attempts=missing_runner,
788
+ known_tokens=tokens,
789
+ unknown_token_attempts=missing_tokens,
790
+ wall_time_seconds=(now - state.started_at).total_seconds(),
791
+ pending_runner_seconds=0 if pending is None else pending.runner_seconds,
792
+ )
793
+
794
+ def admit_block(self, store: ObservationStore, plan: SamplingBlockPlan) -> SampledRunState:
795
+ """Record an externally admitted plan; whole-block budget scheduling is separate."""
796
+
797
+ current = self.load(store)
798
+ if current.pending is not None or (current.blocks and not current.blocks[-1].archived):
799
+ raise RunStateError("previous sampled block must be reconciled and archived")
800
+ return self._publish(
801
+ current, store, blocks=(*current.blocks, SamplingBlockProgress(plan=plan))
802
+ )
803
+
804
+ def continue_block(
805
+ self, store: ObservationStore, *, expected_plan_digest: str, now: datetime
806
+ ) -> SamplingBlockEvidence:
807
+ """Prove exact partial-block continuation; never clear or repeat an unresolved dispatch."""
808
+
809
+ current = self.load(store)
810
+ if current.pending is not None:
811
+ raise RunStateError("pending dispatch must be reconciled before continuation")
812
+ if not current.blocks or current.blocks[-1].archived:
813
+ raise RunStateError("no active sampled block is available")
814
+ block = self._hydrate(current.blocks[-1], store)
815
+ if block.plan.digest != expected_plan_digest:
816
+ raise RunStateError("continuation requires the exact frozen block identity")
817
+ now = _sampled_time(now)
818
+ if not block.plan.frozen_at <= now < block.plan.expires_at:
819
+ raise RunStateError("sampled block freshness has expired")
820
+ if block.attempts and now < block.attempts[-1].observation.recorded_at:
821
+ raise RunStateError("continuation clock precedes recorded sampling progress")
822
+ if block.complete or not block.eligible:
823
+ raise RunStateError("sampled block does not permit incomplete continuation")
824
+ usage = self.usage(store, now=now)
825
+ if usage.wall_time_seconds >= current.allowance.max_wall_time_seconds:
826
+ raise RunStateError("sampled run wall-time allowance is exhausted")
827
+ return block
828
+
829
+ def reserve_dispatch(
830
+ self, store: ObservationStore, reservation: SamplingDispatchReservation
831
+ ) -> SampledRunState:
832
+ block = self.continue_block(
833
+ store, expected_plan_digest=reservation.block_digest, now=reservation.reserved_at
834
+ )
835
+ position = self._next_position(block)
836
+ if reservation.schedule_position != position:
837
+ raise RunStateError("dispatch must follow the frozen unfinished schedule")
838
+ plan = block.plan
839
+ identity, digest = _sampling_arm_identity(plan, reservation.arm)
840
+ previous = store.list_compatible(
841
+ identity,
842
+ candidate_digest=digest,
843
+ role=ObservationRole(plan.role),
844
+ arm=ObservationArm(reservation.arm),
845
+ )
846
+ expected_attempt = 1 + max((item.attempt for item in previous), default=0)
847
+ if reservation.attempt != expected_attempt:
848
+ raise RunStateError("dispatch must pin the next authoritative arm-local attempt")
849
+ current = self.load(store)
850
+ usage = self.usage(store, now=reservation.reserved_at)
851
+ if (
852
+ usage.charged_provider_calls + reservation.provider_calls
853
+ > current.allowance.max_provider_calls
854
+ ):
855
+ raise RunStateError("dispatch exceeds the remaining provider-call allowance")
856
+ return self._publish(current, store, pending=reservation)
857
+
858
+ @staticmethod
859
+ def _next_position(block: SamplingBlockEvidence) -> int:
860
+ position = block.next_schedule_position
861
+ if position is None:
862
+ raise RunStateError("frozen sampling schedule is exhausted")
863
+ return position
864
+
865
+ def append_observation(
866
+ self,
867
+ store: ObservationStore,
868
+ observation: Observation,
869
+ *,
870
+ independence_evidence_digest: str | None = None,
871
+ ) -> SampledRunState:
872
+ """Validate the pinned dispatch, append once, then atomically publish its reference."""
873
+
874
+ current = self.load(store)
875
+ reference = self._reserved_reference(current, observation, independence_evidence_digest)
876
+ updated = self._with_reference(current, reference)
877
+ # Membership must validate before appending immutable evidence. Missing new record is
878
+ # supplied locally only for this check; cached checkpoint is invalidated by progress.
879
+ progress = updated.blocks[-1]
880
+ earlier = self._hydrate(current.blocks[-1], store)
881
+ try:
882
+ SamplingBlockEvidence(
883
+ plan=progress.plan,
884
+ attempts=(
885
+ *earlier.attempts,
886
+ SamplingAttempt(
887
+ schedule_position=reference.schedule_position,
888
+ observation=observation,
889
+ independence_evidence_digest=reference.independence_evidence_digest,
890
+ ),
891
+ ),
892
+ )
893
+ except ValidationError as error:
894
+ raise RunStateError("reserved observation is not valid frozen membership") from error
895
+ store.append(observation)
896
+ updated = self._refresh_checkpoint(updated, store)
897
+ self._validate_authority(updated, store)
898
+ self._write(updated)
899
+ return updated
900
+
901
+ def reconcile(
902
+ self, store: ObservationStore, *, independence_evidence_digest: str | None = None
903
+ ) -> SampledRunState:
904
+ """Publish an already appended exact dispatch, without performing another attempt."""
905
+
906
+ current = self.load(store)
907
+ reservation = current.pending
908
+ if reservation is None:
909
+ return current
910
+ plan = current.blocks[-1].plan
911
+ identity, digest = _sampling_arm_identity(plan, reservation.arm)
912
+ observation = store.get_attempt(
913
+ identity,
914
+ candidate_digest=digest,
915
+ role=ObservationRole(plan.role),
916
+ arm=ObservationArm(reservation.arm),
917
+ attempt=reservation.attempt,
918
+ )
919
+ if observation is None:
920
+ raise RunStateError(
921
+ "pending dispatch remains unresolved; automatic replay is forbidden"
922
+ )
923
+ reference = self._reserved_reference(current, observation, independence_evidence_digest)
924
+ updated = self._with_reference(current, reference)
925
+ updated = self._refresh_checkpoint(updated, store)
926
+ self._validate_authority(updated, store)
927
+ self._write(updated)
928
+ return updated
929
+
930
+ @staticmethod
931
+ def _reserved_reference(
932
+ current: SampledRunState, observation: Observation, independence: str | None
933
+ ) -> SamplingObservationReference:
934
+ reservation = current.pending
935
+ if reservation is None:
936
+ raise RunStateError("observation publication requires a persisted dispatch reservation")
937
+ plan = current.blocks[-1].plan
938
+ identity, digest = _sampling_arm_identity(plan, reservation.arm)
939
+ if (
940
+ observation.identity != identity
941
+ or observation.candidate_digest != digest
942
+ or observation.role is not ObservationRole(plan.role)
943
+ or observation.arm is not ObservationArm(reservation.arm)
944
+ or observation.attempt != reservation.attempt
945
+ or observation.attempt_evidence.attempt_id != reservation.attempt_id
946
+ or observation.attempt_evidence.attempt_digest != reservation.attempt_digest
947
+ or observation.recorded_at < reservation.reserved_at
948
+ ):
949
+ raise RunStateError("observation does not match the exact reserved prepared attempt")
950
+ try:
951
+ return SamplingObservationReference(
952
+ observation_id=observation.observation_id,
953
+ attempt_id=reservation.attempt_id,
954
+ attempt_digest=reservation.attempt_digest,
955
+ schedule_position=reservation.schedule_position,
956
+ independence_evidence_digest=independence,
957
+ )
958
+ except ValidationError as error:
959
+ raise RunStateError("invalid sampling observation reference") from error
960
+
961
+ @staticmethod
962
+ def _with_reference(
963
+ current: SampledRunState, reference: SamplingObservationReference
964
+ ) -> SampledRunState:
965
+ progress = current.blocks[-1]
966
+ updated = SamplingBlockProgress(
967
+ plan=progress.plan, observations=(*progress.observations, reference)
968
+ )
969
+ return _sampled_replace(current, blocks=(*current.blocks[:-1], updated), pending=None)
970
+
971
+ def _refresh_checkpoint(
972
+ self, state: SampledRunState, store: ObservationStore
973
+ ) -> SampledRunState:
974
+ checkpoint = state.checkpoint
975
+ if checkpoint is None or checkpoint.active_block is None:
976
+ return state
977
+ progress = state.blocks[-1]
978
+ if checkpoint.active_block.plan.digest != progress.plan.digest:
979
+ return state
980
+ evidence = self._hydrate(progress, store)
981
+ # Search classification belongs to the reducer. A completed block remains durable in
982
+ # progress while the cached checkpoint retains its previously selected trials.
983
+ checkpoint = SampledReductionCheckpoint.model_validate(
984
+ {**checkpoint.model_dump(), "active_block": None if evidence.complete else evidence}
985
+ )
986
+ return _sampled_replace(state, checkpoint=checkpoint)
987
+
988
+ def archive_block(self, store: ObservationStore) -> SampledRunState:
989
+ current = self.load(store)
990
+ if current.pending is not None or not current.blocks or current.blocks[-1].archived:
991
+ raise RunStateError("only a reconciled active block can be archived")
992
+ block = current.blocks[-1]
993
+ archived = SamplingBlockProgress(
994
+ plan=block.plan, observations=block.observations, archived=True
995
+ )
996
+ return self._publish(current, store, blocks=(*current.blocks[:-1], archived))
997
+
998
+ def record_calibration(
999
+ self, store: ObservationStore, observation_ids: tuple[str, ...]
1000
+ ) -> SampledRunState:
1001
+ """Attach the exact calibration prefix, charging compatible prior observations too.
1002
+
1003
+ Calibration may precede the administrative run. It never supplies observations
1004
+ to a search or confirmation arm. Once search starts, its references are frozen.
1005
+ """
1006
+
1007
+ current = self.load(store)
1008
+ previous = current.calibration_observation_ids
1009
+ if observation_ids[: len(previous)] != previous:
1010
+ raise RunStateError("calibration continuation must preserve its recorded prefix")
1011
+ if current.checkpoint is not None or current.blocks or current.selection is not None:
1012
+ if observation_ids != previous:
1013
+ raise RunStateError("calibration references cannot change after search starts")
1014
+ return current
1015
+ return self._publish(current, store, calibration_observation_ids=observation_ids)
1016
+
1017
+ def extend_budget(
1018
+ self, store: ObservationStore, approval: SamplingBudgetApproval
1019
+ ) -> SampledRunState:
1020
+ current = self.load(store)
1021
+ return self._publish(current, store, approvals=(*current.approvals, approval))
1022
+
1023
+ def save_checkpoint(
1024
+ self, store: ObservationStore, checkpoint: SampledReductionCheckpoint
1025
+ ) -> SampledRunState:
1026
+ current = self.load(store)
1027
+ if current.pending is not None:
1028
+ raise RunStateError("checkpoint publication requires dispatch reconciliation")
1029
+ return self._publish(current, store, checkpoint=checkpoint)
1030
+
1031
+ def select_candidate(
1032
+ self, store: ObservationStore, selection: SampledFinalSelection
1033
+ ) -> SampledRunState:
1034
+ """Freeze selection and close search atomically, including a safe partial last block."""
1035
+
1036
+ current = self.load(store)
1037
+ if current.selection is not None or current.pending is not None:
1038
+ raise RunStateError("selection requires reconciled search and cannot be repeated")
1039
+ if any(b.plan.role != "search" for b in current.blocks):
1040
+ raise RunStateError("selection must precede every confirmation dispatch")
1041
+ blocks = current.blocks
1042
+ if blocks and not blocks[-1].archived:
1043
+ blocks = (*blocks[:-1], blocks[-1].model_copy(update={"archived": True}))
1044
+ return self._publish(current, store, blocks=blocks, selection=selection)
1045
+
1046
+ def _publish(
1047
+ self, current: SampledRunState, store: ObservationStore, **changes: object
1048
+ ) -> SampledRunState:
1049
+ updated = _sampled_replace(current, **changes)
1050
+ self._validate_authority(updated, store)
1051
+ self._write(updated)
1052
+ return updated
1053
+
1054
+ def _write(self, state: SampledRunState) -> None:
1055
+ if self._owner != (os.getpid(), threading.get_ident()):
1056
+ raise RunStateError("sampled state publication requires owned locks")
1057
+ _reject_sampled_path(self.path)
1058
+ _private_directory(self.path.parent)
1059
+ if os.path.lexists(self.path):
1060
+ _private_regular_file(self.path)
1061
+ try:
1062
+ state = SampledRunState.model_validate_json(state.model_dump_json())
1063
+ except ValidationError as error:
1064
+ raise RunStateError("invalid sampled state publication") from error
1065
+ rendered = state.model_dump_json(indent=2).encode("utf-8") + b"\n"
1066
+ if len(rendered) > _MAX_RUN_STATE_BYTES:
1067
+ raise RunStateError("sampled run state exceeds its byte limit")
1068
+ descriptor, temporary_name = tempfile.mkstemp(
1069
+ prefix=".sampled-state-", dir=self.path.parent
1070
+ )
1071
+ temporary = Path(temporary_name)
1072
+ try:
1073
+ with os.fdopen(descriptor, "wb") as stream:
1074
+ stream.write(rendered)
1075
+ stream.flush()
1076
+ os.fsync(stream.fileno())
1077
+ temporary.replace(self.path)
1078
+ _sync_directory(self.path.parent)
1079
+ finally:
1080
+ temporary.unlink(missing_ok=True)
1081
+
1082
+
1083
+ def _sampled_replace(state: SampledRunState, **changes: object) -> SampledRunState:
1084
+ try:
1085
+ return SampledRunState.model_validate({**state.model_dump(), **changes})
1086
+ except ValidationError as error:
1087
+ raise RunStateError("invalid sampled progress transition") from error
1088
+
1089
+
1090
+ def _sampling_arm_identity(
1091
+ plan: SamplingBlockPlan, arm: SamplingArm
1092
+ ) -> tuple[ObservationIdentity, str]:
1093
+ if arm == "baseline":
1094
+ return plan.original_identity, plan.original_candidate_digest
1095
+ return plan.candidate_identity, plan.candidate_digest
1096
+
1097
+
1098
+ def _sampled_unique_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
1099
+ result: dict[str, object] = {}
1100
+ for key, value in pairs:
1101
+ if key in result:
1102
+ raise RunStateError("sampled run state contains duplicate JSON keys")
1103
+ result[key] = value
1104
+ return result
1105
+
1106
+
1107
+ def _reject_sampled_path(path: Path) -> None:
1108
+ try:
1109
+ _reject_unsafe_path(path)
1110
+ except RunStateError as error:
1111
+ raise RunStateError("sampled run-state path is unsafe") from error
1112
+
1113
+
1114
+ def _private_directory(path: Path) -> None:
1115
+ try:
1116
+ metadata = path.lstat()
1117
+ except OSError as error:
1118
+ raise RunStateError("sampled run-state directory is unavailable") from error
1119
+ if not stat.S_ISDIR(metadata.st_mode) or (
1120
+ os.name == "posix" and stat.S_IMODE(metadata.st_mode) != 0o700
1121
+ ):
1122
+ raise RunStateError("sampled run-state directory must be private and regular")
1123
+
1124
+
1125
+ def _private_regular_file(path: Path) -> None:
1126
+ try:
1127
+ metadata = path.lstat()
1128
+ except OSError as error:
1129
+ raise RunStateError("sampled run-state file is unavailable") from error
1130
+ if (
1131
+ not stat.S_ISREG(metadata.st_mode)
1132
+ or metadata.st_nlink != 1
1133
+ or (os.name == "posix" and stat.S_IMODE(metadata.st_mode) != 0o600)
1134
+ ):
1135
+ raise RunStateError("sampled run-state file must be regular with mode 0600")