Repository navigation
feat(runtime): generation-fence authoritative artifacts - #27
Merged
Merged
Conversation
jamespud
force-pushed
the
feat/artifact-generation-fencing
branch
from
October 7, 2026 07:43
d293f0d to
cb01424
Compare
19 tasks done
jamespud
marked this pull request as ready for review
October 7, 2026 07:53
persistArtifact only escalated ErrLeaseLost, so any other repository or storage error on an authoritative artifact write was logged and counted while the run continued. T3 widened the authoritative generation domain past the terminal reference fence (AgentRun, ToolCall, Reflection, DebateRound), so a case could still reach RESOLVED while claiming artifacts that never landed. Route the durable owner through persistArtifact so any write error aborts the execution; the legacy generation-0 and best-effort paths keep the old log-and-count contract. A commit whose outcome is unknown is treated the same way: the generation is abandoned and a later one recovers, never guessed at. Also stop flattening the failure cause to a string in fail(), so a caller can classify it with errors.Is instead of only by message. Tests: unit classification of the durable/legacy split, durable in-memory orchestration failures for agent_run/evidence/claim/tool_call/vote, plus real-MySQL fault injection for an owner-verification lock timeout, an INSERT failure and an abandoned in-flight write against a resolving control.
S27 adds case_id to magi_agent_checkpoint, but the pre-S27 table recorded no Case at all: it stored only run_id plus snapshot state, and run_id is the logical working-memory identity rather than a declared reference to magi_agent_run. The migration comment now states that legacy rows keep case_id empty with execution_generation=0 instead of fabricating provenance, and that a reflection/tool-call row with a broken AgentRun relationship keeps unknown provenance rather than failing the upgrade. CaseRepo.Delete now documents the three buckets it actually has: direct case_id provenance, the pre-S27 containment rule that removes a checkpoint whose run_id names one of the Case's runs, and the unknown-provenance rows it deliberately retains. The containment rule stays because it is an existing cleanup contract; it is a deletion rule, not a provenance claim, and the generation-scoped loader cannot see retained rows. Tests: S27 upgrade assertions for unchanged checkpoint provenance, preserved orphan reflection/tool-call history and RunID collisions, plus the delete/retention contract on SQLite and real MySQL including generation-scoped loader isolation.
persistArtifact only fails closed when it receives the durable owner, so the second defence line is persistArtifactValue refusing a positive-generation entity that arrives without one. That branch had no coverage: a future authoritative write that forgets to thread the owner would silently fall back to the legacy Create path and only the guard would stop it. The regression drives all seven authoritative artifact types through persistArtifact with a nil owner and a repository that does not implement OwnedArtifactRepository (its embedded interface is nil, so any storage call panics), and asserts ErrLeaseLost instead.
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parent: #17 · T3 of 6 (#21) · Base
1beffac68efd2b5bf3599911cb2500a1a4f2f186Summary
Generation-fence authoritative artifacts and checkpoints using the durable owner established by T2.
A positive-generation write is authoritative only when the repository can prove, in the same database transaction as the INSERT/Save:
job_id + case_id + RUNNING + worker_id + execution_generation + unexpired leaseand
decision_case.execution_generation == execution_generation.Generation 0 remains legacy/history compatibility.
S27 schema
Adds
execution_generation BIGINT NOT NULL DEFAULT 0to:Reflection and ToolCall also gain direct
case_idprovenance. Legacy Reflection/ToolCall rows relationally backfill only their Case ID from AgentRun; generation remains 0 and is never inferred from old-aN-IDs.Checkpoint durable identity becomes:
(run_id, execution_generation)so a newer generation cannot silently restore or overwrite an older owner's snapshot.
Explicit owner propagation
The durable execution owner is a single runtime-only value object in
domain/entity:RunManager constructs it after Claim and passes it explicitly through Orchestrator → Dispatcher → AgentLoop/checkpoint. It is not hidden in
context.Valueand is not persisted on DecisionCase.A positive-generation durable run without the owner-aware orchestration capability fails closed.
Artifact write semantics
Production aggregate repository implements
OwnedArtifactRepository.Every owned Create verifies the active T2 owner and writes in the same transaction:
Legacy sub-repository
Createrejects positive-generation rows withErrLeaseLost, preventing a bypass around the owner-aware API.Owner loss propagates back to Orchestrator and terminates the durable attempt instead of being treated as a best-effort artifact failure.
ID namespace
Persisted execution artifact IDs now use the durable generation:
-g<execution_generation>-instead of resettable
ExecutionAttempt/-a<attempt>-.ID text is diagnostic/stable identity only; authorization never parses the ID.
Read semantics
Repository keeps two explicit classes:
OwnedArtifactRepository;ListByCasereads returning all generations with generation metadata.DecisionService current views load the Case's current
ExecutionGenerationand return only that generation for Evidence, Claims, Votes, AgentRuns and ToolCalls.No-cleanup retries therefore remain correct even while old-generation rows remain physically present.
Checkpoint semantics
Logical checkpoint RunID remains stable across durable generations, while storage is generation-scoped.
#11 integration
Resolution now persists
ExecutionGeneration.Atomic terminal reference verification and the explicit non-atomic test fallback require cited Vote/Evidence/Claim rows to match both:
Old generation rows cannot satisfy a newer Resolution merely because their IDs exist.
General Case status/terminal generation CAS is intentionally not included; that remains T4 / #22.
T3 / T5 boundary
Positive-generation retries no longer invoke the historical case-wide
CleanupCaseArtifacts.Correctness is now independent of cleanup. Old rows remain readable as history; T5 / #23 will convert the legacy cleaner into generation-scoped GC.
Real MySQL verification
The MySQL CI gate explicitly runs T3 suites proving:
ErrLeaseLost, zero insert;Verification
Final exact head:
cb014244cd926762702a22576006b7b5d30d937aGitHub Actions run 37589099246: success
go test -race ./...go vet ./...The iterative branch history was reduced to four logical commits. The rewritten final Git tree SHA is byte-identical to the previously validated content tree.
Commit structure
969846b— schema / artifact generation provenancef70f431— runtime owner fencing + checkpoint generation scopee806615— current decision views scoped to generationcb01424— RED→GREEN tests + real MySQL CI gatesNon-goals
Not included:
Those remain T4 #22, T5 #23, and T6 #24.