Repository navigation
[Bug] File-producing task steps can duplicate artifacts after interruption #32
Description
What happened?
Several file-producing workflows use this ordering:
perform file effect → receive random artifact ID → checkpoint/cache that ID
Because Files.import() creates a new randomUUID(), an interruption after the file is created but before the durable task/cache receipt is saved can cause the same logical operation to create another artifact when the task is retried.
I found the same boundary in multiple current paths on main (82ff35b):
-
Document workflow
AgentService.document()calls:files.fill(...) → filled.id → ctx.checkpoint({ state.filledId, artifactIds })If the process/lease is lost after
files.fill()commits the new PDF but before the task checkpoint, recovery sees nostate.filledIdand fills the original again, producing another random-ID PDF. -
Model
fill_pdftoolThe generic
cached()helper executes the operation first, then writes the result intostate.operationsand checkpoints it:operation() → operations[key] = result → checkpoint()So the cache prevents replay only after the post-effect checkpoint succeeds.
-
Attachment import
WorkspaceService.importAttachment()checks theimportsmapping, then callsfiles.import(), then persists the mapping. A crash between import and mapping persistence can also create a second artifact on retry.
The underlying invariant seems to be:
one logical file-producing operation should have one durable operation identity, independent of how many execution attempts occur.
This is similar to the existing write-safety discipline around reviewed external actions and computer operationId, but file effects currently rely on a receipt written after the effect.
Steps to reproduce
A deterministic regression could inject failure immediately after the file effect but before its task/mapping checkpoint.
For the document path:
- Create a task with a fillable source PDF and valid fields.
- Allow
files.fill()to complete and persist its output. - Fail the following
ctx.checkpoint(). - Recover/retry the task.
- Assert that exactly one filled artifact exists for that logical fill operation.
Current behavior should re-enter with no state.filledId and call files.fill() again, yielding a second random artifact ID.
The same failure injection can be applied around cached("fill_pdf", ...) and importAttachment().
Commit or release
82ff35b912e4a08c435d29278631c95445adf658
Platform
API or worker
Environment and safe logs
Code-path audit against current main; I have not claimed a local executable reproduction because this environment cannot run the repository.
I would avoid fixing this only by moving the checkpoint earlier, because that would create the opposite failure mode (receipt says complete before the file exists). A durable operation identity / resumable file receipt seems like the safer abstraction. The smallest implementation might be a deterministic idempotency key accepted by file-producing operations, but the filesystem + DB recovery semantics should be decided explicitly.
AI-use note: I used an AI assistant to trace the file-effect/checkpoint ordering and draft this report; I verified the three cited paths against current source before filing.
Activity
charan-rathore commentedon Sep 23, 2026
I am working on this. I reproduced the document checkpoint gap with a deterministic fixture test: the first attempt persists a filled PDF, loses its task checkpoint, and the retry creates a different artifact ID. The fix in progress uses opt-in operation-scoped file identities and atomic file publication, preserving ordinary manual imports. Document, attachment-mapping, and file metadata/concurrent-replay checks pass locally; the complete fixture suite is running before I open a PR. No live provider accounts are used. AI assistance is being used for implementation and test review.
charan-rathore commentedon Sep 23, 2026
Implemented and opened #45. The document regression fails before the fix and passes afterward; 161 fixture tests passed, followed by both final model artifact/cache checkpoint cases passing. Lint, root/server TypeScript, browser-worker TypeScript, server build, and diff checks pass locally. The PR documents untested mobile/browser/container/live-provider paths and the injected-failure (not power-loss) test scope. Ready for maintainer review.
test(work): reproduce OpenMuse file replay blocker CopilotKit#32