Files
nisse/nisse-dev/src/nisse_dev/reconcile.py
T
dongarandClaude Fable 5 18d4fcd701
CI / checks (push) Successful in 9s
CI / checks (pull_request) Successful in 8s
Implement A9 state-change logging in the state store
Retrofit for the conventions merged in #4 (issue #3):

- OpLog: JSON-lines emitter with the fixed envelope (ts, agent,
  event, task_id, cause); task_id set by the orchestrator at
  unit-of-work start
- StateStore owns emission: transition, intent begin/commit/abort,
  and schema migrations each emit their event right after the
  durable write commits; callers pass the cause
- reconcile_on_boot emits boot.reconcile with action and verdicts,
  and resolves intents with reconciliation-verdict causes
- Failed resolutions emit nothing (a line exists iff the write
  committed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 21:13:36 +00:00

62 lines
2.1 KiB
Python

"""Boot-time reconciliation (spec A3): turn an ambiguous crash into a safe,
known state.
Every pending intent found at startup is checked against external truth by a
registered reconciler — an observable check (query Gitea, inspect the tree),
never model self-assessment. Anything unverifiable gates to AWAIT_INPUT;
the agent never blind-resumes.
"""
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Literal
from nisse_dev.state import Intent, StateStore
# Returns True if the intent's external effect verifiably happened (do not
# repeat it), False if it verifiably never started (safe to discard), or
# None if it cannot be verified.
Reconciler = Callable[[Intent], bool | None]
@dataclass(frozen=True)
class BootReport:
action: Literal["resume", "await_input"]
notes: list[str]
def reconcile_on_boot(
store: StateStore, reconcilers: Mapping[str, Reconciler]
) -> BootReport:
notes: list[str] = []
unresolved = False
for intent in store.pending_intents():
label = f"intent {intent.id} ({intent.kind})"
reconciler = reconcilers.get(intent.kind)
if reconciler is None:
unresolved = True
notes.append(f"{label}: no reconciler registered")
continue
verdict = reconciler(intent)
if verdict is True:
store.commit_intent(
intent.id, cause="reconciliation verdict: effect verified"
)
notes.append(f"{label}: external effect verified, marked committed")
elif verdict is False:
store.abort_intent(
intent.id, cause="reconciliation verdict: never took effect"
)
notes.append(f"{label}: never took effect, marked aborted")
else:
unresolved = True
notes.append(f"{label}: unverifiable")
report = BootReport(
action="await_input" if unresolved else "resume",
notes=notes,
)
store.oplog.emit(
"boot.reconcile", cause="startup", action=report.action, verdicts=notes
)
return report