Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/flow/docs/VERIFIER_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,33 @@ A snapshot doesn't go through the ledger, so move to
call without a `write_key` counts as a new write. A cancellation the
caller sees isn't affected (`delivered=False`). `write_key` closes the
remaining gap for callers that have a stable task id.

### Final verdicts from history

A contract can set `frame.history` to read complete change feeds across its
record surfaces and declared collateral scope. Each surface maps to a feed;
additional feeds extend the collateral scope. The quiet window `quiet_s` is
chosen from server behavior observed during sandbox setup, and
`setup_evidence_sha256` pins that observation.

FHIR feeds read every page of the type-level `_history?_since=t0`, including
deletions and earlier versions. SQL feeds read CDC or audit events and an
independent coverage query in one read-only transaction. The coverage query
returns `retained_since`, `through`, and `event_count`; it must describe the
source's complete retained history, independently of the event query. The
SQL event query returns `event_id`, `record_id`, `operation`, `author`, and
`occurred_at`. Both time columns and event times include a timezone.

A confirmed initial check has `provisional: true` and `final: false` while its
recheck is owed. A consumer that requires a final verdict calls
`recheck(contract, result, params=params, actor=actor, wait=True)`. The ledger
keeps the recheck owed across restarts. The final verdict covers the earliest
coverage frontier returned by all feeds. A write after that frontier remains
outside the receipt's time window.

Every version and tombstone is checked, so reverting a late edit does not
remove its history. Only server-controlled provenance can attribute a late
edit to another user. Missing coverage, a vanished or changed history entry,
an unreadable page, or an unresolved actor prevents confirmation. The history
checkpoint stores keyed event digests; record values and actor identities do
not enter the receipt.
1 change: 1 addition & 0 deletions packages/flow/openadapt_flow/agent/verify_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ def _recheck(self, args: Mapping[str, Any]) -> dict[str, Any]:
"verified": late.verified,
"outcome": late.outcome,
"late": late.late,
"final": late.final,
"reasons": list(late.reasons),
"recheck_due_at": late.due_at,
"read_at": late.read_at,
Expand Down
11 changes: 11 additions & 0 deletions packages/flow/openadapt_flow/runtime/effects/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
selector_value,
stable_id,
)
from openadapt_flow.runtime.effects.history import FrameHistory
from openadapt_flow.runtime.effects.learned import (
NOT_COMPARED,
LearnedFrame,
Expand Down Expand Up @@ -508,6 +509,8 @@ class FrameSpec(BaseModel):
#: (``versions.FrameVersions``): a version in the window that isn't the
#: agent's own is a lost update.
versions: Optional[FrameVersions] = None
#: Complete type/table change feeds for a provisional then final verdict.
history: Optional[FrameHistory] = None
#: Moves a person approved from ``openadapt-flow learn``
#: (``learned.LearnedFrame``): the app's own writes on the acting user's
#: row or the write's own rows, by table, column and change.
Expand All @@ -518,6 +521,8 @@ def _compact(self, handler: Any) -> dict[str, Any]:
# Unset, ``versions`` is left out, so a contract that doesn't use it
# keeps its digests (``frame_sha256``, the check digest).
data: dict[str, Any] = handler(self)
if data.get("history") is None:
data.pop("history", None)
if data.get("versions") is None:
data.pop("versions", None)
if data.get("learned") is None:
Expand All @@ -529,6 +534,12 @@ def _compact(self, handler: Any) -> dict[str, Any]:
@model_validator(mode="after")
def _database_attribution(self) -> "FrameSpec":
a = self.attribution
if (
self.history is not None
and self.database is not None
and self.database.recheck_after_s is not None
):
raise ValueError("choose history or database recheck scheduling, not both")
if (
self.database is not None
and a is not None
Expand Down
Loading
Loading