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>
62 lines
2.1 KiB
Python
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
|