oc_chat
Loading...
Searching...
No Matches
sqlite.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/backends/sqlite.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_storage_foundation.py"
6"""
7SQLite backend for the orchestration registry.
8
9Purpose
10-------
11Own all SQL used by the M11 registry storage foundation, including
12canonical schema loading, connection PRAGMAs, init-time seeding, and
13backend maintenance operations.
14
15Responsibilities
16----------------
17- Load the canonical registry schema from the design-lock SQL file.
18- Apply required SQLite PRAGMAs on every connection.
19- Seed singleton rows for ``python -m oc_chat.registry init``.
20- Execute the single mutating seam and insert one operation-log row.
21- Own lock acquisition, release, renewal, and expiry sweep SQL.
22- Provide integrity check, backup/restore, and vacuum hooks.
23
24Diagnostics
25-----------
26Domain: OC_CHAT.MAIN
27Levels:
28 L2 - lifecycle events
29 L3 - semantic details
30 L4 - deep tracing
31
32Contracts
33---------
34- SQL remains in this module; higher registry layers call backend methods.
35- Init is all-or-nothing: schema load and seed inserts share one transaction.
36- No SQLite ``datetime('now')`` defaults are used; timestamps come from Clock.
37
38Dependencies
39------------
40- oc_diagnostics (structured debug logging via _dbg)
41- sqlite3 (registry storage engine)
42- oc_chat.registry.clock (injected time)
43- oc_chat.registry.backends.base (StorageBackend contract, capability error and return types)
44- oc_chat.registry.canonical (canonical_json for operation payloads and lock metadata)
45- oc_chat.registry.errors (registry error types raised by the mutating seam and lock SQL)
46- oc_chat.registry.migrations.v0_3_1_auth_events (auth events schema migration and version row)
47- oc_chat.registry.migrations.v0_3_2_roadmap_source_fields (roadmap source fields schema migration and version row)
48- oc_chat.registry.migrations.v0_3_roadmap (roadmap schema SQL, version row and upgrade checks)
49- oc_chat.registry.store_scope (assert_live_store guard before each domain command)
50"""
51from __future__ import annotations
52
53from contextlib import contextmanager
54from dataclasses import dataclass
55import json
56from pathlib import Path
57import shutil
58import sqlite3
59from typing import Iterator, Mapping
60import uuid
61
62from oc_diagnostics import _dbg
63
65 BackendCapabilityError,
66 LockHandle,
67 OperationLogRow,
68 StorageBackend,
69)
70from oc_chat.registry.canonical import canonical_json
71from oc_chat.registry.clock import Clock, RealClock, wall_plus_seconds
72from oc_chat.registry.errors import (
73 ActorTypeMismatchError,
74 HandoverStateError,
75 LockHeldError,
76 LockOwnerError,
77 ParentCycleError,
78 PayloadSchemaViolationError,
79 PayloadVersionMismatchError,
80 RoundStateError,
81 UnknownActorError,
82)
84 v0_3_1_auth_events,
85 v0_3_2_roadmap_source_fields,
86 v0_3_roadmap,
87)
88from oc_chat.registry import store_scope as _store_scope
89
90_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
91
92SCHEMA_VERSION = 1
93SCHEMA_DESCRIPTION = "baseline REGISTRY_SCHEMA_V0_2_3"
94_PRAGMA_PREFIX = "PRAGMA "
95_ACTOR_TYPES_BY_SLOT_KIND = {
96 "operator": "human_operator",
97 "orchestrator": "orchestrator_agent",
98 "worker": "worker_agent",
99}
100_MAX_PARENT_DEPTH = 1000
101
102
103@dataclass(frozen=True)
105 """Values substituted into the canonical seed template at init time."""
106
107 workspace_id: str
108 registry_id: str
109 workspace_root: str
110 git_common_dir: str
111 registry_path: str
112 checkpoint_path: str
113 operator_slot: str
114 hmac_key_id: str
115 secret_fingerprint: str
116 app_version: str
117
118
120 """SQLite implementation of the registry storage backend."""
121
123 self,
124 path: str | Path,
125 clock: Clock | None = None,
126 *,
127 check_capabilities: bool = True,
128 ) -> None:
129 self.path = Path(path)
130 self.clock = RealClock() if clock is None else clock
131 if not self._is_memory:
132 self.path.parent.mkdir(parents=True, exist_ok=True)
133 if check_capabilities:
134 super().__init__()
136
137 @property
138 def _is_memory(self) -> bool:
139 return str(self.path) == ":memory:"
140
141 def connect(self) -> sqlite3.Connection:
142 """Open a connection with all required PRAGMAs applied."""
143
144 conn = sqlite3.connect(str(self.path))
145 conn.row_factory = sqlite3.Row
147 return conn
148
149 def apply_schema(self) -> None:
150 """Apply the canonical schema baseline atomically."""
151
152 with self.connect() as conn:
153 self._run_schema_script(conn, commit=True)
154
155 def initialize(self, seed: RegistrySeed) -> None:
156 """Apply schema and seed singleton rows in one transaction."""
157
158 # NOT `with self.connect()`: sqlite3's connection context manager is a
159 # transaction manager and never closes — the leaked handle held a WAL
160 # lock that made regenerate's os.replace swap fail under coverage.
161 conn = self.connect()
162 try:
163 if self._table_count(conn) > 0:
164 raise ValueError(f"registry database is not empty: {self.path}")
165 self._run_schema_script(conn, commit=False)
166 try:
167 self._seed_initial_rows(conn, seed)
168 conn.execute("COMMIT")
169 except Exception:
170 conn.execute("ROLLBACK")
171 raise
172 finally:
173 conn.close()
174
175 @contextmanager
176 def transaction(self, mode: str = "immediate") -> Iterator[sqlite3.Connection]:
177 conn = self.connect()
178 try:
179 begin = "BEGIN IMMEDIATE" if mode in {"immediate", "write"} else "BEGIN"
180 conn.execute(begin)
181 try:
182 yield conn
183 conn.execute("COMMIT")
184 except Exception:
185 conn.execute("ROLLBACK")
186 raise
187 finally:
188 conn.close()
189
191 self,
192 name: str,
193 filters: Mapping[str, object] | None = None,
194 ) -> object:
195 raise NotImplementedError("read models are implemented in a later M11 plan")
196
198 self,
199 operation_type: str,
200 actor_slot: str,
201 payload: Mapping[str, object],
202 *,
203 actor_type: str,
204 payload_version: int = 1,
205 command: str | None = None,
206 parent_operation_id: int | None = None,
207 related_plan_id: str | None = None,
208 related_round: int | None = None,
209 operation_uuid: str | None = None,
210 hmac_key_id: str | None = None,
211 system_actor: bool = False,
212 ) -> OperationLogRow:
213 with self.transaction("immediate") as conn:
214 _store_scope.assert_live_store(conn)
216 conn,
217 operation_type,
218 actor_slot,
219 payload,
220 actor_type=actor_type,
221 payload_version=payload_version,
222 command=command,
223 parent_operation_id=parent_operation_id,
224 related_plan_id=related_plan_id,
225 related_round=related_round,
226 operation_uuid=operation_uuid,
227 hmac_key_id=hmac_key_id,
228 system_actor=system_actor,
229 )
230
232 self,
233 projection: Mapping[str, object],
234 *,
235 actor_slot: str,
236 actor_type: str,
237 ) -> list[OperationLogRow]:
238 """Apply a legacy migration projection and emit seam audit rows."""
239
240 emitted: list[OperationLogRow] = []
241 with self.transaction("immediate") as conn:
242 for agent in _mapping_list(projection.get("agents")):
243 self._upsert_migration_agent(conn, agent)
244 emitted.append(
246 conn,
247 "migrate.import.agent",
248 actor_slot,
249 _required_mapping(agent, "payload"),
250 actor_type=actor_type,
251 related_plan_id=None,
252 )
253 )
254
255 for round_row in _mapping_list(projection.get("rounds")):
256 self._upsert_migration_round(conn, round_row)
257
258 import_rows_by_plan: dict[str, OperationLogRow] = {}
259 for plan in _mapping_list(projection.get("plans")):
260 self._upsert_migration_plan(conn, plan)
261 self._insert_migration_status_event(conn, plan, actor_slot)
262 plan_id = str(plan["plan_id"])
263 import_row = self._execute_domain_command_in_conn(
264 conn,
265 "migrate.import.plan",
266 actor_slot,
267 _required_mapping(plan, "payload"),
268 actor_type=actor_type,
269 related_plan_id=plan_id,
270 related_round=_optional_int(plan.get("closed_round")),
271 )
272 import_rows_by_plan[plan_id] = import_row
273 emitted.append(import_row)
274
275 backfill_payload = plan.get("backfill_payload")
276 if isinstance(backfill_payload, Mapping):
277 emitted.append(
279 conn,
280 "migrate.backfill_completed",
281 actor_slot,
282 backfill_payload,
283 actor_type=actor_type,
284 parent_operation_id=import_row.id,
285 related_plan_id=plan_id,
286 )
287 )
288
289 blocked_payload = plan.get("blocked_payload")
290 if isinstance(blocked_payload, Mapping):
291 emitted.append(
293 conn,
294 "migrate.note_blocked",
295 actor_slot,
296 blocked_payload,
297 actor_type=actor_type,
298 parent_operation_id=import_row.id,
299 related_plan_id=plan_id,
300 )
301 )
302
303 for round_plan in _mapping_list(projection.get("round_plans")):
304 self._upsert_migration_round_plan(conn, round_plan)
305
306 for queue_entry in _mapping_list(projection.get("queue_entries")):
307 plan_id = str(queue_entry["plan_id"])
308 emitted.append(
310 conn,
311 "migrate.import.queue_entry",
312 actor_slot,
313 _required_mapping(queue_entry, "payload"),
314 actor_type=actor_type,
315 parent_operation_id=(
316 import_rows_by_plan[plan_id].id
317 if plan_id in import_rows_by_plan
318 else None
319 ),
320 related_plan_id=plan_id,
321 )
322 )
323
324 max_plan_number = conn.execute(
325 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
326 "SELECT COALESCE(MAX(plan_number), 0) FROM plans"
327 ).fetchone()[0]
328 conn.execute(
329 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
330 "UPDATE plan_seq SET next_number = ? WHERE id = 1",
331 (int(max_plan_number) + 1,),
332 )
333 return emitted
334
335 def migration_snapshot(self) -> dict[str, object]:
336 """Return the current-state rows that ``migrate verify`` compares."""
337
338 with self.connect() as conn:
339 agents = [
340 _row_dict(row)
341 for row in conn.execute(
342 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
343 "SELECT slot, slot_kind, identity_kind, model_class, "
344 "orchestrator_eligible, provider, profile, active "
345 "FROM agents WHERE identity_kind = 'agent' "
346 "ORDER BY slot"
347 ).fetchall()
348 ]
349 plans = [
350 _row_dict(row)
351 for row in conn.execute(
352 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
353 "SELECT plan_id, plan_number, title, plan_type, status, "
354 "assigned_slot, queue_position, branch, worktree, body_path, "
355 "body_sha256, body_observed_at, model_override, "
356 "thinking_override, session_override, ready_for_merge_at, "
357 "completed_at, closed_round FROM plans ORDER BY plan_id"
358 ).fetchall()
359 ]
360 queue_entries = [
361 _row_dict(row)
362 for row in conn.execute(
363 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
364 "SELECT plan_id, assigned_slot AS slot, queue_position AS position "
365 "FROM plans WHERE queue_position IS NOT NULL "
366 "ORDER BY slot, position"
367 ).fetchall()
368 ]
369 rounds = [
370 _row_dict(row)
371 for row in conn.execute(
372 "SELECT round_no, status, cleanup_status, orchestrator_slot, "
373 "opened_at, closed_at, final_commit_sha FROM rounds "
374 "ORDER BY round_no"
375 ).fetchall()
376 ]
377 round_plans = [
378 _row_dict(row)
379 for row in conn.execute(
380 "SELECT round_no, plan_id, merge_order, merge_commit_sha, "
381 "test_result FROM round_plans ORDER BY round_no, merge_order"
382 ).fetchall()
383 ]
384 return {
385 "agents": agents,
386 "plans": plans,
387 "queue_entries": queue_entries,
388 "rounds": rounds,
389 "round_plans": round_plans,
390 }
391
393 """Return completed null-round plans missing a backfill audit marker."""
394
395 with self.connect() as conn:
396 plan_ids = [
397 str(row["plan_id"])
398 for row in conn.execute(
399 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
400 "SELECT plan_id FROM plans "
401 "WHERE status = 'completed' AND closed_round IS NULL "
402 "ORDER BY plan_id"
403 ).fetchall()
404 ]
405 backfilled: set[str] = set()
406 for row in conn.execute(
407 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
408 "SELECT args_json FROM operation_log "
409 "WHERE operation_type = 'migrate.backfill_completed'"
410 ).fetchall():
411 try:
412 payload = json.loads(str(row["args_json"]))
413 except (TypeError, ValueError):
414 continue
415 plan_id = payload.get("plan_id") if isinstance(payload, dict) else None
416 if isinstance(plan_id, str):
417 backfilled.add(plan_id)
418 return [plan_id for plan_id in plan_ids if plan_id not in backfilled]
419
421 self,
422 conn: sqlite3.Connection,
423 operation_type: str,
424 actor_slot: str,
425 payload: Mapping[str, object],
426 *,
427 actor_type: str,
428 payload_version: int = 1,
429 command: str | None = None,
430 parent_operation_id: int | None = None,
431 related_plan_id: str | None = None,
432 related_round: int | None = None,
433 operation_uuid: str | None = None,
434 hmac_key_id: str | None = None,
435 system_actor: bool = False,
436 ) -> OperationLogRow:
437 command_text = command or operation_type
438 operation_uuid = operation_uuid or str(uuid.uuid4())
439 args_json = self._validate_payload(
440 operation_type,
441 payload,
442 payload_version,
443 )
444
445 slot_kind = self._actor_slot_kind(conn, actor_slot)
446 expected_actor_type = _ACTOR_TYPES_BY_SLOT_KIND[slot_kind]
447 if actor_type != expected_actor_type:
449 actor_slot=actor_slot,
450 expected_actor_type=expected_actor_type,
451 supplied_actor_type=actor_type,
452 )
454 conn,
455 operation_type,
456 actor_slot,
457 slot_kind,
458 )
459 if parent_operation_id is not None:
461 conn,
462 parent_operation_id,
463 operation_uuid,
464 )
465 row_hmac_key_id = (
466 None
467 if system_actor
468 else hmac_key_id or self._active_hmac_key_id(conn, actor_slot)
469 )
470 current_version = self._current_state_version(conn)
471 next_version = current_version + 1
472 now = self.clock.now_wall()
473 cursor = conn.execute(
474 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
475 "INSERT INTO operation_log("
476 "operation_uuid, ts, state_version, actor_slot, actor_type, "
477 "hmac_key_id, command, operation_type, payload_version, "
478 "args_json, before_hash, after_hash, auth_ok, parent_operation_id, "
479 "related_plan_id, related_round"
480 ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 1, ?, ?, ?)",
481 (
482 operation_uuid,
483 now,
484 next_version,
485 actor_slot,
486 actor_type,
487 row_hmac_key_id,
488 command_text,
489 operation_type,
490 payload_version,
491 args_json,
492 parent_operation_id,
493 related_plan_id,
494 related_round,
495 ),
496 )
497 conn.execute(
498 "UPDATE registry_meta SET state_version = ?, updated_at = ? "
499 "WHERE id = 1",
500 (next_version, now),
501 )
503 conn,
504 operation_type,
505 payload,
506 actor_slot=actor_slot,
507 ts=now,
508 related_round=related_round,
509 source_operation_id=int(cursor.lastrowid),
510 )
511 return OperationLogRow(
512 id=int(cursor.lastrowid),
513 operation_uuid=operation_uuid,
514 ts=now,
515 state_version=next_version,
516 actor_slot=actor_slot,
517 actor_type=actor_type,
518 command=command_text,
519 operation_type=operation_type,
520 payload_version=payload_version,
521 args_json=args_json,
522 parent_operation_id=parent_operation_id,
523 related_plan_id=related_plan_id,
524 related_round=related_round,
525 hmac_key_id=row_hmac_key_id,
526 )
527
529 self,
530 conn: sqlite3.Connection,
531 operation_type: str,
532 payload: Mapping[str, object],
533 *,
534 actor_slot: str,
535 ts: str,
536 related_round: int | None,
537 source_operation_id: int,
538 ) -> None:
539 if operation_type == "plan.create":
540 self._project_plan_create(conn, payload, ts)
541 elif operation_type == "plan.set_status":
542 self._project_plan_set_status(conn, payload, actor_slot, ts)
543 elif operation_type == "plan.assign":
544 self._project_plan_assign(conn, payload, ts)
545 elif operation_type == "plan.supersede":
546 self._project_plan_supersede(conn, payload, ts)
547 elif operation_type == "plan.reimport":
548 self._project_plan_reimport(conn, payload, ts)
549 elif operation_type == "queue.enqueue":
550 self._project_queue_enqueue(conn, payload, ts)
551 elif operation_type == "queue.move":
552 self._project_queue_move(conn, payload, ts)
553 elif operation_type == "queue.dequeue":
554 self._project_queue_dequeue(conn, payload, ts)
555 elif operation_type == "queue.complete":
556 self._project_queue_complete(conn, payload, ts, related_round)
557 elif operation_type in {"role.handover.start", "handover.initiate"}:
558 self._project_handover_start(conn, payload, actor_slot, ts)
559 elif operation_type in {"role.handover.complete", "handover.accept"}:
560 self._project_handover_complete(conn, payload, actor_slot, ts)
561 elif operation_type == "roadmap_item.create":
563 conn,
564 payload,
565 actor_slot,
566 ts,
567 source_operation_id,
568 )
569 elif operation_type == "roadmap_item.set_status":
571 conn,
572 payload,
573 ts,
574 source_operation_id,
575 )
576 elif operation_type == "roadmap_item.add_dependency":
578 conn,
579 payload,
580 actor_slot,
581 ts,
582 source_operation_id,
583 )
584 elif operation_type == "roadmap_version.bump":
585 self._project_roadmap_version_bump(conn, payload, ts)
586 elif operation_type == "roadmap_phase.complete":
588 conn,
589 payload,
590 ts,
591 source_operation_id,
592 )
593 elif operation_type == "roadmap_section.upsert":
594 self._project_roadmap_section_upsert(conn, payload)
595 elif operation_type == "roadmap_plan_link.create":
596 self._project_roadmap_plan_link_create(conn, payload, actor_slot, ts)
597 elif operation_type == "round.open":
598 self._project_round_open(conn, payload, actor_slot, ts)
599 elif operation_type == "round.merge":
600 self._project_round_merge(conn, payload, ts)
601 elif operation_type == "round.test":
602 self._project_round_test(conn, payload)
603 elif operation_type == "round.lint":
604 self._project_round_lint(conn, payload)
605 elif operation_type == "round.consolidate":
606 self._project_round_consolidate(conn, payload)
607 elif operation_type == "round.ff_main":
608 self._project_round_ff_main(conn, payload)
609 elif operation_type == "round.cleanup":
610 self._project_round_cleanup(conn, payload, ts)
611 elif operation_type == "round.abort":
612 self._project_round_abort(conn, payload, ts)
613
615 self,
616 conn: sqlite3.Connection,
617 payload: Mapping[str, object],
618 actor_slot: str,
619 ts: str,
620 ) -> None:
621 state = self._orchestration_state(conn)
622 if state["handover_status"] != "stable":
623 raise HandoverStateError(
624 state="handover_in_flight",
625 actor_slot=actor_slot,
626 operation_type="role.handover.start",
627 )
628 from_slot = str(payload["from_slot"])
629 to_slot = str(payload["to_slot"])
630 conn.execute(
631 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
632 "UPDATE orchestration_state SET "
633 "orchestrator_slot = COALESCE(orchestrator_slot, ?), "
634 "handover_status = 'requested', handover_outgoing = ?, "
635 "handover_incoming = ?, handover_requested_at = ?, "
636 "ready_to_handover = 0, updated_at = ?, updated_by = ? "
637 "WHERE id = 1",
638 (from_slot, from_slot, to_slot, ts, ts, actor_slot),
639 )
640
642 self,
643 conn: sqlite3.Connection,
644 payload: Mapping[str, object],
645 actor_slot: str,
646 ts: str,
647 ) -> None:
648 state = self._orchestration_state(conn)
649 to_slot = str(payload["to_slot"])
650 if state["handover_status"] not in {"requested", "draining"}:
651 raise HandoverStateError(
652 state="no_handover_pending",
653 actor_slot=actor_slot,
654 operation_type="role.handover.complete",
655 handover_id=_optional_int(payload.get("parent_operation_id")),
656 )
657 if str(state["handover_incoming"]) != to_slot:
658 raise HandoverStateError(
659 state="handover_target_mismatch",
660 actor_slot=actor_slot,
661 operation_type="role.handover.complete",
662 handover_id=_optional_int(payload.get("parent_operation_id")),
663 )
664 if actor_slot != to_slot and self._actor_slot_kind(conn, actor_slot) != "operator":
665 raise HandoverStateError(
666 state="wrong_accepting_slot",
667 actor_slot=actor_slot,
668 operation_type="role.handover.complete",
669 handover_id=_optional_int(payload.get("parent_operation_id")),
670 )
671 conn.execute(
672 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
673 "UPDATE orchestration_state SET "
674 "orchestrator_slot = ?, handover_status = 'stable', "
675 "handover_outgoing = NULL, handover_incoming = NULL, "
676 "handover_requested_at = NULL, ready_to_handover = 0, "
677 "updated_at = ?, updated_by = ? WHERE id = 1",
678 (to_slot, ts, actor_slot),
679 )
680
682 self,
683 conn: sqlite3.Connection,
684 payload: Mapping[str, object],
685 ts: str,
686 ) -> None:
687 plan_number = int(payload["plan_number"])
688 conn.execute(
689 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
690 "INSERT INTO plans("
691 "plan_id, plan_number, title, plan_type, status, assigned_slot, "
692 "queue_position, branch, worktree, body_path, body_sha256, "
693 "body_observed_at, model_override, thinking_override, "
694 "session_override, created_at, updated_at"
695 ") VALUES (?, ?, ?, ?, 'draft', NULL, NULL, NULL, NULL, ?, ?, "
696 "?, ?, ?, ?, ?, ?) "
697 "ON CONFLICT(plan_id) DO UPDATE SET "
698 "plan_number = excluded.plan_number, "
699 "title = excluded.title, "
700 "plan_type = excluded.plan_type, "
701 "body_path = excluded.body_path, "
702 "body_sha256 = excluded.body_sha256, "
703 "body_observed_at = excluded.body_observed_at, "
704 "model_override = excluded.model_override, "
705 "thinking_override = excluded.thinking_override, "
706 "session_override = excluded.session_override, "
707 "updated_at = excluded.updated_at",
708 (
709 str(payload["plan_id"]),
710 plan_number,
711 str(payload["title"]),
712 _optional_str(payload.get("plan_type")),
713 str(payload["body_path"]),
714 str(payload["body_sha256"]),
715 _optional_str(payload.get("body_observed_at")) or ts,
716 _optional_str(payload.get("model_override")),
717 _optional_str(payload.get("thinking_override")),
718 _optional_str(payload.get("session_override")),
719 ts,
720 ts,
721 ),
722 )
723 conn.execute(
724 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
725 "UPDATE plan_seq SET next_number = CASE "
726 "WHEN next_number <= ? THEN ? ELSE next_number END WHERE id = 1",
727 (plan_number, plan_number + 1),
728 )
729
731 self,
732 conn: sqlite3.Connection,
733 payload: Mapping[str, object],
734 actor_slot: str,
735 ts: str,
736 ) -> None:
737 plan_id = str(payload["plan_id"])
738 to_status = str(payload["to_status"])
739 conn.execute(
740 "INSERT INTO plan_status_events("
741 "plan_id, from_status, to_status, actor_slot, source, created_at, note"
742 ") VALUES (?, ?, ?, ?, ?, ?, ?)",
743 (
744 plan_id,
745 _optional_str(payload.get("from_status")),
746 to_status,
747 actor_slot,
748 _optional_str(payload.get("source")),
749 ts,
750 _optional_str(payload.get("note")),
751 ),
752 )
753 queue_position_expr = (
754 "queue_position"
755 if to_status in {"pending", "implementing", "ready_for_merge"}
756 else "NULL"
757 )
758 conn.execute(
759 "UPDATE plans SET "
760 "status = ?, "
761 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
762 f"queue_position = {queue_position_expr}, "
763 "ready_for_merge_at = CASE "
764 "WHEN ? = 'ready_for_merge' AND ready_for_merge_at IS NULL THEN ? "
765 "ELSE ready_for_merge_at END, "
766 "completed_at = CASE "
767 "WHEN ? = 'completed' AND completed_at IS NULL THEN ? "
768 "ELSE completed_at END, "
769 "updated_at = ? "
770 "WHERE plan_id = ?",
771 (to_status, to_status, ts, to_status, ts, ts, plan_id),
772 )
773
775 self,
776 conn: sqlite3.Connection,
777 payload: Mapping[str, object],
778 ts: str,
779 ) -> None:
780 conn.execute(
781 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
782 "UPDATE plans SET assigned_slot = ?, branch = ?, worktree = ?, "
783 "queue_position = ?, status = CASE "
784 "WHEN status = 'draft' THEN 'pending' ELSE status END, "
785 "updated_at = ? WHERE plan_id = ?",
786 (
787 str(payload["to_slot"]),
788 str(payload["branch"]),
789 str(payload["worktree"]),
790 int(payload["queue_position"]),
791 ts,
792 str(payload["plan_id"]),
793 ),
794 )
795
797 self,
798 conn: sqlite3.Connection,
799 payload: Mapping[str, object],
800 ts: str,
801 ) -> None:
802 conn.execute(
803 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
804 "UPDATE plans SET status = 'superseded', queue_position = NULL, "
805 "superseded_by = ?, updated_at = ? WHERE plan_id = ?",
806 (
807 str(payload["superseded_by_plan_id"]),
808 ts,
809 str(payload["plan_id"]),
810 ),
811 )
812
814 self,
815 conn: sqlite3.Connection,
816 payload: Mapping[str, object],
817 ts: str,
818 ) -> None:
819 conn.execute(
820 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
821 "UPDATE plans SET body_sha256 = ?, body_observed_at = ?, "
822 "updated_at = ? WHERE plan_id = ?",
823 (
824 str(payload["new_body_sha256"]),
825 str(payload["new_body_observed_at"]),
826 ts,
827 str(payload["plan_id"]),
828 ),
829 )
830
832 self,
833 conn: sqlite3.Connection,
834 payload: Mapping[str, object],
835 ts: str,
836 ) -> None:
837 plan_id = str(payload["plan_id"])
838 slot = str(payload["slot"])
839 position = int(payload["position"])
840 if not self._plan_exists(conn, plan_id):
841 return
842 current = self._plan_queue_location(conn, plan_id)
843 if current is not None:
844 self._remove_queue_position(conn, current[0], current[1], plan_id)
845 self._shift_queue_positions_up(conn, slot, position, exclude_plan_id=plan_id)
846 conn.execute(
847 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
848 "UPDATE plans SET assigned_slot = ?, queue_position = ?, "
849 "status = CASE WHEN status IN "
850 "('draft','partial','failed','superseded','completed') "
851 "THEN 'pending' ELSE status END, updated_at = ? "
852 "WHERE plan_id = ?",
853 (slot, position, ts, plan_id),
854 )
855
857 self,
858 conn: sqlite3.Connection,
859 payload: Mapping[str, object],
860 ts: str,
861 ) -> None:
862 plan_id = str(payload["plan_id"])
863 if not self._plan_exists(conn, plan_id):
864 return
865 current = self._plan_queue_location(conn, plan_id)
866 from_slot = current[0] if current is not None else str(payload["from_slot"])
867 to_slot = str(payload["to_slot"])
868 from_position = current[1] if current is not None else int(payload["from_position"])
869 to_position = int(payload["to_position"])
870 self._remove_queue_position(conn, from_slot, from_position, plan_id)
871 self._shift_queue_positions_up(conn, to_slot, to_position, exclude_plan_id=plan_id)
872 conn.execute(
873 "UPDATE plans SET assigned_slot = ?, queue_position = ?, "
874 "updated_at = ? WHERE plan_id = ?",
875 (to_slot, to_position, ts, plan_id),
876 )
877
879 self,
880 conn: sqlite3.Connection,
881 payload: Mapping[str, object],
882 ts: str,
883 ) -> None:
884 plan_id = str(payload["plan_id"])
885 current = self._plan_queue_location(conn, plan_id)
886 slot = current[0] if current is not None else str(payload["slot"])
887 position = current[1] if current is not None else int(payload["position"])
888 reason = str(payload["reason"])
889 self._remove_queue_position(conn, slot, position, plan_id)
890 if reason == "transition_to_failed":
891 conn.execute(
892 "UPDATE plans SET status = 'failed', updated_at = ? WHERE plan_id = ?",
893 (ts, plan_id),
894 )
895 elif reason == "transition_to_integrating":
896 conn.execute(
897 "UPDATE plans SET status = 'integrating', updated_at = ? "
898 "WHERE plan_id = ?",
899 (ts, plan_id),
900 )
901 else:
902 conn.execute(
903 "UPDATE plans SET updated_at = ? WHERE plan_id = ?",
904 (ts, plan_id),
905 )
906
908 self,
909 conn: sqlite3.Connection,
910 payload: Mapping[str, object],
911 ts: str,
912 related_round: int | None,
913 ) -> None:
914 round_no = int(payload["round_no"])
915 conn.execute(
916 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
917 "UPDATE plans SET status = 'completed', queue_position = NULL, "
918 "completed_at = COALESCE(completed_at, ?), closed_round = ?, "
919 "updated_at = ? WHERE plan_id = ?",
920 (
921 ts,
922 related_round if related_round is not None else round_no,
923 ts,
924 str(payload["plan_id"]),
925 ),
926 )
927
929 self,
930 conn: sqlite3.Connection,
931 payload: Mapping[str, object],
932 actor_slot: str,
933 ts: str,
934 source_operation_id: int,
935 ) -> None:
936 item_id = str(payload["item_id"])
937 track_kind = str(payload.get("track_kind") or "m_phase")
938 phase_code = _optional_str(payload.get("phase_code"))
939 phase_id = None
940 if track_kind == "m_phase":
941 phase_code = phase_code or _phase_code_for_item_id(item_id)
942 if phase_code is None:
943 raise ValueError("roadmap m_phase item requires phase_code")
944 phase_id = self._roadmap_phase_id(
945 conn,
946 phase_code,
947 ts=ts,
948 label=_optional_str(payload.get("phase_label")) or phase_code,
949 )
950 parent_item_id = _optional_str(payload.get("parent_item_id"))
951 if parent_item_id is not None:
952 parent_row = conn.execute(
953 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
954 "SELECT 1 FROM roadmap_items_v3 WHERE item_id = ?",
955 (parent_item_id,),
956 ).fetchone()
957 if parent_row is None:
958 parent_item_id = None
959 introduced = _optional_str(payload.get("introduced_in_version_id"))
960 status = str(payload.get("status") or "todo")
961 conn.execute(
962 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
963 "INSERT INTO roadmap_items_v3("
964 "item_id, track_kind, phase_id, parent_item_id, title, prose, prose_path, "
965 "status, priority, sort_order, target_version, introduced_in_version_id, "
966 "closed_in_version_id, closed_round_no, superseded_by_item_id, "
967 "created_at, updated_at"
968 ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
969 "ON CONFLICT(item_id) DO UPDATE SET "
970 "track_kind = excluded.track_kind, "
971 "phase_id = excluded.phase_id, "
972 "parent_item_id = excluded.parent_item_id, "
973 "title = excluded.title, "
974 "prose = excluded.prose, "
975 "prose_path = excluded.prose_path, "
976 "status = excluded.status, "
977 "priority = excluded.priority, "
978 "sort_order = excluded.sort_order, "
979 "target_version = excluded.target_version, "
980 "introduced_in_version_id = COALESCE("
981 "roadmap_items_v3.introduced_in_version_id, "
982 "excluded.introduced_in_version_id"
983 "), "
984 "closed_in_version_id = excluded.closed_in_version_id, "
985 "closed_round_no = excluded.closed_round_no, "
986 "superseded_by_item_id = excluded.superseded_by_item_id, "
987 "updated_at = excluded.updated_at",
988 (
989 item_id,
990 track_kind,
991 phase_id,
992 parent_item_id,
993 str(payload["title"]),
994 _optional_str(payload.get("prose")),
995 _optional_str(payload.get("prose_path")),
996 status,
997 _optional_str(payload.get("priority")),
998 int(payload.get("sort_order") or 0),
999 _optional_str(payload.get("target_version")),
1000 introduced,
1001 _optional_str(payload.get("closed_in_version_id")),
1002 _optional_int(payload.get("closed_round_no")),
1003 _optional_str(payload.get("superseded_by_item_id")),
1004 ts,
1005 ts,
1006 ),
1007 )
1009 conn,
1010 item_id,
1011 "created",
1012 from_value_json=None,
1013 to_value_json=canonical_json(dict(payload)),
1014 in_version_id=introduced,
1015 source_operation_id=source_operation_id,
1016 ts=ts,
1017 note=f"created by {actor_slot}",
1018 )
1019
1021 self,
1022 conn: sqlite3.Connection,
1023 payload: Mapping[str, object],
1024 ts: str,
1025 source_operation_id: int,
1026 ) -> None:
1027 item_id = str(payload["item_id"])
1028 from_status = _optional_str(payload.get("from_status"))
1029 to_status = str(payload["to_status"])
1030 in_version_id = _optional_str(payload.get("in_version_id"))
1031 conn.execute(
1032 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1033 "UPDATE roadmap_items_v3 SET status = ?, "
1034 "closed_in_version_id = CASE "
1035 "WHEN ? IN ('done','deferred','superseded') THEN COALESCE(?, closed_in_version_id) "
1036 "ELSE closed_in_version_id END, "
1037 "closed_round_no = COALESCE(?, closed_round_no), "
1038 "superseded_by_item_id = COALESCE(?, superseded_by_item_id), "
1039 "updated_at = ? WHERE item_id = ?",
1040 (
1041 to_status,
1042 to_status,
1043 in_version_id,
1044 _optional_int(payload.get("closed_round_no")),
1045 _optional_str(payload.get("superseded_by_item_id")),
1046 ts,
1047 item_id,
1048 ),
1049 )
1051 conn,
1052 item_id,
1053 "status_change",
1054 from_value_json=canonical_json({"status": from_status}),
1055 to_value_json=canonical_json({"status": to_status}),
1056 in_version_id=in_version_id,
1057 source_operation_id=source_operation_id,
1058 ts=ts,
1059 note=_optional_str(payload.get("note")),
1060 )
1061
1063 self,
1064 conn: sqlite3.Connection,
1065 payload: Mapping[str, object],
1066 actor_slot: str,
1067 ts: str,
1068 source_operation_id: int,
1069 ) -> None:
1070 from_item_id = str(payload["from_item_id"])
1071 to_item_id = str(payload["to_item_id"])
1072 kind = str(payload.get("kind") or "gates")
1073 conn.execute(
1074 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1075 "INSERT OR IGNORE INTO roadmap_item_dependencies("
1076 "from_item_id, to_item_id, kind, created_at, created_by_slot"
1077 ") VALUES (?, ?, ?, ?, ?)",
1078 (from_item_id, to_item_id, kind, ts, actor_slot),
1079 )
1081 conn,
1082 from_item_id,
1083 "dependency_added",
1084 from_value_json=None,
1085 to_value_json=canonical_json({"to_item_id": to_item_id, "kind": kind}),
1086 in_version_id=_optional_str(payload.get("in_version_id")),
1087 source_operation_id=source_operation_id,
1088 ts=ts,
1089 note=_optional_str(payload.get("note")),
1090 )
1091
1093 self,
1094 conn: sqlite3.Connection,
1095 payload: Mapping[str, object],
1096 ts: str,
1097 ) -> None:
1098 from_version = _optional_str(payload.get("from_version_id"))
1099 to_version = str(payload["to_version_id"])
1100 era_id = _optional_str(payload.get("era_id")) or _era_id_for_version(to_version)
1101 self._roadmap_era(conn, era_id, ts)
1102 status = str(payload.get("status") or "draft")
1103 if status == "current":
1104 conn.execute(
1105 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1106 "UPDATE roadmap_versions SET status = 'superseded', "
1107 "superseded_by_version_id = ?, last_updated_at = ? "
1108 "WHERE era_id = ? AND status = 'current'",
1109 (to_version, ts, era_id),
1110 )
1111 conn.execute(
1112 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1113 "INSERT INTO roadmap_versions("
1114 "version_id, era_id, released_at, status, parent_version_id, "
1115 "supersedes_version_id, superseded_by_version_id, last_updated_at, "
1116 "source_round_no, bump_kind, notes"
1117 ") VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?) "
1118 "ON CONFLICT(version_id) DO UPDATE SET "
1119 "status = excluded.status, "
1120 "last_updated_at = excluded.last_updated_at, "
1121 "source_round_no = excluded.source_round_no, "
1122 "bump_kind = excluded.bump_kind, "
1123 "notes = excluded.notes",
1124 (
1125 to_version,
1126 era_id,
1127 _optional_str(payload.get("released_at")) or ts,
1128 status,
1129 from_version,
1130 from_version,
1131 _optional_str(payload.get("last_updated_at")) or ts,
1132 _optional_int(payload.get("source_round_no")),
1133 str(payload.get("bump_kind") or "patch"),
1134 _optional_str(payload.get("notes")),
1135 ),
1136 )
1137
1139 self,
1140 conn: sqlite3.Connection,
1141 payload: Mapping[str, object],
1142 ts: str,
1143 source_operation_id: int,
1144 ) -> None:
1145 phase_code = str(payload["phase_code"])
1146 in_version_id = _optional_str(payload.get("in_version_id"))
1147 row = conn.execute(
1148 "SELECT item_id FROM roadmap_items_v3 WHERE item_id = ?",
1149 (phase_code,),
1150 ).fetchone()
1151 if row is not None:
1152 conn.execute(
1153 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1154 "UPDATE roadmap_items_v3 SET status = 'done', "
1155 "closed_in_version_id = COALESCE(?, closed_in_version_id), "
1156 "updated_at = ? WHERE item_id = ?",
1157 (in_version_id, ts, phase_code),
1158 )
1160 conn,
1161 phase_code,
1162 "status_change",
1163 from_value_json=None,
1164 to_value_json=canonical_json({"status": "done"}),
1165 in_version_id=in_version_id,
1166 source_operation_id=source_operation_id,
1167 ts=ts,
1168 note=_optional_str(payload.get("note")),
1169 )
1170 conn.execute(
1171 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1172 "UPDATE roadmap_phases SET retired_in_version_id = COALESCE(?, retired_in_version_id), "
1173 "updated_at = ? WHERE phase_code = ?",
1174 (in_version_id, ts, phase_code),
1175 )
1176
1178 self,
1179 conn: sqlite3.Connection,
1180 payload: Mapping[str, object],
1181 ) -> None:
1182 conn.execute(
1183 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1184 "DELETE FROM roadmap_render_sections "
1185 "WHERE file_kind = ? AND section_path = ?",
1186 (str(payload["file_kind"]), str(payload["section_path"])),
1187 )
1188 conn.execute(
1189 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1190 "INSERT INTO roadmap_render_sections("
1191 "file_kind, section_path, sort_order, query_kind, "
1192 "query_params_json, required_min_version, active"
1193 ") VALUES (?, ?, ?, ?, ?, ?, ?)",
1194 (
1195 str(payload["file_kind"]),
1196 str(payload["section_path"]),
1197 int(payload["sort_order"]),
1198 str(payload["query_kind"]),
1199 _optional_str(payload.get("query_params_json")),
1200 _optional_str(payload.get("required_min_version")),
1201 _bool_int(payload.get("active", True)),
1202 ),
1203 )
1204
1206 self,
1207 conn: sqlite3.Connection,
1208 payload: Mapping[str, object],
1209 actor_slot: str,
1210 ts: str,
1211 ) -> None:
1212 conn.execute(
1213 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1214 "INSERT OR REPLACE INTO roadmap_plan_links("
1215 "plan_id, item_id, contribution, created_at, created_by_slot"
1216 ") VALUES (?, ?, ?, ?, ?)",
1217 (
1218 str(payload["plan_id"]),
1219 str(payload["item_id"]),
1220 str(payload.get("contribution") or "partial"),
1221 ts,
1222 actor_slot,
1223 ),
1224 )
1225
1227 self,
1228 conn: sqlite3.Connection,
1229 era_id: str,
1230 ts: str,
1231 ) -> None:
1232 conn.execute(
1233 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1234 "INSERT OR IGNORE INTO roadmap_eras("
1235 "era_id, era_label, major_bump_reason, started_at, ended_at, "
1236 "archived_under, sort_order"
1237 ") VALUES (?, ?, NULL, ?, NULL, NULL, ?)",
1238 (era_id, f"{era_id} roadmap era", ts, _era_sort_order(era_id)),
1239 )
1240
1242 self,
1243 conn: sqlite3.Connection,
1244 phase_code: str,
1245 *,
1246 ts: str,
1247 label: str,
1248 ) -> int:
1249 parent_code = _parent_phase_code(phase_code)
1250 parent_phase_id = None
1251 if parent_code is not None and parent_code != phase_code:
1252 parent_phase_id = self._roadmap_phase_id(
1253 conn,
1254 parent_code,
1255 ts=ts,
1256 label=parent_code,
1257 )
1258 conn.execute(
1259 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1260 "INSERT INTO roadmap_phases("
1261 "phase_code, label, parent_phase_id, branch_suggestion, "
1262 "commit_prefix, target_version, priority_label, parallel_track, "
1263 "full_spec_url, sort_order, introduced_in_version_id, "
1264 "retired_in_version_id, created_at, updated_at"
1265 ") VALUES (?, ?, ?, NULL, NULL, NULL, NULL, ?, NULL, ?, NULL, NULL, ?, ?) "
1266 "ON CONFLICT(phase_code) DO UPDATE SET "
1267 "label = excluded.label, "
1268 "parent_phase_id = COALESCE(roadmap_phases.parent_phase_id, excluded.parent_phase_id), "
1269 "updated_at = excluded.updated_at",
1270 (
1271 phase_code,
1272 label,
1273 parent_phase_id,
1274 1 if phase_code == "M11" else 0,
1275 _phase_sort_order(phase_code),
1276 ts,
1277 ts,
1278 ),
1279 )
1280 row = conn.execute(
1281 "SELECT phase_id FROM roadmap_phases WHERE phase_code = ?",
1282 (phase_code,),
1283 ).fetchone()
1284 if row is None:
1285 raise ValueError(f"failed to create roadmap phase: {phase_code}")
1286 return int(row["phase_id"])
1287
1289 self,
1290 conn: sqlite3.Connection,
1291 item_id: str,
1292 event_kind: str,
1293 *,
1294 from_value_json: str | None,
1295 to_value_json: str | None,
1296 in_version_id: str | None,
1297 source_operation_id: int,
1298 ts: str,
1299 note: str | None,
1300 ) -> None:
1301 conn.execute(
1302 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1303 "INSERT INTO roadmap_item_events("
1304 "item_id, event_kind, from_value_json, to_value_json, "
1305 "in_version_id, source_operation_id, source_version_seen, ts, note"
1306 ") VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)",
1307 (
1308 item_id,
1309 event_kind,
1310 from_value_json,
1311 to_value_json,
1312 in_version_id,
1313 source_operation_id,
1314 ts,
1315 note,
1316 ),
1317 )
1318
1319 def _plan_exists(self, conn: sqlite3.Connection, plan_id: str) -> bool:
1320 row = conn.execute("SELECT 1 FROM plans WHERE plan_id = ?", (plan_id,)).fetchone()
1321 return row is not None
1322
1324 self,
1325 conn: sqlite3.Connection,
1326 plan_id: str,
1327 ) -> tuple[str, int] | None:
1328 row = conn.execute(
1329 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1330 "SELECT assigned_slot, queue_position FROM plans WHERE plan_id = ?",
1331 (plan_id,),
1332 ).fetchone()
1333 if row is None or row["assigned_slot"] is None or row["queue_position"] is None:
1334 return None
1335 return str(row["assigned_slot"]), int(row["queue_position"])
1336
1338 self,
1339 conn: sqlite3.Connection,
1340 slot: str,
1341 start_position: int,
1342 *,
1343 exclude_plan_id: str | None = None,
1344 ) -> None:
1345 if exclude_plan_id is None:
1346 conn.execute(
1347 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1348 "UPDATE plans SET queue_position = queue_position + 100000 "
1349 "WHERE assigned_slot = ? AND queue_position >= ?",
1350 (slot, start_position),
1351 )
1352 else:
1353 conn.execute(
1354 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1355 "UPDATE plans SET queue_position = queue_position + 100000 "
1356 "WHERE assigned_slot = ? AND queue_position >= ? AND plan_id != ?",
1357 (slot, start_position, exclude_plan_id),
1358 )
1359 conn.execute(
1360 "UPDATE plans SET queue_position = queue_position - 99999 "
1361 "WHERE assigned_slot = ? AND queue_position >= ?",
1362 (slot, start_position + 100000),
1363 )
1364
1366 self,
1367 conn: sqlite3.Connection,
1368 slot: str,
1369 position: int,
1370 plan_id: str,
1371 ) -> None:
1372 conn.execute(
1373 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1374 "UPDATE plans SET queue_position = NULL WHERE plan_id = ?",
1375 (plan_id,),
1376 )
1377 conn.execute(
1378 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1379 "UPDATE plans SET queue_position = queue_position + 100000 "
1380 "WHERE assigned_slot = ? AND queue_position > ?",
1381 (slot, position),
1382 )
1383 conn.execute(
1384 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1385 "UPDATE plans SET queue_position = queue_position - 100001 "
1386 "WHERE assigned_slot = ? AND queue_position >= ?",
1387 (slot, position + 100001),
1388 )
1389
1391 self,
1392 conn: sqlite3.Connection,
1393 agent: Mapping[str, object],
1394 ) -> None:
1395 now = self.clock.now_wall()
1396 conn.execute(
1397 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1398 "INSERT INTO agents("
1399 "slot, slot_kind, identity_kind, model_class, orchestrator_eligible, "
1400 "provider, profile, active, created_at"
1401 ") VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?) "
1402 "ON CONFLICT(slot) DO UPDATE SET "
1403 "slot_kind = excluded.slot_kind, "
1404 "identity_kind = excluded.identity_kind, "
1405 "model_class = excluded.model_class, "
1406 "orchestrator_eligible = excluded.orchestrator_eligible, "
1407 "provider = excluded.provider, "
1408 "profile = excluded.profile, "
1409 "active = 1",
1410 (
1411 str(agent["slot"]),
1412 str(agent["slot_kind"]),
1413 str(agent["identity_kind"]),
1414 str(agent["model_class"]),
1415 _bool_int(agent["orchestrator_eligible"]),
1416 _optional_str(agent.get("provider")),
1417 _optional_str(agent.get("profile")),
1418 now,
1419 ),
1420 )
1421
1422 # -- M11.7 round-close projections (operation_log seam) ------------------
1423
1425 self,
1426 conn: sqlite3.Connection,
1427 payload: Mapping[str, object],
1428 actor_slot: str,
1429 ts: str,
1430 ) -> None:
1431 round_no = int(payload["round_no"])
1432 existing = conn.execute(
1433 "SELECT status FROM rounds WHERE round_no = ?",
1434 (round_no,),
1435 ).fetchone()
1436 if existing is not None and str(existing["status"]) in {"closed", "aborted"}:
1437 raise RoundStateError(
1438 round_no=round_no,
1439 status=str(existing["status"]),
1440 operation_type="round.open",
1441 )
1442 orchestrator_slot = _optional_str(payload.get("orchestrator_slot")) or actor_slot
1443 # SF-001: idempotent-additive — a re-open never clobbers an open round.
1444 conn.execute(
1445 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1446 "INSERT INTO rounds(round_no, status, cleanup_status, "
1447 "orchestrator_slot, opened_at) VALUES (?, 'open', 'pending', ?, ?) "
1448 "ON CONFLICT(round_no) DO NOTHING",
1449 (round_no, orchestrator_slot, ts),
1450 )
1451 plan_id = _optional_str(payload.get("plan_id"))
1452 if plan_id is not None:
1453 merge_order = _optional_int(payload.get("merge_order"))
1454 if merge_order is None:
1455 merge_order = self._next_round_merge_order(conn, round_no)
1456 conn.execute(
1457 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1458 "INSERT INTO round_plans(round_no, plan_id, merge_order) "
1459 "VALUES (?, ?, ?) ON CONFLICT(round_no, plan_id) DO NOTHING",
1460 (round_no, plan_id, merge_order),
1461 )
1462
1464 self,
1465 conn: sqlite3.Connection,
1466 round_no: int,
1467 ) -> int:
1468 row = conn.execute(
1469 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1470 "SELECT COALESCE(MAX(merge_order), 0) AS hi FROM round_plans "
1471 "WHERE round_no = ?",
1472 (round_no,),
1473 ).fetchone()
1474 return int(row["hi"]) + 1
1475
1477 self,
1478 conn: sqlite3.Connection,
1479 payload: Mapping[str, object],
1480 ts: str,
1481 ) -> None:
1482 round_no = int(payload["round_no"])
1483 plan_id = str(payload["plan_id"])
1484 staging_branch = _optional_str(payload.get("staging_branch"))
1485 merge_base_sha = _optional_str(payload.get("merge_base_sha"))
1486 conn.execute(
1487 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1488 "UPDATE rounds SET status = 'closing', "
1489 "staging_branch = COALESCE(?, staging_branch), "
1490 "merge_base_sha = COALESCE(?, merge_base_sha) "
1491 "WHERE round_no = ? AND status IN ('open', 'closing')",
1492 (staging_branch, merge_base_sha, round_no),
1493 )
1494 merge_order = _optional_int(payload.get("merge_order"))
1495 if merge_order is None:
1496 merge_order = self._next_round_merge_order(conn, round_no)
1497 conn.execute(
1498 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1499 "INSERT INTO round_plans(round_no, plan_id, merge_order, base_sha, "
1500 "merge_commit_sha) VALUES (?, ?, ?, ?, ?) "
1501 "ON CONFLICT(round_no, plan_id) DO UPDATE SET "
1502 "base_sha = COALESCE(excluded.base_sha, round_plans.base_sha), "
1503 "merge_commit_sha = COALESCE(excluded.merge_commit_sha, "
1504 "round_plans.merge_commit_sha)",
1505 (
1506 round_no,
1507 plan_id,
1508 merge_order,
1509 _optional_str(payload.get("base_sha")),
1510 _optional_str(payload.get("merge_commit_sha")),
1511 ),
1512 )
1513
1515 self,
1516 conn: sqlite3.Connection,
1517 payload: Mapping[str, object],
1518 ) -> None:
1519 round_no = int(payload["round_no"])
1520 conn.execute(
1521 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1522 "UPDATE rounds SET "
1523 "test_count_before = COALESCE(?, test_count_before), "
1524 "test_count_after = COALESCE(?, test_count_after) "
1525 "WHERE round_no = ?",
1526 (
1527 _optional_int(payload.get("test_count_before")),
1528 _optional_int(payload.get("test_count_after")),
1529 round_no,
1530 ),
1531 )
1532 for entry in _mapping_list(payload.get("plan_results")):
1533 conn.execute(
1534 "UPDATE round_plans SET test_result = ? "
1535 "WHERE round_no = ? AND plan_id = ?",
1536 (
1537 _optional_str(entry.get("test_result")),
1538 round_no,
1539 str(entry["plan_id"]),
1540 ),
1541 )
1542
1544 self,
1545 conn: sqlite3.Connection,
1546 payload: Mapping[str, object],
1547 ) -> None:
1548 # Lint is audit-only at the rounds projection level; the operation_log
1549 # row carries errors/warnings counts. No rounds-table column to update.
1550 _ = (conn, payload)
1551
1553 self,
1554 conn: sqlite3.Connection,
1555 payload: Mapping[str, object],
1556 ) -> None:
1557 round_no = int(payload["round_no"])
1558 conn.execute(
1559 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1560 "UPDATE rounds SET status = 'committed', final_commit_sha = ? "
1561 "WHERE round_no = ? AND status IN ('open', 'closing', 'committed')",
1562 (_optional_str(payload.get("final_commit_sha")), round_no),
1563 )
1564
1566 self,
1567 conn: sqlite3.Connection,
1568 payload: Mapping[str, object],
1569 ) -> None:
1570 # Fast-forwarding main is a git-only effect; nothing to project.
1571 _ = (conn, payload)
1572
1574 self,
1575 conn: sqlite3.Connection,
1576 payload: Mapping[str, object],
1577 ts: str,
1578 ) -> None:
1579 round_no = int(payload["round_no"])
1580 outcome = _optional_str(payload.get("outcome")) or "clean"
1581 if outcome == "clean":
1582 conn.execute(
1583 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1584 "UPDATE rounds SET cleanup_status = 'clean', status = 'closed', "
1585 "closed_at = ?, "
1586 "roadmap_version = COALESCE(?, roadmap_version), "
1587 "app_version = COALESCE(?, app_version) "
1588 "WHERE round_no = ? AND status IN ('committed', 'closed')",
1589 (
1590 ts,
1591 _optional_str(payload.get("roadmap_version")),
1592 _optional_str(payload.get("app_version")),
1593 round_no,
1594 ),
1595 )
1596 else:
1597 conn.execute(
1598 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1599 "UPDATE rounds SET cleanup_status = 'failed' WHERE round_no = ?",
1600 (round_no,),
1601 )
1602
1604 self,
1605 conn: sqlite3.Connection,
1606 payload: Mapping[str, object],
1607 ts: str,
1608 ) -> None:
1609 round_no = int(payload["round_no"])
1610 conn.execute(
1611 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1612 "UPDATE rounds SET status = 'aborted', closed_at = ? "
1613 "WHERE round_no = ? AND status NOT IN ('closed')",
1614 (ts, round_no),
1615 )
1616
1617 # -- M11.7 round-job tracking seam (not operation_log audit rows) ---------
1618
1620 self,
1621 *,
1622 job_id: str,
1623 round_no: int,
1624 created_by: str,
1625 staging_branch_head_sha: str | None = None,
1626 deadline_at: str | None = None,
1627 ) -> None:
1628 """Insert a new resumable round-close job row."""
1629
1630 now = self.clock.now_wall()
1631 with self.transaction("immediate") as conn:
1632 conn.execute(
1633 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1634 "INSERT INTO round_jobs(job_id, round_no, status, current_step, "
1635 "created_by, deadline_at, staging_branch_head_sha, created_at, "
1636 "updated_at) VALUES (?, ?, 'running', NULL, ?, ?, ?, ?, ?)",
1637 (
1638 job_id,
1639 round_no,
1640 created_by,
1641 deadline_at,
1642 staging_branch_head_sha,
1643 now,
1644 now,
1645 ),
1646 )
1647
1648 def get_round_job(self, job_id: str) -> dict[str, object] | None:
1649 """Return a round-job row by id, or None."""
1650
1651 with self.connect() as conn:
1652 row = conn.execute(
1653 "SELECT * FROM round_jobs WHERE job_id = ?",
1654 (job_id,),
1655 ).fetchone()
1656 return None if row is None else _row_dict(row)
1657
1659 self,
1660 round_no: int,
1661 *,
1662 statuses: tuple[str, ...] | None = None,
1663 ) -> dict[str, object] | None:
1664 """Return the most-recent round-job for a round, optional status filter."""
1665
1666 query = "SELECT * FROM round_jobs WHERE round_no = ?"
1667 params: list[object] = [round_no]
1668 if statuses:
1669 placeholders = ", ".join("?" for _ in statuses)
1670 query += f" AND status IN ({placeholders})"
1671 params.extend(statuses)
1672 query += " ORDER BY created_at DESC, rowid DESC LIMIT 1"
1673 with self.connect() as conn:
1674 row = conn.execute(query, tuple(params)).fetchone()
1675 return None if row is None else _row_dict(row)
1676
1678 self,
1679 job_id: str,
1680 *,
1681 status: str,
1682 current_step: str | None = None,
1683 update_step: bool = True,
1684 ) -> None:
1685 """Update a round-job's status (and optionally current_step)."""
1686
1687 now = self.clock.now_wall()
1688 with self.transaction("immediate") as conn:
1689 if update_step:
1690 conn.execute(
1691 "UPDATE round_jobs SET status = ?, current_step = ?, "
1692 "updated_at = ? WHERE job_id = ?",
1693 (status, current_step, now, job_id),
1694 )
1695 else:
1696 conn.execute(
1697 "UPDATE round_jobs SET status = ?, updated_at = ? "
1698 "WHERE job_id = ?",
1699 (status, now, job_id),
1700 )
1701
1702 def set_round_job_head_sha(self, job_id: str, sha: str | None) -> None:
1703 """Record the staging-branch HEAD snapshot for a round-job."""
1704
1705 now = self.clock.now_wall()
1706 with self.transaction("immediate") as conn:
1707 conn.execute(
1708 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1709 "UPDATE round_jobs SET staging_branch_head_sha = ?, "
1710 "updated_at = ? WHERE job_id = ?",
1711 (sha, now, job_id),
1712 )
1713
1715 self,
1716 *,
1717 job_id: str,
1718 step_no: int,
1719 step_name: str,
1720 status: str,
1721 input_hash: str | None = None,
1722 post_step_head_sha: str | None = None,
1723 result_summary: str | None = None,
1724 error_summary: str | None = None,
1725 artifact_paths_json: str | None = None,
1726 started_at: str | None = None,
1727 finished_at: str | None = None,
1728 ) -> None:
1729 """Insert or update a single round-job step row."""
1730
1731 with self.transaction("immediate") as conn:
1732 conn.execute(
1733 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1734 "INSERT INTO round_job_steps(job_id, step_no, step_name, status, "
1735 "input_hash, post_step_head_sha, started_at, finished_at, "
1736 "result_summary, error_summary, artifact_paths_json) "
1737 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
1738 "ON CONFLICT(job_id, step_no) DO UPDATE SET "
1739 "step_name = excluded.step_name, status = excluded.status, "
1740 "input_hash = COALESCE(excluded.input_hash, "
1741 "round_job_steps.input_hash), "
1742 "post_step_head_sha = COALESCE(excluded.post_step_head_sha, "
1743 "round_job_steps.post_step_head_sha), "
1744 "started_at = COALESCE(excluded.started_at, "
1745 "round_job_steps.started_at), "
1746 "finished_at = COALESCE(excluded.finished_at, "
1747 "round_job_steps.finished_at), "
1748 "result_summary = COALESCE(excluded.result_summary, "
1749 "round_job_steps.result_summary), "
1750 "error_summary = excluded.error_summary, "
1751 "artifact_paths_json = COALESCE(excluded.artifact_paths_json, "
1752 "round_job_steps.artifact_paths_json)",
1753 (
1754 job_id,
1755 step_no,
1756 step_name,
1757 status,
1758 input_hash,
1759 post_step_head_sha,
1760 started_at,
1761 finished_at,
1762 result_summary,
1763 error_summary,
1764 artifact_paths_json,
1765 ),
1766 )
1767
1768 def list_round_job_steps(self, job_id: str) -> list[dict[str, object]]:
1769 """Return all step rows for a round-job ordered by step number."""
1770
1771 with self.connect() as conn:
1772 rows = conn.execute(
1773 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1774 "SELECT * FROM round_job_steps WHERE job_id = ? ORDER BY step_no",
1775 (job_id,),
1776 ).fetchall()
1777 return [_row_dict(row) for row in rows]
1778
1779 def get_round(self, round_no: int) -> dict[str, object] | None:
1780 """Return a round row by number, or None."""
1781
1782 with self.connect() as conn:
1783 row = conn.execute(
1784 "SELECT * FROM rounds WHERE round_no = ?",
1785 (round_no,),
1786 ).fetchone()
1787 return None if row is None else _row_dict(row)
1788
1789 def list_round_plans(self, round_no: int) -> list[dict[str, object]]:
1790 """Return round_plans rows for a round ordered by merge_order."""
1791
1792 with self.connect() as conn:
1793 rows = conn.execute(
1794 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1795 "SELECT * FROM round_plans WHERE round_no = ? ORDER BY merge_order",
1796 (round_no,),
1797 ).fetchall()
1798 return [_row_dict(row) for row in rows]
1799
1801 self,
1802 round_no: int,
1803 plan_id: str,
1804 ) -> dict[str, object] | None:
1805 """Return a single round_plans row, or None."""
1806
1807 with self.connect() as conn:
1808 row = conn.execute(
1809 "SELECT * FROM round_plans WHERE round_no = ? AND plan_id = ?",
1810 (round_no, plan_id),
1811 ).fetchone()
1812 return None if row is None else _row_dict(row)
1813
1815 self,
1816 conn: sqlite3.Connection,
1817 round_row: Mapping[str, object],
1818 ) -> None:
1819 conn.execute(
1820 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1821 "INSERT INTO rounds("
1822 "round_no, status, cleanup_status, orchestrator_slot, opened_at, "
1823 "closed_at, final_commit_sha"
1824 ") VALUES (?, ?, ?, ?, ?, ?, ?) "
1825 "ON CONFLICT(round_no) DO UPDATE SET "
1826 "status = excluded.status, "
1827 "cleanup_status = excluded.cleanup_status, "
1828 "orchestrator_slot = excluded.orchestrator_slot, "
1829 "opened_at = excluded.opened_at, "
1830 "closed_at = excluded.closed_at, "
1831 "final_commit_sha = excluded.final_commit_sha",
1832 (
1833 int(round_row["round_no"]),
1834 str(round_row["status"]),
1835 str(round_row["cleanup_status"]),
1836 str(round_row["orchestrator_slot"]),
1837 str(round_row["opened_at"]),
1838 _optional_str(round_row.get("closed_at")),
1839 _optional_str(round_row.get("final_commit_sha")),
1840 ),
1841 )
1842
1844 self,
1845 conn: sqlite3.Connection,
1846 plan: Mapping[str, object],
1847 ) -> None:
1848 now = self.clock.now_wall()
1849 conn.execute(
1850 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1851 "INSERT INTO plans("
1852 "plan_id, plan_number, title, plan_type, status, assigned_slot, "
1853 "queue_position, branch, worktree, body_path, body_sha256, "
1854 "body_observed_at, model_override, thinking_override, "
1855 "session_override, created_at, updated_at, ready_for_merge_at, "
1856 "completed_at, closed_round"
1857 ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
1858 "ON CONFLICT(plan_id) DO UPDATE SET "
1859 "plan_number = excluded.plan_number, "
1860 "title = excluded.title, "
1861 "plan_type = excluded.plan_type, "
1862 "status = excluded.status, "
1863 "assigned_slot = excluded.assigned_slot, "
1864 "queue_position = excluded.queue_position, "
1865 "branch = excluded.branch, "
1866 "worktree = excluded.worktree, "
1867 "body_path = excluded.body_path, "
1868 "body_sha256 = excluded.body_sha256, "
1869 "body_observed_at = excluded.body_observed_at, "
1870 "model_override = excluded.model_override, "
1871 "thinking_override = excluded.thinking_override, "
1872 "session_override = excluded.session_override, "
1873 "updated_at = excluded.updated_at, "
1874 "ready_for_merge_at = excluded.ready_for_merge_at, "
1875 "completed_at = excluded.completed_at, "
1876 "closed_round = excluded.closed_round",
1877 (
1878 str(plan["plan_id"]),
1879 int(plan["plan_number"]),
1880 str(plan["title"]),
1881 _optional_str(plan.get("plan_type")),
1882 str(plan["status"]),
1883 _optional_str(plan.get("assigned_slot")),
1884 _optional_int(plan.get("queue_position")),
1885 _optional_str(plan.get("branch")),
1886 _optional_str(plan.get("worktree")),
1887 str(plan["body_path"]),
1888 _optional_str(plan.get("body_sha256")),
1889 _optional_str(plan.get("body_observed_at")),
1890 _optional_str(plan.get("model_override")),
1891 _optional_str(plan.get("thinking_override")),
1892 _optional_str(plan.get("session_override")),
1893 now,
1894 now,
1895 _optional_str(plan.get("ready_for_merge_at")),
1896 _optional_str(plan.get("completed_at")),
1897 _optional_int(plan.get("closed_round")),
1898 ),
1899 )
1900
1902 self,
1903 conn: sqlite3.Connection,
1904 plan: Mapping[str, object],
1905 actor_slot: str,
1906 ) -> None:
1907 conn.execute(
1908 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1909 "INSERT INTO plan_status_events("
1910 "plan_id, from_status, to_status, actor_slot, source, created_at, note"
1911 ") VALUES (?, NULL, ?, ?, 'migrate', ?, ?)",
1912 (
1913 str(plan["plan_id"]),
1914 str(plan["status"]),
1915 actor_slot,
1916 self.clock.now_wall(),
1917 _optional_str(plan.get("legacy_status")),
1918 ),
1919 )
1920
1922 self,
1923 conn: sqlite3.Connection,
1924 round_plan: Mapping[str, object],
1925 ) -> None:
1926 conn.execute(
1927 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1928 "INSERT INTO round_plans("
1929 "round_no, plan_id, merge_order, merge_commit_sha, test_result"
1930 ") VALUES (?, ?, ?, ?, ?) "
1931 "ON CONFLICT(round_no, plan_id) DO UPDATE SET "
1932 "merge_order = excluded.merge_order, "
1933 "merge_commit_sha = excluded.merge_commit_sha, "
1934 "test_result = excluded.test_result",
1935 (
1936 int(round_plan["round_no"]),
1937 str(round_plan["plan_id"]),
1938 int(round_plan["merge_order"]),
1939 _optional_str(round_plan.get("merge_commit_sha")),
1940 _optional_str(round_plan.get("test_result")),
1941 ),
1942 )
1943
1944 def append_operation_log(self, entry: Mapping[str, object]) -> object:
1945 raise NotImplementedError("operation-log writing is implemented in plan 082")
1946
1948 self,
1949 name: str,
1950 owner: str,
1951 ttl_seconds: int,
1952 metadata: Mapping[str, object] | None = None,
1953 ) -> LockHandle:
1954 if ttl_seconds <= 0:
1955 raise ValueError("ttl_seconds must be positive")
1956 self.startup_sweep()
1957 acquired_at = self.clock.now_wall()
1958 expires_at = wall_plus_seconds(acquired_at, ttl_seconds)
1959 metadata_json = None if metadata is None else canonical_json(metadata)
1960 try:
1961 with self.transaction("immediate") as conn:
1962 conn.execute(
1963 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
1964 "INSERT INTO locks("
1965 "name, owner_slot, acquired_at, expires_at, metadata_json"
1966 ") VALUES (?, ?, ?, ?, ?)",
1967 (name, owner, acquired_at, expires_at, metadata_json),
1968 )
1969 except sqlite3.IntegrityError as exc:
1970 holder = self._lock_holder(name)
1971 if holder is None:
1972 raise
1973 raise LockHeldError(
1974 name=name,
1975 owner_slot=holder["owner_slot"],
1976 expires_at=holder["expires_at"],
1977 ) from exc
1978 return LockHandle(
1979 name=name,
1980 owner_slot=owner,
1981 expires_at=expires_at,
1982 _release=self.release_lock,
1983 _renew=self.renew_lock,
1984 )
1985
1986 def release_lock(self, name: str, owner: str) -> None:
1987 with self.transaction("immediate") as conn:
1988 row = conn.execute(
1989 "SELECT owner_slot FROM locks WHERE name = ?",
1990 (name,),
1991 ).fetchone()
1992 if row is None:
1993 return
1994 actual_owner = str(row["owner_slot"])
1995 if actual_owner != owner:
1996 raise LockOwnerError(
1997 name=name,
1998 owner_slot=owner,
1999 actual_owner_slot=actual_owner,
2000 )
2001 conn.execute("DELETE FROM locks WHERE name = ?", (name,))
2002
2003 def renew_lock(self, name: str, owner: str, ttl_seconds: int) -> str | None:
2004 if ttl_seconds <= 0:
2005 raise ValueError("ttl_seconds must be positive")
2006 expires_at = wall_plus_seconds(self.clock.now_wall(), ttl_seconds)
2007 with self.transaction("immediate") as conn:
2008 row = conn.execute(
2009 "SELECT owner_slot FROM locks WHERE name = ?",
2010 (name,),
2011 ).fetchone()
2012 if row is None:
2013 raise LockOwnerError(
2014 name=name,
2015 owner_slot=owner,
2016 actual_owner_slot=None,
2017 )
2018 actual_owner = str(row["owner_slot"])
2019 if actual_owner != owner:
2020 raise LockOwnerError(
2021 name=name,
2022 owner_slot=owner,
2023 actual_owner_slot=actual_owner,
2024 )
2025 conn.execute(
2026 "UPDATE locks SET expires_at = ? WHERE name = ?",
2027 (expires_at, name),
2028 )
2029 return expires_at
2030
2031 def startup_sweep(self) -> int:
2032 return len(self.startup_sweep_with_names())
2033
2034 def startup_sweep_with_names(self) -> list[str]:
2035 """Remove expired lock rows and return their names."""
2036
2037 with self.transaction("immediate") as conn:
2038 now = self.clock.now_wall()
2039 names = [
2040 str(row["name"])
2041 for row in conn.execute(
2042 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2043 "SELECT name FROM locks "
2044 "WHERE expires_at IS NOT NULL AND expires_at < ? "
2045 "ORDER BY name",
2046 (now,),
2047 ).fetchall()
2048 ]
2049 conn.execute(
2050 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2051 "DELETE FROM locks WHERE expires_at IS NOT NULL AND expires_at < ?",
2052 (now,),
2053 )
2054 return names
2055
2056 def active_operator_slot(self) -> str | None:
2057 """Return the first active human operator slot, if one exists."""
2058
2059 with self.connect() as conn:
2060 row = conn.execute(
2061 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2062 "SELECT slot FROM agents "
2063 "WHERE slot_kind = 'operator' AND active = 1 "
2064 "ORDER BY slot LIMIT 1"
2065 ).fetchone()
2066 return None if row is None else str(row["slot"])
2067
2068 def actor_slot_kind(self, actor_slot: str) -> str:
2069 """Return an actor's slot kind or raise ``UnknownActorError``."""
2070
2071 with self.connect() as conn:
2072 return self._actor_slot_kind(conn, actor_slot)
2073
2074 def credential_fingerprints(self) -> list[dict[str, object]]:
2075 """Return credential rows needed for doctor fingerprint checks."""
2076
2077 with self.connect() as conn:
2078 rows = conn.execute(
2079 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2080 "SELECT hmac_key_id, slot, secret_fingerprint, rotation_phase, "
2081 "revoked_at FROM agent_credentials ORDER BY slot, rotation_phase"
2082 ).fetchall()
2083 return [
2084 {
2085 "hmac_key_id": str(row["hmac_key_id"]),
2086 "slot": str(row["slot"]),
2087 "secret_fingerprint": str(row["secret_fingerprint"]),
2088 "rotation_phase": str(row["rotation_phase"]),
2089 "revoked_at": row["revoked_at"],
2090 }
2091 for row in rows
2092 ]
2093
2094 def backup(self, target: str | Path) -> Path:
2095 target_path = Path(target)
2096 target_path.parent.mkdir(parents=True, exist_ok=True)
2097 with self.connect() as source, sqlite3.connect(str(target_path)) as dest:
2098 source.backup(dest)
2099 return target_path
2100
2101 def restore(self, source: str | Path) -> None:
2102 if self._is_memory:
2103 raise BackendCapabilityError("cannot restore over an in-memory database")
2104 source_path = Path(source)
2105 if not source_path.is_file():
2106 raise FileNotFoundError(source_path)
2109 shutil.copy2(source_path, self.path)
2111
2112 def integrity_check(self) -> dict[str, object]:
2113 errors: list[dict[str, object]] = []
2114 try:
2115 # conn closed explicitly: `with conn` alone never closes and the
2116 # leaked handle holds a WAL lock (breaks regenerate's swap).
2117 conn = self.connect()
2118 try:
2119 with conn:
2120 integrity_rows = [
2121 row[0] for row in conn.execute("PRAGMA integrity_check").fetchall()
2122 ]
2123 if integrity_rows != ["ok"]:
2124 errors.extend(
2125 {
2126 "kind": "physical_corruption",
2127 "details": str(row),
2128 }
2129 for row in integrity_rows
2130 )
2131 fk_rows = conn.execute("PRAGMA foreign_key_check").fetchall()
2132 errors.extend(_foreign_key_error(row) for row in fk_rows)
2133 self._check_append_only_triggers(conn, errors)
2134 finally:
2135 conn.close()
2136 except sqlite3.Error as exc:
2137 errors.append({"kind": "physical_corruption", "details": str(exc)})
2138 return {"ok": not errors, "errors": errors}
2139
2140 def vacuum(self) -> None:
2141 with self.connect() as conn:
2142 conn.execute("VACUUM")
2143
2144 def connection_pragmas(self) -> dict[str, object]:
2145 """Return the effective PRAGMA values for a fresh backend connection."""
2146
2147 # Closed explicitly: `with conn` never closes, and this runs in every
2148 # backend construction (capability assert) — the leaked handle held a
2149 # WAL lock that broke regenerate's os.replace swap under coverage.
2150 conn = self.connect()
2151 try:
2152 return self._read_pragmas(conn)
2153 finally:
2154 conn.close()
2155
2156 def _sqlite_sidecars(self) -> tuple[Path, Path]:
2157 return (
2158 self.path.with_name(f"{self.path.name}-wal"),
2159 self.path.with_name(f"{self.path.name}-shm"),
2160 )
2161
2162 def _checkpoint_for_restore(self) -> None:
2163 try:
2164 conn = self.connect()
2165 except sqlite3.Error:
2166 return
2167 try:
2168 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2169 conn.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchall()
2170 except sqlite3.Error:
2171 pass
2172 finally:
2173 conn.close()
2174
2175 def _delete_sqlite_sidecars(self) -> None:
2176 for sidecar in self._sqlite_sidecars():
2177 try:
2178 sidecar.unlink(missing_ok=True)
2179 except PermissionError:
2180 # A just-closed SQLite handle can retain the sidecar briefly on
2181 # Windows. The checkpoint above makes the sidecar non-authoritative.
2182 pass
2183
2184 def _assert_capabilities(self) -> None:
2185 if not self._is_memory:
2186 pragmas = self.connection_pragmas()
2187 if pragmas["foreign_keys"] != 1:
2188 raise BackendCapabilityError("SQLite foreign_keys pragma is not ON")
2189 if str(pragmas["journal_mode"]).lower() != "wal":
2190 raise BackendCapabilityError("SQLite journal_mode pragma is not WAL")
2191 if int(pragmas["busy_timeout"]) < 5000:
2192 raise BackendCapabilityError("SQLite busy_timeout is below 5000 ms")
2193 super()._assert_capabilities()
2194
2195 def _apply_connection_pragmas(self, conn: sqlite3.Connection) -> None:
2196 conn.execute("PRAGMA foreign_keys = ON")
2197 conn.execute("PRAGMA journal_mode = WAL")
2198 conn.execute("PRAGMA busy_timeout = 5000")
2199
2200 def _read_pragmas(self, conn: sqlite3.Connection) -> dict[str, object]:
2201 return {
2202 "foreign_keys": conn.execute("PRAGMA foreign_keys").fetchone()[0],
2203 "journal_mode": conn.execute("PRAGMA journal_mode").fetchone()[0],
2204 "busy_timeout": conn.execute("PRAGMA busy_timeout").fetchone()[0],
2205 }
2206
2207 def _run_schema_script(self, conn: sqlite3.Connection, *, commit: bool) -> None:
2209 roadmap_schema = v0_3_roadmap.load_schema_sql()
2210 auth_events_schema = v0_3_1_auth_events.migration_sql()
2211 source_fields_schema = v0_3_2_roadmap_source_fields.migration_sql()
2212 suffix = "\nCOMMIT;" if commit else ""
2213 try:
2214 conn.executescript(
2215 "BEGIN IMMEDIATE;\n"
2216 f"{schema}\n{roadmap_schema}\n{auth_events_schema}\n"
2217 f"{source_fields_schema}{suffix}"
2218 )
2219 except Exception:
2220 try:
2221 conn.execute("ROLLBACK")
2222 except sqlite3.Error:
2223 pass
2224 raise
2225
2226 def _seed_initial_rows(self, conn: sqlite3.Connection, seed: RegistrySeed) -> None:
2227 now = self.clock.now_wall()
2228 conn.execute(
2229 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2230 "INSERT INTO schema_migrations(version, applied_at, description) "
2231 "VALUES (?, ?, ?)",
2232 (SCHEMA_VERSION, now, SCHEMA_DESCRIPTION),
2233 )
2234 conn.execute(
2235 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2236 "INSERT OR IGNORE INTO schema_migrations(version, applied_at, description) "
2237 "VALUES (?, ?, ?)",
2238 (
2239 v0_3_roadmap.ROADMAP_SCHEMA_VERSION,
2240 now,
2241 v0_3_roadmap.ROADMAP_SCHEMA_DESCRIPTION,
2242 ),
2243 )
2244 conn.execute(
2245 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2246 "INSERT OR IGNORE INTO schema_migrations(version, applied_at, description) "
2247 "VALUES (?, ?, ?)",
2248 (
2249 v0_3_1_auth_events.AUTH_EVENTS_SCHEMA_VERSION,
2250 now,
2251 v0_3_1_auth_events.AUTH_EVENTS_SCHEMA_DESCRIPTION,
2252 ),
2253 )
2254 conn.execute(
2255 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2256 "INSERT OR IGNORE INTO schema_migrations(version, applied_at, description) "
2257 "VALUES (?, ?, ?)",
2258 (
2259 v0_3_2_roadmap_source_fields.ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION,
2260 now,
2261 v0_3_2_roadmap_source_fields.ROADMAP_SOURCE_FIELDS_SCHEMA_DESCRIPTION,
2262 ),
2263 )
2264 conn.execute(
2265 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2266 "INSERT INTO workspaces("
2267 "workspace_id, workspace_root, git_common_dir, registry_path, "
2268 "checkpoint_path, created_at, last_seen_at, active"
2269 ") VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
2270 (
2271 seed.workspace_id,
2272 seed.workspace_root,
2273 seed.git_common_dir,
2274 seed.registry_path,
2275 seed.checkpoint_path,
2276 now,
2277 now,
2278 ),
2279 )
2280 conn.execute(
2281 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2282 "INSERT INTO registry_meta("
2283 "id, registry_id, workspace_id, schema_version, state_version, "
2284 "last_checkpoint_id, created_at, updated_at"
2285 ") VALUES (1, ?, ?, ?, 0, NULL, ?, ?)",
2286 (seed.registry_id, seed.workspace_id, SCHEMA_VERSION, now, now),
2287 )
2288 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2289 conn.execute("INSERT INTO plan_seq(id, next_number) VALUES (1, 1)")
2290 conn.execute(
2291 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2292 "INSERT INTO agents("
2293 "slot, slot_kind, identity_kind, model_class, "
2294 "orchestrator_eligible, active, created_at"
2295 ") VALUES (?, 'operator', 'human', 'human', 0, 1, ?)",
2296 (seed.operator_slot, now),
2297 )
2298 conn.execute(
2299 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2300 "INSERT INTO orchestration_state("
2301 "id, orchestrator_slot, handover_status, ready_to_handover, "
2302 "updated_at, updated_by"
2303 ") VALUES (1, NULL, 'stable', 0, ?, ?)",
2304 (now, seed.operator_slot),
2305 )
2306 conn.execute(
2307 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2308 "INSERT INTO leases("
2309 "id, holder_slot, lease_expires_at, heartbeat_interval_sec, "
2310 "last_heartbeat_at"
2311 ") VALUES (1, NULL, NULL, 30, NULL)"
2312 )
2313 conn.executemany(
2314 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2315 "INSERT INTO settings(key, value, updated_at, updated_by) "
2316 "VALUES (?, ?, ?, ?)",
2317 [
2318 (key, value, now, seed.operator_slot)
2319 for key, value in _default_settings(seed.app_version)
2320 ],
2321 )
2322 conn.execute(
2323 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2324 "INSERT INTO agent_credentials("
2325 "hmac_key_id, slot, secret_fingerprint, created_at, rotation_phase"
2326 ") VALUES (?, ?, ?, ?, 'active')",
2327 (
2328 seed.hmac_key_id,
2329 seed.operator_slot,
2330 seed.secret_fingerprint,
2331 now,
2332 ),
2333 )
2334
2335 def _table_count(self, conn: sqlite3.Connection) -> int:
2336 row = conn.execute(
2337 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2338 "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'"
2339 ).fetchone()
2340 return int(row[0])
2341
2343 self,
2344 conn: sqlite3.Connection,
2345 errors: list[dict[str, object]],
2346 ) -> None:
2347 table_row = conn.execute(
2348 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2349 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
2350 ("operation_log",),
2351 ).fetchone()
2352 if table_row is None:
2353 return
2354 trigger_rows = conn.execute(
2355 "SELECT name FROM sqlite_master WHERE type = 'trigger'"
2356 ).fetchall()
2357 triggers = {str(row[0]) for row in trigger_rows}
2358 required = {"operation_log_no_update", "operation_log_no_delete"}
2359 missing = sorted(required - triggers)
2360 if missing:
2361 errors.append(
2362 {
2363 "kind": "append_only_trigger",
2364 "details": f"missing append-only triggers: {', '.join(missing)}",
2365 "missing": missing,
2366 }
2367 )
2368 roadmap_table = conn.execute(
2369 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2370 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
2371 ("roadmap_item_events",),
2372 ).fetchone()
2373 if roadmap_table is None:
2374 return
2375 roadmap_required = {
2376 "roadmap_item_events_no_update",
2377 "roadmap_item_events_no_delete",
2378 }
2379 roadmap_missing = sorted(roadmap_required - triggers)
2380 if roadmap_missing:
2381 errors.append(
2382 {
2383 "kind": "append_only_trigger",
2384 "details": "missing roadmap append-only triggers: "
2385 + ", ".join(roadmap_missing),
2386 "missing": roadmap_missing,
2387 }
2388 )
2389
2391 """Apply the v0.3 roadmap extension to initialized registries."""
2392
2393 if self._is_memory:
2394 return
2395 # conn closed explicitly (leak held a WAL lock); the inner `with conn`
2396 # keeps the commit-on-success semantics the migration stamps rely on.
2397 conn = self.connect()
2398 try:
2399 self._auto_migrate_in_conn(conn)
2400 finally:
2401 conn.close()
2402
2403 def _auto_migrate_in_conn(self, conn: sqlite3.Connection) -> None:
2404 with conn:
2405 if self._table_count(conn) == 0:
2406 return
2407 row = conn.execute(
2408 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2409 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
2410 ("registry_meta",),
2411 ).fetchone()
2412 if row is None:
2413 return
2414 now = self.clock.now_wall()
2415 if not (
2416 v0_3_roadmap.current_user_version(conn)
2417 >= v0_3_roadmap.ROADMAP_SCHEMA_VERSION
2418 and v0_3_roadmap.tables_exist(conn)
2419 ):
2420 conn.executescript(
2421 "BEGIN IMMEDIATE;\n"
2422 + v0_3_roadmap.load_schema_sql()
2423 + "\nCOMMIT;"
2424 )
2425 conn.execute(
2426 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2427 "INSERT OR IGNORE INTO schema_migrations("
2428 "version, applied_at, description"
2429 ") VALUES (?, ?, ?)",
2430 (
2431 v0_3_roadmap.ROADMAP_SCHEMA_VERSION,
2432 now,
2433 v0_3_roadmap.ROADMAP_SCHEMA_DESCRIPTION,
2434 ),
2435 )
2436 if not v0_3_1_auth_events.is_applied(conn):
2437 v0_3_1_auth_events.apply(conn)
2438 conn.execute(
2439 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2440 "INSERT OR IGNORE INTO schema_migrations("
2441 "version, applied_at, description"
2442 ") VALUES (?, ?, ?)",
2443 (
2444 v0_3_1_auth_events.AUTH_EVENTS_SCHEMA_VERSION,
2445 now,
2446 v0_3_1_auth_events.AUTH_EVENTS_SCHEMA_DESCRIPTION,
2447 ),
2448 )
2449 elif (
2450 v0_3_1_auth_events.current_user_version(conn)
2451 < v0_3_1_auth_events.AUTH_EVENTS_SCHEMA_VERSION
2452 ):
2453 conn.execute(
2454 f"PRAGMA user_version = "
2455 f"{v0_3_1_auth_events.AUTH_EVENTS_SCHEMA_VERSION}"
2456 )
2457 if not v0_3_2_roadmap_source_fields.is_applied(conn):
2458 v0_3_2_roadmap_source_fields.apply(conn)
2459 conn.execute(
2460 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2461 "INSERT OR IGNORE INTO schema_migrations("
2462 "version, applied_at, description"
2463 ") VALUES (?, ?, ?)",
2464 (
2465 v0_3_2_roadmap_source_fields.ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION,
2466 now,
2467 v0_3_2_roadmap_source_fields.ROADMAP_SOURCE_FIELDS_SCHEMA_DESCRIPTION,
2468 ),
2469 )
2470 elif (
2471 v0_3_1_auth_events.current_user_version(conn)
2472 < v0_3_2_roadmap_source_fields.ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION
2473 ):
2474 conn.execute(
2475 f"PRAGMA user_version = "
2476 f"{v0_3_2_roadmap_source_fields.ROADMAP_SOURCE_FIELDS_SCHEMA_VERSION}"
2477 )
2478 conn.commit()
2479
2481 self,
2482 operation_type: str,
2483 payload: Mapping[str, object],
2484 payload_version: int,
2485 ) -> str:
2486 if not isinstance(operation_type, str) or not operation_type.strip():
2488 operation_type=str(operation_type),
2489 message="operation_type must be a non-empty string",
2490 )
2491 if not isinstance(payload, Mapping):
2493 operation_type=operation_type,
2494 message="payload must be a mapping",
2495 )
2496 if "version" not in payload:
2498 operation_type=operation_type,
2499 message="payload must include version",
2500 )
2501 supplied_version = payload["version"]
2502 if not isinstance(supplied_version, int) or supplied_version != payload_version:
2504 operation_type=operation_type,
2505 payload_version=payload_version,
2506 supplied_version=supplied_version,
2507 )
2508 try:
2509 return canonical_json(payload)
2510 except (TypeError, ValueError) as exc:
2512 operation_type=operation_type,
2513 message=str(exc),
2514 ) from exc
2515
2516 def _actor_slot_kind(self, conn: sqlite3.Connection, actor_slot: str) -> str:
2517 row = conn.execute(
2518 "SELECT slot_kind FROM agents WHERE slot = ?",
2519 (actor_slot,),
2520 ).fetchone()
2521 if row is None:
2522 raise UnknownActorError(actor_slot)
2523 return str(row["slot_kind"])
2524
2526 self,
2527 conn: sqlite3.Connection,
2528 actor_slot: str,
2529 ) -> str | None:
2530 row = conn.execute(
2531 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2532 "SELECT hmac_key_id FROM agent_credentials "
2533 "WHERE slot = ? AND revoked_at IS NULL "
2534 "ORDER BY CASE rotation_phase "
2535 "WHEN 'rotating_new' THEN 0 "
2536 "WHEN 'active' THEN 1 "
2537 "WHEN 'rotating_old' THEN 2 "
2538 "ELSE 3 END LIMIT 1",
2539 (actor_slot,),
2540 ).fetchone()
2541 return None if row is None else str(row["hmac_key_id"])
2542
2544 self,
2545 conn: sqlite3.Connection,
2546 operation_type: str,
2547 actor_slot: str,
2548 slot_kind: str,
2549 ) -> None:
2550 if not operation_type.startswith("round.") or slot_kind != "orchestrator":
2551 return
2552 state = self._orchestration_state(conn)
2553 holder = state["orchestrator_slot"]
2554 status = str(state["handover_status"])
2555 outgoing = state["handover_outgoing"]
2556 incoming = state["handover_incoming"]
2557 if status == "stable":
2558 if holder is None:
2559 conn.execute(
2560 "UPDATE orchestration_state SET orchestrator_slot = ?, "
2561 "updated_at = ?, updated_by = ? WHERE id = 1",
2562 (actor_slot, self.clock.now_wall(), actor_slot),
2563 )
2564 return
2565 if str(holder) != actor_slot:
2566 raise HandoverStateError(
2567 state="handover_in_flight",
2568 actor_slot=actor_slot,
2569 operation_type=operation_type,
2570 )
2571 return
2572 if outgoing is not None and str(outgoing) == actor_slot:
2573 return
2574 state_name = (
2575 "pending_not_accepted"
2576 if incoming is not None and str(incoming) == actor_slot
2577 else "handover_in_flight"
2578 )
2579 raise HandoverStateError(
2580 state=state_name,
2581 actor_slot=actor_slot,
2582 operation_type=operation_type,
2583 )
2584
2585 def _orchestration_state(self, conn: sqlite3.Connection) -> sqlite3.Row:
2586 row = conn.execute(
2587 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2588 "SELECT orchestrator_slot, handover_status, handover_outgoing, "
2589 "handover_incoming, handover_requested_at FROM orchestration_state "
2590 "WHERE id = 1"
2591 ).fetchone()
2592 if row is None:
2593 raise BackendCapabilityError("orchestration_state singleton row is missing")
2594 return row
2595
2597 self,
2598 conn: sqlite3.Connection,
2599 parent_operation_id: int,
2600 operation_uuid: str,
2601 ) -> None:
2602 seen: set[int] = set()
2603 current_id: int | None = parent_operation_id
2604 depth = 0
2605 while current_id is not None:
2606 if current_id in seen or depth >= _MAX_PARENT_DEPTH:
2607 raise ParentCycleError(parent_operation_id, operation_uuid)
2608 seen.add(current_id)
2609 row = conn.execute(
2610 "SELECT operation_uuid, parent_operation_id "
2611 "FROM operation_log WHERE id = ?",
2612 (current_id,),
2613 ).fetchone()
2614 if row is None:
2615 return
2616 if row["operation_uuid"] == operation_uuid:
2617 raise ParentCycleError(parent_operation_id, operation_uuid)
2618 current_id = row["parent_operation_id"]
2619 depth += 1
2620
2621 def _current_state_version(self, conn: sqlite3.Connection) -> int:
2622 row = conn.execute(
2623 "SELECT state_version FROM registry_meta WHERE id = 1"
2624 ).fetchone()
2625 if row is None:
2626 raise BackendCapabilityError("registry_meta singleton row is missing")
2627 return int(row["state_version"])
2628
2629 def _lock_holder(self, name: str) -> sqlite3.Row | None:
2630 with self.connect() as conn:
2631 return conn.execute(
2632 # OCT-LINT: disable=no_hardcoded_secrets reason="Credential-related column names are schema fields, not embedded secrets"
2633 "SELECT owner_slot, expires_at FROM locks WHERE name = ?",
2634 (name,),
2635 ).fetchone()
2636
2637
2638def _mapping_list(value: object) -> list[Mapping[str, object]]:
2639 if value is None:
2640 return []
2641 if not isinstance(value, list):
2642 raise TypeError("migration projection sections must be lists")
2643 rows: list[Mapping[str, object]] = []
2644 for item in value:
2645 if not isinstance(item, Mapping):
2646 raise TypeError("migration projection rows must be mappings")
2647 rows.append(item)
2648 return rows
2649
2650
2651def _required_mapping(row: Mapping[str, object], key: str) -> Mapping[str, object]:
2652 value = row.get(key)
2653 if not isinstance(value, Mapping):
2654 raise TypeError(f"migration projection row missing mapping: {key}")
2655 return value
2656
2657
2658def _row_dict(row: sqlite3.Row) -> dict[str, object]:
2659 return {key: row[key] for key in row.keys()}
2660
2661
2662def _optional_str(value: object) -> str | None:
2663 if value is None:
2664 return None
2665 text = str(value)
2666 return text if text else None
2667
2668
2669def _optional_int(value: object) -> int | None:
2670 return None if value is None else int(value)
2671
2672
2673def _bool_int(value: object) -> int:
2674 return 1 if bool(value) else 0
2675
2676
2677def _phase_code_for_item_id(item_id: str) -> str | None:
2678 if not item_id.startswith("M"):
2679 return None
2680 if "." not in item_id:
2681 return item_id
2682 return item_id.split(".", 1)[0]
2683
2684
2685def _parent_phase_code(phase_code: str) -> str | None:
2686 if "." not in phase_code:
2687 return None
2688 return phase_code.split(".", 1)[0]
2689
2690
2691def _phase_sort_order(phase_code: str) -> int:
2692 digits = "".join(ch if ch.isdigit() else " " for ch in phase_code).split()
2693 if not digits:
2694 return 999000
2695 value = int(digits[0]) * 1000
2696 if len(digits) > 1:
2697 value += int(digits[1])
2698 return value
2699
2700
2701def _era_id_for_version(version_id: str) -> str:
2702 major = version_id.removeprefix("v").split(".", 1)[0]
2703 return f"v{major}"
2704
2705
2706def _era_sort_order(era_id: str) -> int:
2707 text = era_id.removeprefix("v")
2708 return int(text) if text.isdigit() else 999
2709
2710
2712 return (
2713 Path(__file__).resolve().parents[2]
2714 / "docs"
2715 / "architecture"
2716 / "orchestration"
2717 / "REGISTRY_SCHEMA_V0_2_3.sql"
2718 )
2719
2720
2722 return canonical_schema_path().read_text(encoding="utf-8")
2723
2724
2726 lines = []
2727 for line in load_canonical_schema_sql().splitlines():
2728 if line.lstrip().upper().startswith(_PRAGMA_PREFIX):
2729 continue
2730 lines.append(line)
2731 return "\n".join(lines) + "\n"
2732
2733
2734def _foreign_key_error(row: sqlite3.Row) -> dict[str, object]:
2735 values = tuple(row)
2736 table = "" if len(values) < 1 else str(values[0])
2737 row_id = None if len(values) < 2 else values[1]
2738 parent = "" if len(values) < 3 else str(values[2])
2739 fkid = None if len(values) < 4 else values[3]
2740 return {
2741 "kind": "fk_violation",
2742 "table": table,
2743 "row": row_id,
2744 "parent": parent,
2745 "fkid": fkid,
2746 "details": repr(values),
2747 }
2748
2749
2750def _default_settings(app_version: str) -> list[tuple[str, str]]:
2751 return [
2752 ("workflow_version", "1"),
2753 ("renderer_version", "1"),
2754 ("roadmap_version", "1"),
2755 ("app_version", app_version),
2756 ("hmac_max_skew_seconds", "300"),
2757 ("hmac_rotation_grace_seconds", "86400"),
2758 ("round_close_timeout_seconds", "3600"),
2759 ("round_auto_batch_limit", "5"),
2760 ("handover_drain_timeout_seconds", "300"),
2761 ("grant_default_ttl_seconds", "3600"),
2762 ("max_replay_rows_for_merge", "10000"),
2763 ("oplog_retention_days", "90"),
2764 ("auth_events_retention_days", "30"),
2765 ("auth_failure_alert_threshold_per_hour", "10"),
2766 ]
AbstractContextManager[Any] transaction(self, str mode="immediate")
Definition base.py:128
str|None renew_lock(self, str name, str owner, int ttl_seconds)
Definition base.py:177
None release_lock(self, str name, str owner)
Definition base.py:173
None _upsert_migration_round_plan(self, sqlite3.Connection conn, Mapping[str, object] round_plan)
Definition sqlite.py:1925
None _project_round_test(self, sqlite3.Connection conn, Mapping[str, object] payload)
Definition sqlite.py:1518
int _table_count(self, sqlite3.Connection conn)
Definition sqlite.py:2335
bool _plan_exists(self, sqlite3.Connection conn, str plan_id)
Definition sqlite.py:1319
None upsert_round_job_step(self, *, str job_id, int step_no, str step_name, str status, str|None input_hash=None, str|None post_step_head_sha=None, str|None result_summary=None, str|None error_summary=None, str|None artifact_paths_json=None, str|None started_at=None, str|None finished_at=None)
Definition sqlite.py:1728
int _roadmap_phase_id(self, sqlite3.Connection conn, str phase_code, *, str ts, str label)
Definition sqlite.py:1248
None initialize(self, RegistrySeed seed)
Definition sqlite.py:155
None _project_round_lint(self, sqlite3.Connection conn, Mapping[str, object] payload)
Definition sqlite.py:1547
dict[str, object] _read_pragmas(self, sqlite3.Connection conn)
Definition sqlite.py:2200
dict[str, object] migration_snapshot(self)
Definition sqlite.py:335
None _upsert_migration_agent(self, sqlite3.Connection conn, Mapping[str, object] agent)
Definition sqlite.py:1394
dict[str, object]|None get_round_plan(self, int round_no, str plan_id)
Definition sqlite.py:1804
str _actor_slot_kind(self, sqlite3.Connection conn, str actor_slot)
Definition sqlite.py:2516
None _run_schema_script(self, sqlite3.Connection conn, *, bool commit)
Definition sqlite.py:2207
list[str] migration_null_round_backfill_violations(self)
Definition sqlite.py:392
None _project_roadmap_item_create(self, sqlite3.Connection conn, Mapping[str, object] payload, str actor_slot, str ts, int source_operation_id)
Definition sqlite.py:935
None _apply_domain_projection(self, sqlite3.Connection conn, str operation_type, Mapping[str, object] payload, *, str actor_slot, str ts, int|None related_round, int source_operation_id)
Definition sqlite.py:538
None _project_round_merge(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:1481
None _project_round_cleanup(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:1578
None _check_append_only_triggers(self, sqlite3.Connection conn, list[dict[str, object]] errors)
Definition sqlite.py:2346
None _insert_roadmap_event(self, sqlite3.Connection conn, str item_id, str event_kind, *, str|None from_value_json, str|None to_value_json, str|None in_version_id, int source_operation_id, str ts, str|None note)
Definition sqlite.py:1300
None _project_round_ff_main(self, sqlite3.Connection conn, Mapping[str, object] payload)
Definition sqlite.py:1569
list[dict[str, object]] credential_fingerprints(self)
Definition sqlite.py:2074
dict[str, object]|None get_round(self, int round_no)
Definition sqlite.py:1779
None _auto_migrate_in_conn(self, sqlite3.Connection conn)
Definition sqlite.py:2403
str actor_slot_kind(self, str actor_slot)
Definition sqlite.py:2068
list[dict[str, object]] list_round_job_steps(self, str job_id)
Definition sqlite.py:1768
None _project_queue_complete(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts, int|None related_round)
Definition sqlite.py:913
None _project_plan_set_status(self, sqlite3.Connection conn, Mapping[str, object] payload, str actor_slot, str ts)
Definition sqlite.py:736
None _shift_queue_positions_up(self, sqlite3.Connection conn, str slot, int start_position, *, str|None exclude_plan_id=None)
Definition sqlite.py:1344
None _project_round_consolidate(self, sqlite3.Connection conn, Mapping[str, object] payload)
Definition sqlite.py:1556
dict[str, object] connection_pragmas(self)
Definition sqlite.py:2144
LockHandle acquire_lock(self, str name, str owner, int ttl_seconds, Mapping[str, object]|None metadata=None)
Definition sqlite.py:1953
None set_round_job_head_sha(self, str job_id, str|None sha)
Definition sqlite.py:1702
None _assert_no_parent_cycle(self, sqlite3.Connection conn, int parent_operation_id, str operation_uuid)
Definition sqlite.py:2601
None _project_roadmap_section_upsert(self, sqlite3.Connection conn, Mapping[str, object] payload)
Definition sqlite.py:1181
None __init__(self, str|Path path, Clock|None clock=None, *, bool check_capabilities=True)
Definition sqlite.py:128
None _project_queue_dequeue(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:883
None _project_roadmap_phase_complete(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts, int source_operation_id)
Definition sqlite.py:1144
None _remove_queue_position(self, sqlite3.Connection conn, str slot, int position, str plan_id)
Definition sqlite.py:1371
list[dict[str, object]] list_round_plans(self, int round_no)
Definition sqlite.py:1789
None _apply_connection_pragmas(self, sqlite3.Connection conn)
Definition sqlite.py:2195
object append_operation_log(self, Mapping[str, object] entry)
Definition sqlite.py:1944
None _project_plan_reimport(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:818
sqlite3.Row|None _lock_holder(self, str name)
Definition sqlite.py:2629
None _upsert_migration_plan(self, sqlite3.Connection conn, Mapping[str, object] plan)
Definition sqlite.py:1847
None _project_roadmap_item_set_status(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts, int source_operation_id)
Definition sqlite.py:1026
None _project_round_abort(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:1608
None _project_queue_enqueue(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:836
None _project_plan_create(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:686
None _insert_migration_status_event(self, sqlite3.Connection conn, Mapping[str, object] plan, str actor_slot)
Definition sqlite.py:1906
None _project_round_open(self, sqlite3.Connection conn, Mapping[str, object] payload, str actor_slot, str ts)
Definition sqlite.py:1430
None set_round_job_status(self, str job_id, *, str status, str|None current_step=None, bool update_step=True)
Definition sqlite.py:1684
sqlite3.Row _orchestration_state(self, sqlite3.Connection conn)
Definition sqlite.py:2585
Path backup(self, str|Path target)
Definition sqlite.py:2094
None release_lock(self, str name, str owner)
Definition sqlite.py:1986
int _current_state_version(self, sqlite3.Connection conn)
Definition sqlite.py:2621
None _project_roadmap_plan_link_create(self, sqlite3.Connection conn, Mapping[str, object] payload, str actor_slot, str ts)
Definition sqlite.py:1211
str|None _active_hmac_key_id(self, sqlite3.Connection conn, str actor_slot)
Definition sqlite.py:2529
None _roadmap_era(self, sqlite3.Connection conn, str era_id, str ts)
Definition sqlite.py:1231
OperationLogRow _execute_domain_command_in_conn(self, sqlite3.Connection conn, str operation_type, str actor_slot, Mapping[str, object] payload, *, str actor_type, int payload_version=1, str|None command=None, int|None parent_operation_id=None, str|None related_plan_id=None, int|None related_round=None, str|None operation_uuid=None, str|None hmac_key_id=None, bool system_actor=False)
Definition sqlite.py:436
str|None renew_lock(self, str name, str owner, int ttl_seconds)
Definition sqlite.py:2003
None _seed_initial_rows(self, sqlite3.Connection conn, RegistrySeed seed)
Definition sqlite.py:2226
dict[str, object]|None latest_round_job(self, int round_no, *, tuple[str,...]|None statuses=None)
Definition sqlite.py:1663
None _project_roadmap_item_add_dependency(self, sqlite3.Connection conn, Mapping[str, object] payload, str actor_slot, str ts, int source_operation_id)
Definition sqlite.py:1069
None create_round_job(self, *, str job_id, int round_no, str created_by, str|None staging_branch_head_sha=None, str|None deadline_at=None)
Definition sqlite.py:1627
None _upsert_migration_round(self, sqlite3.Connection conn, Mapping[str, object] round_row)
Definition sqlite.py:1818
None _project_roadmap_version_bump(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:1097
None _project_plan_supersede(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:801
str _validate_payload(self, str operation_type, Mapping[str, object] payload, int payload_version)
Definition sqlite.py:2485
dict[str, object]|None get_round_job(self, str job_id)
Definition sqlite.py:1648
Iterator[sqlite3.Connection] transaction(self, str mode="immediate")
Definition sqlite.py:176
None _project_queue_move(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:861
None _project_handover_start(self, sqlite3.Connection conn, Mapping[str, object] payload, str actor_slot, str ts)
Definition sqlite.py:620
tuple[str, int]|None _plan_queue_location(self, sqlite3.Connection conn, str plan_id)
Definition sqlite.py:1327
None _project_handover_complete(self, sqlite3.Connection conn, Mapping[str, object] payload, str actor_slot, str ts)
Definition sqlite.py:647
None _assert_handover_allows(self, sqlite3.Connection conn, str operation_type, str actor_slot, str slot_kind)
Definition sqlite.py:2549
None restore(self, str|Path source)
Definition sqlite.py:2101
int _next_round_merge_order(self, sqlite3.Connection conn, int round_no)
Definition sqlite.py:1467
OperationLogRow execute_domain_command(self, str operation_type, str actor_slot, Mapping[str, object] payload, *, str actor_type, int payload_version=1, str|None command=None, int|None parent_operation_id=None, str|None related_plan_id=None, int|None related_round=None, str|None operation_uuid=None, str|None hmac_key_id=None, bool system_actor=False)
Definition sqlite.py:212
object read_model(self, str name, Mapping[str, object]|None filters=None)
Definition sqlite.py:194
None _project_plan_assign(self, sqlite3.Connection conn, Mapping[str, object] payload, str ts)
Definition sqlite.py:779
list[OperationLogRow] apply_migration_projection(self, Mapping[str, object] projection, *, str actor_slot, str actor_type)
Definition sqlite.py:237
Mapping[str, object] _required_mapping(Mapping[str, object] row, str key)
Definition sqlite.py:2651
int _era_sort_order(str era_id)
Definition sqlite.py:2706
str|None _parent_phase_code(str phase_code)
Definition sqlite.py:2685
int|None _optional_int(object value)
Definition sqlite.py:2669
int _phase_sort_order(str phase_code)
Definition sqlite.py:2691
list[Mapping[str, object]] _mapping_list(object value)
Definition sqlite.py:2638
str|None _optional_str(object value)
Definition sqlite.py:2662
dict[str, object] _row_dict(sqlite3.Row row)
Definition sqlite.py:2658
list[tuple[str, str]] _default_settings(str app_version)
Definition sqlite.py:2750
str _era_id_for_version(str version_id)
Definition sqlite.py:2701
str|None _phase_code_for_item_id(str item_id)
Definition sqlite.py:2677
dict[str, object] _foreign_key_error(sqlite3.Row row)
Definition sqlite.py:2734
int _bool_int(object value)
Definition sqlite.py:2673