oc_chat
Loading...
Searching...
No Matches
errors.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/errors.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_seam_and_locks.py"
6"""
7Typed registry errors.
8
9Purpose
10-------
11Expose precise exception types for the M11 registry mutating seam and
12lock primitives.
13
14Responsibilities
15----------------
16- Define typed pre-commit validation failures.
17- Define lock contention and ownership failures with useful attributes.
18- Keep error classes independent from SQLite implementation details.
19
20Diagnostics
21-----------
22Domain: OC_CHAT.MAIN
23Levels:
24 L2 - lifecycle events
25 L3 - semantic details
26 L4 - deep tracing
27
28Contracts
29---------
30- Pre-commit failures are raised before the backend commits.
31- Lock errors carry enough data for callers to render operator messages.
32
33Dependencies
34------------
35- oc_diagnostics (structured debug logging via _dbg)
36- dataclasses (dataclass decorator for the error types' attribute fields)
37"""
38from __future__ import annotations
39
40from dataclasses import dataclass
41
42from oc_diagnostics import _dbg
43
44
45_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
46
47
48class RegistryError(RuntimeError):
49 """Base class for registry API errors."""
50
51
52@dataclass
54 """Raised when supplied actor_type disagrees with agents.slot_kind."""
55
56 actor_slot: str
57 expected_actor_type: str
58 supplied_actor_type: str
59
60 def __post_init__(self) -> None:
61 super().__init__(
62 "actor_type mismatch for "
63 f"{self.actor_slot!r}: expected {self.expected_actor_type!r}, "
64 f"got {self.supplied_actor_type!r}"
65 )
66
67
68@dataclass
70 """Raised when a slot kind is not allowed to author an operation."""
71
72 operation_type: str
73 actor_slot: str
74 slot_kind: str
75 allowed_slot_kinds: tuple[str, ...]
76
77 def __post_init__(self) -> None:
78 super().__init__(
79 f"{self.actor_slot!r} ({self.slot_kind}) cannot author "
80 f"{self.operation_type!r}; allowed slot kinds: "
81 f"{', '.join(self.allowed_slot_kinds) or 'none'}"
82 )
83
84
85@dataclass
87 """Raised when HMAC verification fails before the main write transaction."""
88
89 attempted_slot: str | None
90 hmac_key_id: str | None
91 command: str
92 failure_reason: str
93
94 def __post_init__(self) -> None:
95 super().__init__(
96 f"HMAC verification failed for {self.command!r}: "
97 f"{self.failure_reason}"
98 )
99
100
101@dataclass
103 """Raised when an operation violates the orchestrator handover state."""
104
105 state: str
106 actor_slot: str
107 operation_type: str
108 handover_id: int | None = None
109
110 def __post_init__(self) -> None:
111 suffix = "" if self.handover_id is None else f" ({self.handover_id})"
112 super().__init__(
113 f"handover state {self.state!r}{suffix} rejects "
114 f"{self.operation_type!r} from {self.actor_slot!r}"
115 )
116
117
118@dataclass
120 """Raised when a slot already has a pending key rotation."""
121
122 slot: str
123
124 def __post_init__(self) -> None:
125 super().__init__(f"credential rotation is already in flight for {self.slot!r}")
126
127
128@dataclass
130 """Raised when a named lock is already held."""
131
132 name: str
133 owner_slot: str
134 expires_at: str | None
135
136 def __post_init__(self) -> None:
137 super().__init__(
138 f"lock {self.name!r} is held by {self.owner_slot!r} "
139 f"until {self.expires_at!r}"
140 )
141
142
143@dataclass
145 """Raised when a caller tries to release or renew another slot's lock."""
146
147 name: str
148 owner_slot: str
149 actual_owner_slot: str | None
150
151 def __post_init__(self) -> None:
152 super().__init__(
153 f"lock {self.name!r} is owned by {self.actual_owner_slot!r}, "
154 f"not {self.owner_slot!r}"
155 )
156
157
158@dataclass
160 """Raised when payload['version'] and operation payload_version differ."""
161
162 operation_type: str
163 payload_version: int
164 supplied_version: object
165
166 def __post_init__(self) -> None:
167 super().__init__(
168 f"{self.operation_type!r} payload version mismatch: "
169 f"expected {self.payload_version!r}, got {self.supplied_version!r}"
170 )
171
172
173@dataclass
175 """Raised for the minimal payload-shape checks owned by M11.1b."""
176
177 operation_type: str
178 message: str
179
180 def __post_init__(self) -> None:
181 super().__init__(f"{self.operation_type!r}: {self.message}")
182
183
184@dataclass
186 """Raised when parent_operation_id would create or expose a cycle."""
187
188 parent_operation_id: int
189 operation_uuid: str
190
191 def __post_init__(self) -> None:
192 super().__init__(
193 "parent_operation_id cycle detected for "
194 f"{self.operation_uuid!r} via {self.parent_operation_id!r}"
195 )
196
197
198@dataclass
200 """Raised when actor_slot is not present in agents."""
201
202 actor_slot: str
203
204 def __post_init__(self) -> None:
205 super().__init__(f"unknown actor_slot: {self.actor_slot!r}")
206
207
208@dataclass
210 """Raised when a round.* operation is illegal for the round's status."""
211
212 round_no: int
213 status: str
214 operation_type: str
215
216 def __post_init__(self) -> None:
217 super().__init__(
218 f"round {self.round_no} in status {self.status!r} rejects "
219 f"{self.operation_type!r}"
220 )
221
222
223@dataclass
225 """Raised when a round-close invariant is violated (torn state, gaps)."""
226
227 round_no: int
228 message: str
229
230 def __post_init__(self) -> None:
231 super().__init__(f"round {self.round_no} invariant violated: {self.message}")
232
233
234@dataclass
236 """Raised when a pre-open or pre-resume preflight check fails."""
237
238 round_no: int | None
239 reasons: tuple[str, ...]
240
241 def __post_init__(self) -> None:
242 joined = "; ".join(self.reasons) or "unspecified"
243 scope = "round-close" if self.round_no is None else f"round {self.round_no}"
244 super().__init__(f"{scope} preflight failed: {joined}")
245
246
247@dataclass
249 """Raised when a plan-branch merge into staging hits a conflict."""
250
251 round_no: int
252 plan_id: str
253 conflicted_paths: tuple[str, ...]
254
255 def __post_init__(self) -> None:
256 paths = ", ".join(self.conflicted_paths) or "unknown"
257 super().__init__(
258 f"merge of {self.plan_id!r} into round {self.round_no} staging "
259 f"conflicted on: {paths}"
260 )
261
262
263@dataclass
265 """Raised when a conflict is outside the auto-resolve allowlist."""
266
267 round_no: int
268 plan_id: str
269 conflicted_paths: tuple[str, ...]
270
271 def __post_init__(self) -> None:
272 paths = ", ".join(self.conflicted_paths) or "unknown"
273 super().__init__(
274 f"manual merge required for {self.plan_id!r} in round {self.round_no}; "
275 f"non-allowlisted conflicts: {paths}"
276 )
277
278
279@dataclass
281 """Raised when the staging branch HEAD no longer matches recorded state."""
282
283 round_no: int
284 expected_sha: str | None
285 actual_sha: str | None
286
287 def __post_init__(self) -> None:
288 super().__init__(
289 f"round {self.round_no} staging branch diverged: expected "
290 f"{self.expected_sha!r}, found {self.actual_sha!r}; rollback required"
291 )