feat(jobs): a resumed job does not re-send what it delivered #746

Merged
coilyco-ops merged 1 commit from feat/an-emitted-message-is-not-emitted-twice into main 2026-08-13 22:11:24 +00:00
Member

RecordEffect and EffectApplied had no production caller, so job.Effects was empty for every job that ran and a re-execution repeated everything. That was harmless while a job's only output was a status line. It stopped being harmless when #356 and #742 gave a job ten ordered messages to say.

A job that emits ten messages and then resumes emits some of them twice, and the member reads the same paragraph again.

What ships

Each message records an effect keyed by position, content:N. A re-execution checks before sending and skips what was delivered.

The effect is recorded after the send, deliberately. Recording first would skip a message the origin never received, and a truncated answer is worse than a repeated paragraph. TestAFailedSendRecordsNoEffect holds that, and reversing the order fails it.

A replay still delivers what the first run did not reach, so a crash mid-answer is resumed rather than frozen.

What it does not do, which matters for reading this honestly

agent.go:311 builds a MemoryJobStore unless JobStoreDir is set, and deploy#464 records both lanes running the in-memory store today. So this guards in-process re-execution now, and nothing across a restart until deploy sets a directory. It is written to be inert rather than required: TestContentWorksWithoutAStore asserts the path still works with no store at all.

I would rather ship a guard that is honest about its reach than describe it as durable idempotency.

This closes two rows of 621, not four

RecordEffect and EffectApplied leave unwiredCapabilities. CommandFromPrompt and IsGrantDenial stay: those are still the deletion judgement #621 was filed for.

The guard another seat built caught the wiring and told me exactly what to do:

RecordEffect now has 1 production caller(s). sirens-echo#621 asks that wiring be recorded, so document it and drop this row

That is the repository working as designed, and it is the reason this change did not land with a stale table.

Verification

ward exec gate PASS on all six steps.

Two mutations, both compiling:

  • the replay guard never firing: fails TestAReplayedMessageIsNotSentTwice and TestAReplayStillDeliversWhatTheFirstRunDidNot
  • recording the effect before the send: fails TestAFailedSendRecordsNoEffect

closes #745

`RecordEffect` and `EffectApplied` had no production caller, so `job.Effects` was empty for every job that ran and a re-execution repeated everything. That was harmless while a job's only output was a status line. It stopped being harmless when [#356](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/356) and [#742](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/742) gave a job **ten ordered messages** to say. **A job that emits ten messages and then resumes emits some of them twice**, and the member reads the same paragraph again. ## What ships Each message records an effect keyed by position, `content:N`. A re-execution checks before sending and skips what was delivered. **The effect is recorded after the send, deliberately.** Recording first would skip a message the origin never received, and a truncated answer is worse than a repeated paragraph. `TestAFailedSendRecordsNoEffect` holds that, and reversing the order fails it. A replay still delivers what the first run did not reach, so a crash mid-answer is resumed rather than frozen. ## What it does not do, which matters for reading this honestly `agent.go:311` builds a `MemoryJobStore` unless `JobStoreDir` is set, and [deploy#464](https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/464) records both lanes running the in-memory store today. **So this guards in-process re-execution now, and nothing across a restart until deploy sets a directory.** It is written to be inert rather than required: `TestContentWorksWithoutAStore` asserts the path still works with no store at all. I would rather ship a guard that is honest about its reach than describe it as durable idempotency. ## This closes two rows of 621, not four `RecordEffect` and `EffectApplied` leave `unwiredCapabilities`. `CommandFromPrompt` and `IsGrantDenial` stay: those are still the deletion judgement [#621](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/621) was filed for. The guard another seat built caught the wiring and told me exactly what to do: > RecordEffect now has 1 production caller(s). sirens-echo#621 asks that wiring be recorded, so document it and drop this row That is the repository working as designed, and it is the reason this change did not land with a stale table. ## Verification `ward exec gate` PASS on all six steps. Two mutations, both compiling: - the replay guard never firing: fails `TestAReplayedMessageIsNotSentTwice` and `TestAReplayStillDeliversWhatTheFirstRunDidNot` - recording the effect before the send: fails `TestAFailedSendRecordsNoEffect` closes #745
feat(jobs): a resumed job does not re-send what it delivered
All checks were successful
ci / image-build (pull_request) Successful in 22s
ci / test (pull_request) Successful in 37s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
11d5fe2aca
RecordEffect and EffectApplied had no production caller, so job.Effects was
empty and a re-execution repeated everything. Harmless while a job's only
output was a status line; not harmless now that 356 and 742 give a job ten
ordered messages to say.

Each message records an effect keyed by position. A re-execution checks before
sending and skips what landed, and still delivers what the first run did not
reach.

The effect is recorded after the send. Recording first would skip a message
the origin never received, and a truncated answer is worse than a repeat.

Inert rather than required: the deployment runs an in-memory store today, so
this guards in-process re-execution and nothing across a restart until deploy
sets a directory. A test asserts the path works with no store at all.

closes #745

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!746
No description provided.