mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-25 23:32:22 +01:00
feat(runtime): claim effects before dispatch and gate completion on them
Dispatcher seam: mark_dispatch, which runs inside the live Wave 3 binding scope immediately before backend invocation, now durably claims a possible effect before execution_id is assigned. If the claim cannot be persisted the action stays undispatched and the dispatcher returns BLOCKED; dispatched() closes the never-awaited coroutine. record_action appends the outcome (including cancellation/interruption) and admitted-read observations before the receipt reduction drops producer facts. Adapters consume only the bound operations the dispatcher admitted: filesystem bindings give exact scope and predicates (write_file content digest after fence unwrapping, apply_patch add/delete, edit existence); bash/python launches have unknown scope with the launch generation as lineage; job kills scope the exact job and its processes; owned operations scope their exact revisioned records; external backends are claimed as external and never verified by acknowledgement; browser session_info yields session lifecycle observations only, and a page binding is never effect scope. Complete read_file re-reads the exact bound source to digest it; offset/limit, truncation, extraction and listings are partial. Background launches stay RUNNING until an admitted read of the exact job generation (via a durable launch index, across continuation runs) reports settlement. Producer seams: typed job lifecycle facts on manage_bg_jobs reads/kills, a structured timed_out flag on containment timeouts, and mutation_attempted on in-place write_file/edit_file failures after truncation. Completion: the existing EvidenceLedger consumes effect assessments through a single helper used for the decision, ask_user and prose filtering. A required artifact is unsettled by a later unresolved effect that may have touched it, a fresh contradicting readback fails the decision, and partial reads no longer count as artifact validation. Ordinary conversation and read-only turns are unchanged; no second completion policy is introduced.
This commit is contained in:
@@ -54,6 +54,7 @@ _SHA256 = re.compile(r"[a-f0-9]{64}")
|
||||
class ResourceKind(str, Enum):
|
||||
FILESYSTEM = "filesystem"
|
||||
PROCESS = "process"
|
||||
PROCESS_LAUNCH = "process_launch"
|
||||
BACKGROUND_JOB = "background_job"
|
||||
OWNED = "owned"
|
||||
EXTERNAL = "external"
|
||||
@@ -108,6 +109,11 @@ class ResourceRef:
|
||||
"""
|
||||
if self.kind is not other.kind:
|
||||
return False
|
||||
if self.kind is ResourceKind.OWNED:
|
||||
# A collection binding ("*") covers every record it can create,
|
||||
# list or change; specific records only overlap themselves.
|
||||
return self.location[:-1] == other.location[:-1] and (
|
||||
self.location[-1] == other.location[-1] or "*" in (self.location[-1], other.location[-1]))
|
||||
if self.kind is not ResourceKind.FILESYSTEM:
|
||||
return self.location == other.location
|
||||
if self.location[:-1] != other.location[:-1]:
|
||||
@@ -153,6 +159,13 @@ def resource_ref(resource: Any, role: str) -> ResourceRef:
|
||||
location = ("process", resource.namespace, resource.owner, resource.request_id, resource.thread_id,
|
||||
str(ident.pid), ident.start_token, resource.role)
|
||||
return ResourceRef(ResourceKind.PROCESS, role, location, ident.start_token, _sha(resource.to_dict()))
|
||||
if isinstance(resource, wave3.ProcessLaunchResource):
|
||||
# The reservation generation is the exact launch -> job linkage that
|
||||
# Wave 3 validates in ``job_from_record``.
|
||||
location = ("process_launch", resource.namespace, resource.owner, resource.request_id,
|
||||
resource.thread_id, resource.generation)
|
||||
return ResourceRef(ResourceKind.PROCESS_LAUNCH, role, location, resource.generation,
|
||||
_sha(resource.to_dict()))
|
||||
if isinstance(resource, wave3.BackgroundJobResource):
|
||||
location = ("background_job", resource.namespace, resource.owner, resource.request_id,
|
||||
resource.thread_id, resource.job_id, resource.generation)
|
||||
@@ -378,11 +391,13 @@ class ProducerFacts:
|
||||
job_state: str = ""
|
||||
remote_acknowledged: bool = False
|
||||
external: bool = False
|
||||
# The producer reached its mutation stage before reporting failure.
|
||||
mutation_attempted: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.exit_code is not None and type(self.exit_code) is not int:
|
||||
raise ValueError("Malformed producer exit code")
|
||||
for name in ("timed_out", "output_truncated", "remote_acknowledged", "external"):
|
||||
for name in ("timed_out", "output_truncated", "remote_acknowledged", "external", "mutation_attempted"):
|
||||
if type(getattr(self, name)) is not bool:
|
||||
raise ValueError("Malformed producer flag")
|
||||
for name in ("failure_kind", "job_state"):
|
||||
@@ -395,7 +410,7 @@ class ProducerFacts:
|
||||
return {"exit_code": self.exit_code, "timed_out": self.timed_out,
|
||||
"output_truncated": self.output_truncated, "failure_kind": self.failure_kind,
|
||||
"job_state": self.job_state, "remote_acknowledged": self.remote_acknowledged,
|
||||
"external": self.external}
|
||||
"external": self.external, "mutation_attempted": self.mutation_attempted}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: Any) -> "ProducerFacts":
|
||||
@@ -428,6 +443,7 @@ def producer_facts(result: Any) -> ProducerFacts:
|
||||
failure_kind=_label(result.get("failure_kind")),
|
||||
job_state=_label(job),
|
||||
external=external,
|
||||
mutation_attempted=result.get("mutation_attempted") is True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user