7Registry-side listener for the dispatch layer's plan-state events.
11Plan 257 P1 (A7, cut 1). This module owns the legacy-compat mirroring
12that used to live inside ``oc_chat/scripts/plan_status.py``: opening the
13orchestration registry, taking the legacy-compat write lock, and
14replaying a worker's ``.PLAN_STATUS.md`` lifecycle into the ``plans``
15table. The code was moved here verbatim, not rewritten -- the point of
16the move is that the dispatch layer stops naming the registry, not that
17the mirroring behaves differently.
21- :class:`RegistryMirrorObserver` -- implements
22 :class:`oc_chat.core.state_observer.StateObserver` against the live
23 orchestration registry.
24- :func:`compat_context` -- the fail-soft registry handle the mirror
25 runs on: ``None`` when there is no registry to write to.
31 L2 - lifecycle events (mirror attempts, drift reports)
32 L3 - semantic details (skipped mirrors, absent registry)
33 L4 - deep tracing (module load)
37- Behaviour is byte-identical to the pre-257 ``plan_status`` mirror: the
38 same ``REGISTRY DRIFT - <operation> failed: <Type>: <msg>. Run `oct
39 agent migrate verify`.`` line on stderr, the same return code ``2``,
40 and the same silent ``0`` when no registry file exists.
41- A missing registry is not an error. A registry that exists but has no
42 active operator slot IS -- that refusal is deliberately re-raised out
43 of :func:`compat_context` and reported as drift.
44- No exception escapes a callback. Every failure becomes the drift line
45 plus return code ``2``.
46- The registry handle is injectable so a witness can drive every branch
47 without a SQLite file.
51- oc_diagnostics (structured debug logging via _dbg)
52- oc_chat.registry (lazy; Registry facade opened by compat_context)
53- oc_chat.registry.backends.sqlite (lazy; SQLiteBackend and the active operator slot in compat_context)
54- oc_chat.registry.discovery (lazy; discover_registry_location in compat_context)
55- oc_chat.registry.domain (lazy; plan_create, plan_assign and plan_set_status writers for the mirrors)
56- oc_chat.scripts.plan_status (lazy; _plan_number for the plan_created mirror)
59from __future__
import annotations
62from typing
import Callable
64from oc_diagnostics
import _dbg
66_dbg(
"OC_CHAT.MAIN",
"module",
"mirror_observer loaded", 4)
72CompatContext = Callable[[],
"tuple[object, object, str] | None"]
76 """Open the discovered orchestration registry, or return ``None``.
78 Moved verbatim from ``plan_status._compat_context`` (plan 257 P1).
79 The broad ``except`` is deliberate and pre-existing: a worker must be
80 able to record its own status in a checkout that has no registry at
81 all. The one refusal that is NOT swallowed is a registry that exists
82 but has no active operator slot, because writing unattributed rows
83 would be worse than refusing.
85 func =
"compat_context"
91 location = discover_registry_location()
92 if not location.registry_path.is_file():
94 backend =
SQLiteBackend(location.registry_path, check_capabilities=
False)
95 actor_slot = backend.active_operator_slot()
96 if actor_slot
is None:
97 raise RuntimeError(
"no active operator slot found")
98 return Registry(backend), backend, actor_slot
99 except Exception
as exc:
100 if "no active operator slot" in str(exc):
102 _dbg(
"OC_CHAT.MAIN", func, f
"no registry to mirror into: {exc}", 3)
107 """True when *plan_id* already has a row in the registry."""
112 """The registry's current status for *plan_id*, or ``None``."""
114 return None if row
is None else str(row[
"status"])
118 """The registry's ``plans`` row for *plan_id*, or ``None``."""
119 connect = getattr(getattr(registry,
"backend",
None),
"connect",
None)
122 with connect()
as conn:
125 "SELECT status, assigned_slot, queue_position, branch, worktree "
126 "FROM plans WHERE plan_id = ?",
133 """Mirrors dispatch-layer plan events into the orchestration registry.
135 The status tool registers one of these by default, so a worker that
136 changes nothing sees exactly the pre-257 behaviour. Passing
137 ``context_factory`` swaps the registry handle, which is how the
138 witnesses drive the drift and skip branches without a SQLite file.
141 def __init__(self, *, context_factory: CompatContext |
None =
None) ->
None:
149 plan_type: str |
None,
152 body_observed_at: str |
None,
160 """Create the plan row, re-anchor its assignment, set its status."""
161 func =
"plan_created"
162 _dbg(
"OC_CHAT.MAIN", func, f
"mirroring init for {plan_id}", 2)
164 def _mirror(registry: object, actor_slot: str) ->
None:
177 plan_number=_plan_number(plan_id),
181 body_sha256=body_sha256,
182 body_observed_at=body_observed_at,
183 model_override=model,
184 thinking_override=thinking,
185 session_override=session,
186 actor_slot=actor_slot,
187 actor_type=
"human_operator",
188 command=
"plan_status init",
193 and plan_row[
"assigned_slot"]
is not None
194 and plan_row[
"queue_position"]
is not None
196 str(plan_row[
"branch"]
or "") != branch
197 or str(plan_row[
"worktree"]
or "") != worktree
203 from_slot=str(plan_row[
"assigned_slot"]),
204 to_slot=str(plan_row[
"assigned_slot"]),
207 queue_position=int(plan_row[
"queue_position"]),
208 actor_slot=actor_slot,
209 actor_type=
"human_operator",
210 command=
"plan_status init",
213 if target_status != from_status:
217 from_status=from_status,
218 to_status=target_status,
219 source=
"worker_self",
221 "legacy plan_status init"
222 if target_status
in {
"partial",
"failed"}
225 actor_slot=actor_slot,
226 actor_type=
"human_operator",
227 command=
"plan_status init",
230 return self.
_run(
"plan_status.init", _mirror)
240 """Record one status transition against the registry."""
241 func =
"status_changed"
242 _dbg(
"OC_CHAT.MAIN", func, f
"mirroring {from_status} -> {to_status}", 2)
244 def _mirror(registry: object, actor_slot: str) ->
None:
250 from_status=from_status,
252 source=
"worker_self",
254 actor_slot=actor_slot,
255 actor_type=
"human_operator",
256 command=
"plan_status set",
259 return self.
_run(
"plan.set_status", _mirror)
264 callback: Callable[[object, str],
None],
266 """Take the legacy-compat lock and run *callback*; report drift.
268 Moved verbatim from ``plan_status._mirror_plan_status``.
275 registry, _backend, actor_slot = context
276 lock = registry.acquire_lock(
277 "legacy-compat-write",
280 {
"source":
"plan_status",
"operation_type": operation_type},
283 callback(registry, actor_slot)
287 except Exception
as exc:
294 f
"EXTERNAL_ERROR: {operation_type}: {type(exc).__name__}: {exc}",
298 f
"REGISTRY DRIFT - {operation_type} failed: {type(exc).__name__}: {exc}. "
299 "Run `oct agent migrate verify`.",
307 "RegistryMirrorObserver",
309 "registry_plan_exists",
311 "registry_plan_status",
CompatContext _context_factory
int status_changed(self, *, str plan_id, str from_status, str to_status, str|None note)
int _run(self, str operation_type, Callable[[object, str], None] callback)
int plan_created(self, *, str plan_id, str title, str|None plan_type, str body_path, str body_sha256, str|None body_observed_at, str model, str thinking, str session, str branch, str worktree, str target_status)
None __init__(self, *, CompatContext|None context_factory=None)
str|None registry_plan_status(object registry, str plan_id)
tuple[object, object, str]|None compat_context()
bool registry_plan_exists(object registry, str plan_id)
object|None registry_plan_row(object registry, str plan_id)