oc_chat
Loading...
Searching...
No Matches
handover.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/auth/handover.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_handover.py"
6"""
7Orchestrator handover state machine.
8
9Purpose
10-------
11Provide the M11.5 audited handover operations that prevent two
12orchestrators from writing round state at the same time.
13
14Responsibilities
15----------------
16- Emit ``role.handover.start`` and ``role.handover.complete`` operations.
17- Recover the start payload needed by accept calls.
18- Keep durable state projection inside the SQLite backend.
19
20Diagnostics
21-----------
22Domain: OC_CHAT.MAIN
23Levels:
24 L2 - lifecycle events
25 L3 - semantic details
26 L4 - deep tracing
27
28Contracts
29---------
30- ``role.handover.start`` enters the requested/draining state.
31- ``role.handover.complete`` makes the incoming orchestrator the holder.
32- Round writes are rejected unless the durable state permits the actor.
33
34Dependencies
35------------
36- json (decode the stored handover-start arguments)
37- oc_chat.registry.api (Registry that executes the handover domain commands)
38- oc_chat.registry.errors (HandoverStateError for an unknown handover)
39- oc_chat.registry.backends.base (OperationLogRow returned by both operations)
40- oc_diagnostics (structured debug logging via _dbg)
41"""
42from __future__ import annotations
43
44import json
45from typing import Mapping
46
47from oc_diagnostics import _dbg
48
49from oc_chat.registry.api import Registry
50from oc_chat.registry.backends.base import OperationLogRow
51from oc_chat.registry.errors import HandoverStateError
52
53
54_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
55
56
58 registry: Registry,
59 *,
60 from_slot: str,
61 to_slot: str,
62 reason: str,
63 actor_slot: str | None = None,
64 actor_type: str = "orchestrator_agent",
65 command: str | None = None,
66) -> OperationLogRow:
67 """Emit a handover-start operation."""
68
69 return registry.execute_domain_command(
70 "role.handover.start",
71 actor_slot or from_slot,
72 {
73 "version": 1,
74 "from_slot": from_slot,
75 "to_slot": to_slot,
76 "reason": reason,
77 },
78 actor_type=actor_type,
79 command=command or "handover.initiate",
80 )
81
82
84 registry: Registry,
85 *,
86 handover_id: int,
87 actor_slot: str,
88 actor_type: str = "orchestrator_agent",
89 command: str | None = None,
90) -> OperationLogRow:
91 """Emit a handover-complete operation for ``handover_id``."""
92
93 start = _handover_start(registry, handover_id)
94 if start is None:
96 state="unknown_handover",
97 actor_slot=actor_slot,
98 operation_type="role.handover.complete",
99 handover_id=handover_id,
100 )
101 from_slot = str(start["from_slot"])
102 to_slot = str(start["to_slot"])
103 started_at = str(start["started_at"])
104 return registry.execute_domain_command(
105 "role.handover.complete",
106 actor_slot,
107 {
108 "version": 1,
109 "from_slot": from_slot,
110 "to_slot": to_slot,
111 "started_at": started_at,
112 "completed_at": registry.clock.now_wall(),
113 "parent_operation_id": handover_id,
114 },
115 actor_type=actor_type,
116 command=command or "handover.accept",
117 parent_operation_id=handover_id,
118 )
119
120
122 registry: Registry,
123 handover_id: int,
124) -> Mapping[str, object] | None:
125 connect_func = getattr(registry.backend, "connect", None)
126 if connect_func is None:
127 return None
128 with connect_func() as conn:
129 row = conn.execute(
130 # OCT-LINT: disable=no_hardcoded_secrets reason="Static SQL projection and operation names are not secrets"
131 "SELECT ts, args_json FROM operation_log "
132 "WHERE id = ? AND operation_type = 'role.handover.start'",
133 (handover_id,),
134 ).fetchone()
135 if row is None:
136 return None
137 payload = json.loads(str(row["args_json"]))
138 if not isinstance(payload, dict):
139 return None
140 payload["started_at"] = row["ts"]
141 return payload
OperationLogRow accept_handover(Registry registry, *, int handover_id, str actor_slot, str actor_type="orchestrator_agent", str|None command=None)
Definition handover.py:90
OperationLogRow initiate_handover(Registry registry, *, str from_slot, str to_slot, str reason, str|None actor_slot=None, str actor_type="orchestrator_agent", str|None command=None)
Definition handover.py:66
Mapping[str, object]|None _handover_start(Registry registry, int handover_id)
Definition handover.py:124