oc_chat
Loading...
Searching...
No Matches
readers.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/readers.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_readers.py"
6"""
7Read-only registry projection helpers.
8
9Purpose
10-------
11Expose typed reader libraries for queue history, plan lifecycle, plan
12inspection, registry status, and actor identity.
13
14Responsibilities
15----------------
16- Read current-state and operation-log projections without mutating state.
17- Decode canonical JSON payloads into typed dataclass records.
18- Keep SQL limited to SELECT statements.
19
20Diagnostics
21-----------
22Domain: OC_CHAT.MAIN
23Levels:
24 L2 - lifecycle events
25 L3 - semantic details
26 L4 - deep tracing
27
28Contracts
29---------
30- Reader functions never call ``execute_domain_command``.
31- Missing plan rows return ``None`` for explicit plan views.
32- Unknown actor lookups raise ``UnknownActorError``.
33
34Dependencies
35------------
36- json (payload decoding)
37- oc_diagnostics (structured debug logging via _dbg)
38- oc_chat.registry.dataclasses (typed read models)
39- oc_chat.registry.errors (typed unknown-actor error)
40- oc_chat.registry.api (Registry facade whose backend connection the readers query)
41"""
42from __future__ import annotations
43
44import json
45from typing import Iterable, Mapping
46
47from oc_diagnostics import _dbg
48
49from oc_chat.registry.api import Registry
51 ActorView,
52 CurrentStatus,
53 PlanAssignmentEvent,
54 PlanStatusEvent,
55 PlanTimeline,
56 PlanView,
57 QueueEvent,
58)
59from oc_chat.registry.errors import UnknownActorError
60
61
62_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
63
64QUEUE_HISTORY_TYPES = {
65 "queue.enqueue",
66 "queue.move",
67 "queue.dequeue",
68 "queue.complete",
69 "plan.assign",
70 "plan.set_status",
71 "migrate.import.queue_entry",
72}
73
74
75def queue_history(registry: Registry, plan_id: str) -> list[QueueEvent]:
76 """Return typed queue history events for ``plan_id``."""
77
78 placeholders = ", ".join("?" for _ in QUEUE_HISTORY_TYPES)
79 sql = (
80 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
81 "SELECT ts, operation_type, args_json, state_version, actor_slot, "
82 "actor_type, related_plan_id, related_round FROM operation_log "
83 f"WHERE related_plan_id = ? AND operation_type IN ({placeholders}) "
84 "ORDER BY state_version"
85 )
86 with _connect(registry) as conn:
87 rows = conn.execute(sql, (plan_id, *sorted(QUEUE_HISTORY_TYPES))).fetchall()
88 return [
90 ts=str(row["ts"]),
91 operation_type=str(row["operation_type"]),
92 payload=_payload(row["args_json"]),
93 state_version=int(row["state_version"]),
94 actor_slot=str(row["actor_slot"]),
95 actor_type=str(row["actor_type"]),
96 related_plan_id=row["related_plan_id"],
97 related_round=row["related_round"],
98 is_migration_synthetic=str(row["operation_type"]).startswith("migrate."),
99 )
100 for row in rows
101 ]
102
103
104def plan_lifecycle(registry: Registry, plan_id: str) -> PlanTimeline:
105 """Return typed status and assignment events for ``plan_id``."""
106
107 with _connect(registry) as conn:
108 status_rows = conn.execute(
109 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
110 "SELECT plan_id, from_status, to_status, actor_slot, source, "
111 "created_at, note FROM plan_status_events "
112 "WHERE plan_id = ? ORDER BY created_at, id",
113 (plan_id,),
114 ).fetchall()
115 op_rows = conn.execute(
116 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
117 "SELECT ts, operation_type, args_json, state_version FROM operation_log "
118 "WHERE related_plan_id = ? AND operation_type = 'plan.assign' "
119 "ORDER BY state_version",
120 (plan_id,),
121 ).fetchall()
122
123 status_events = [
125 ts=str(row["created_at"]),
126 plan_id=str(row["plan_id"]),
127 from_status=row["from_status"],
128 to_status=str(row["to_status"]),
129 actor_slot=row["actor_slot"],
130 source=row["source"],
131 note=row["note"],
132 )
133 for row in status_rows
134 ]
135 assignment_events: list[PlanAssignmentEvent] = []
136 for row in op_rows:
137 payload = _payload(row["args_json"])
138 if row["operation_type"] == "plan.assign":
139 assignment_events.append(
141 ts=str(row["ts"]),
142 plan_id=str(payload["plan_id"]),
143 from_slot=_optional_str(payload.get("from_slot")),
144 to_slot=_optional_str(payload.get("to_slot")),
145 queue_position=_optional_int(payload.get("queue_position")),
146 state_version=int(row["state_version"]),
147 )
148 )
149 return PlanTimeline(
150 plan_id=plan_id,
151 status_events=tuple(status_events),
152 assignment_events=tuple(assignment_events),
153 )
154
155
156def show_plan(registry: Registry, plan_id: str) -> PlanView | None:
157 """Return the current projected plan row, or ``None`` if absent."""
158
159 with _connect(registry) as conn:
160 row = conn.execute(
161 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
162 "SELECT plan_id, plan_number, title, status, assigned_slot, "
163 "queue_position, branch, worktree, body_path, body_sha256 "
164 "FROM plans WHERE plan_id = ?",
165 (plan_id,),
166 ).fetchone()
167 if row is None:
168 return None
169 return PlanView(
170 plan_id=str(row["plan_id"]),
171 plan_number=int(row["plan_number"]),
172 title=str(row["title"]),
173 status=str(row["status"]),
174 assigned_slot=row["assigned_slot"],
175 queue_position=row["queue_position"],
176 branch=row["branch"],
177 worktree=row["worktree"],
178 body_path=str(row["body_path"]),
179 body_sha256=row["body_sha256"],
180 )
181
182
183def current_status(registry: Registry) -> CurrentStatus:
184 """Return compact registry status counters and active runtime rows."""
185
186 with _connect(registry) as conn:
187 plan_counts = {
188 str(row["status"]): int(row["count"])
189 for row in conn.execute(
190 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
191 "SELECT status, COUNT(*) AS count FROM plans GROUP BY status"
192 ).fetchall()
193 }
194 open_rounds = tuple(
195 _row_dict(row)
196 for row in conn.execute(
197 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
198 "SELECT round_no, status, cleanup_status, orchestrator_slot "
199 "FROM rounds WHERE status IN ('open', 'closing', 'committed') "
200 "ORDER BY round_no"
201 ).fetchall()
202 )
203 active_leases = tuple(
204 _row_dict(row)
205 for row in conn.execute(
206 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
207 "SELECT id, holder_slot, lease_expires_at, last_heartbeat_at "
208 "FROM leases WHERE holder_slot IS NOT NULL ORDER BY id"
209 ).fetchall()
210 )
211 state_row = conn.execute(
212 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
213 "SELECT id, orchestrator_slot, handover_status, ready_to_handover, "
214 "updated_at, updated_by FROM orchestration_state WHERE id = 1"
215 ).fetchone()
216 return CurrentStatus(
217 plan_counts=plan_counts,
218 open_rounds=open_rounds,
219 active_leases=active_leases,
220 orchestration_state=None if state_row is None else _row_dict(state_row),
221 )
222
223
224def whoami(registry: Registry, actor_slot: str) -> ActorView:
225 """Return current actor metadata for ``actor_slot``."""
226
227 with _connect(registry) as conn:
228 row = conn.execute(
229 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection queries are not secrets"
230 "SELECT slot, slot_kind, identity_kind, model_class, "
231 "orchestrator_eligible, active, provider, profile "
232 "FROM agents WHERE slot = ?",
233 (actor_slot,),
234 ).fetchone()
235 if row is None:
236 raise UnknownActorError(actor_slot)
237 return ActorView(
238 slot=str(row["slot"]),
239 slot_kind=str(row["slot_kind"]),
240 identity_kind=str(row["identity_kind"]),
241 model_class=str(row["model_class"]),
242 orchestrator_eligible=bool(row["orchestrator_eligible"]),
243 active=bool(row["active"]),
244 provider=row["provider"],
245 profile=row["profile"],
246 )
247
248
249def _connect(registry: Registry) -> object:
250 connect = getattr(registry.backend, "connect", None)
251 if connect is None:
252 raise TypeError("registry backend does not expose read connections")
253 return connect()
254
255
256def _payload(value: object) -> Mapping[str, object]:
257 if value is None:
258 return {}
259 payload = json.loads(str(value))
260 if not isinstance(payload, dict):
261 return {}
262 return payload
263
264
265def _row_dict(row: object) -> dict[str, object]:
266 keys: Iterable[str] = row.keys()
267 return {key: row[key] for key in keys}
268
269
270def _optional_str(value: object) -> str | None:
271 return None if value is None else str(value)
272
273
274def _optional_int(value: object) -> int | None:
275 return None if value is None else int(value)
PlanView|None show_plan(Registry registry, str plan_id)
Definition readers.py:156
PlanTimeline plan_lifecycle(Registry registry, str plan_id)
Definition readers.py:104
object _connect(Registry registry)
Definition readers.py:249
Mapping[str, object] _payload(object value)
Definition readers.py:256
CurrentStatus current_status(Registry registry)
Definition readers.py:183
list[QueueEvent] queue_history(Registry registry, str plan_id)
Definition readers.py:75
ActorView whoami(Registry registry, str actor_slot)
Definition readers.py:224
str|None _optional_str(object value)
Definition readers.py:270
int|None _optional_int(object value)
Definition readers.py:274
dict[str, object] _row_dict(object row)
Definition readers.py:265