fix(issue-5): address phase 2a round 2 findings (F-C, F-D, R1, R4)

Both reviewers sustained the round 1 fixes and found four more. Each was
reproduced here before being accepted; none were rebutted.

F-C (claude, blocking) -- my own round 1 fix caused this. Making refused writes
exit non-zero also classified an idempotent ACK as a refusal, and the wrapper
finalises an approved job with `status --set completed` on a record that is
already completed. It runs under `set -e`, so the *success* path aborted with
exit 4 before its validation hook and its final stdout line. Reproduced at the
shell. An idempotent result is now distinguishable from a refusal and exits 0; a
genuine conflict still exits 4.

F-D (claude) -- routing the operator write around update_status dropped the
status vocabulary check. Under the rollback flag an invalid status was written
to the record with exit 0, where before it was refused with exit 1 and the record
left alone. The check now runs first, in both modes.

R1 (codex, blocking) -- the previous fix compared identifiers only when they
happened to be present, so evidence carrying none at all was still approved on
the dispatch record's own flags. That is the self-assertion the check exists to
replace. Every identifier must now be supplied and agree, and a report manifest
must associate with the attempt being judged. Identifiers are read from a
collector envelope beside the payload rather than from inside it: a native Herdr
payload may not carry MAM fields, so one appearing there is either fabricated or
a contract violation, and either way it is not evidence.

R4 (codex) -- a sequence number orders nothing unless the compare and the write
are atomic. Two flushes could both read the old value, the newer land first, and
the slower overwrite it. Reproduced deterministically. The mirror now serialises
read-compare-write under a per-job audit lock, taken outside the registry
transaction so a slow mirror still cannot lengthen it, and each write uses its
own temp file instead of a shared name.

All eight new regressions fail against the pre-fix code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-06 20:10:41 +09:00
1 parent 3b134e3358
commit cb575fa851
6 files changed
+298 -48

No files matched your search

+40 -16
View File
@@ -17,6 +17,11 @@ from lib_py.loop.evidence import Decision
# nothing about completion; it only makes completion *possible* to consider.
QUIESCENT = ("idle", "done")
# The four identifiers an external collector must supply before a completion can
# be attributed to this attempt.
ATTRIBUTION_FIELDS = ("job_id", "attempt_id", "agent_session_uuid",
"pane_process_generation")
def _timeline_by_kind(scenario):
out = {}
@@ -28,22 +33,32 @@ def _timeline_by_kind(scenario):
def collect_observed_identity(scenario) -> dict:
"""Gather identifiers the *evidence* carries, for comparison with dispatch.
Without this, attribution only proves the dispatch record is internally
well-formed -- it never confronts what was actually seen. Association of a
job or attempt to an observation is the external collector's business, so
these are read from the timeline items and the report manifest, never
invented as native Herdr fields.
Two sources, and the distinction matters. Association of a job or attempt to
an observation is the external collector's business, so it lives in a
``collector_attribution`` envelope beside the payload -- never inside it,
because a native Herdr payload must stay native (phase 0 forbids MAM fields
there, and a fabricated one would be indistinguishable from a real reading).
The report manifest is a filesystem fact and stays in that payload.
Every occurrence is kept. Taking only the first would drop a later,
contradicting observation, which is precisely what this exists to catch.
"""
observed = {}
for item in scenario.get("timeline") or []:
envelope = item.get("collector_attribution") or {}
for field in ATTRIBUTION_FIELDS:
if envelope.get(field) is not None:
observed.setdefault(field, []).append(envelope[field])
payload = item.get("payload") or {}
for field in ("job_id", "attempt_id", "agent_session_uuid",
"pane_process_generation"):
if payload.get(field) is not None:
# Every occurrence is kept. Taking only the first would let a
# later, contradicting observation pass unnoticed -- which is the
# opposite of what this collection is for.
observed.setdefault(field, []).append(payload[field])
if item.get("source_kind") != "herdr":
# A MAM-side source (an event, a filesystem fact) legitimately names
# the job or attempt in its own payload. A native Herdr payload never
# may: phase 0 forbids MAM fields there, so an identifier appearing
# inside one is either fabricated or a contract violation, and either
# way it is not evidence.
for field in ATTRIBUTION_FIELDS:
if payload.get(field) is not None:
observed.setdefault(field, []).append(payload[field])
manifest = payload.get("report_manifest")
if isinstance(manifest, dict) and manifest.get("attempt_id") is not None:
observed.setdefault("manifest_attempt_id", []).append(manifest["attempt_id"])
@@ -97,15 +112,24 @@ def decide(scenario) -> Decision:
if not attributed.ok:
return Decision("hold", attributed.reason)
for field, values in observed.items():
for value in values:
# Absence of proof is not proof. Comparing only the identifiers that happen
# to be present would let evidence carrying none at all be approved on the
# dispatch record's own say-so, which is the self-assertion this check exists
# to replace. Every identifier must be supplied, then must agree.
for field in ATTRIBUTION_FIELDS:
if not observed.get(field):
return Decision("hold",
"no observed {} to attribute this completion to".format(field))
for value in observed[field]:
if value != dispatch.get(field):
return Decision("hold", "attribution mismatch on {}: observed {!r}, "
"dispatched {!r}".format(field, value,
dispatch.get(field)))
# The finalized report must belong to the attempt being judged; a manifest
# from another attempt is evidence of a different run finishing.
# The finalized report must belong to the attempt being judged. A missing
# manifest attempt holds for the same reason: it proves nothing.
if not manifest_attempts:
return Decision("hold", "no report manifest attempt to associate with this attempt")
for manifest_attempt in manifest_attempts:
if manifest_attempt != dispatch.get("attempt_id"):
return Decision("hold",