Repository navigation
Reopened producer preparation replays dependency patches over stale task-local workspace #949
Description
Summary
When resume --retry-failed reopens an agent producer and its generated prepare:<attempt> task, the durable task worktree may still contain source files authored by the producer's earlier execution. Initial preparation calls materializeWorkspacePatchDependencies() before restoring the persisted workspace-patch preparation tree, so the first dependency patch sees task-local stale source and rejects it as a base-tree mismatch.
Preparation has a retry budget, but retrying is deterministic: the preparation task body does not reset the worktree between attempts. The restore currently occurs only inside resetTaskArtifactsForRetry(), which is reached from the agent's generate() method after preparation succeeds. A failure in preparation can never reach that restore.
Generic reproduction
- Execute a generated task whose dependency chain publishes a workspace patch.
- Let the agent author a task-local source file and finish, then fail its zero-retry artifact verifier.
- Resume with
--retry-failedso the producer closure is reopened. - Observe the reopened
prepare:<attempt>task fail while applying the first dependency patch because the durable worktree contains both the dependency state and the task's earlier source output. - Observe the automatic preparation retry fail identically because no reset runs between attempts.
A sibling whose prior execution authored artifacts only, without changing captured source roots, prepares successfully. This distinguishes task-local stale workspace state from dependency artifact corruption or a transient Git race.
Root cause
The generated workflow's ordering is:
prepareArtifactMirror
-> materializeWorkspacePatchDependencies
-> restoreInvariantSuiteWorkspaceSnapshot
agent generate
-> resetTaskArtifactsForRetry
-> restoreWorkspacePatchPreparation
The durable workspace-patch-preparation.json already records the authenticated pre-agent tree needed for recovery, but the reopened preparation path reads it only after attempting dependency replay. The recovery primitive therefore exists but is ordered behind the failure.
Relationship to existing fixes
This is distinct from #312 / #314. That fix safely skips an authenticated linear dependency prefix when the current tree exactly equals a dependency result_tree. Here, the current tree includes the task's own prior output and intentionally matches no dependency result, so the strict mismatch remains correct.
It is also the stateful-retry test gap called out during review of #829: the retry test proved Smithers invokes preparation twice, but did not exercise the real preparation sequence after durable workspace mutation.
Expected behavior
When authenticated persisted preparation evidence exists for a reopened producer, initial preparation must restore that exact tree before replaying dependency patches. A genuinely fresh task with no persisted evidence must retain the current strict dependency replay behavior. The automatic preparation retry must begin from the same authenticated preparation state rather than the partially mutated or task-authored worktree.
Acceptance criteria
- A reopened producer whose durable worktree contains task-local source drift restores its persisted preparation tree before dependency replay.
- Its
prepare:<attempt>task succeeds without weakeningapplyWorkspacePatchbase-tree validation. - A fresh task with no preparation evidence still replays dependency patches from the pinned baseline.
- Missing, malformed, modified, or unauthenticated preparation evidence still fails closed.
- A preparation failure after partial workspace mutation retries from the authenticated preparation tree.
- Regression coverage exercises both stale task-local drift before prepare and a reopened producer closure, not just generic Smithers retry dispatch.
Activity
aviggiano commentedon Aug 27, 2026
Production validation of the reopened-producer restore exposed a cleanup-policy edge case. A prior agent execution had created an ignored package-manager binary link under a non-runtime scratch directory. The persisted preparation tree correctly excluded it, and every parent component was a real directory, but
removeStaleWorkspaceFilesrejected the candidate because its symlink check included the leaf itself.The leaf is safe to unlink: Node removes the directory entry without traversing its target. Only a symlink in a parent component can redirect deletion outside the owned worktree. The draft fix now checks parent components while permitting stale leaf links to be removed. Regression coverage uses a link to an out-of-worktree target, proves the link is removed while the target bytes remain untouched, and separately proves a symlinked parent is still detected. The original strict preparation-tree and dependency-patch authority checks are unchanged.