User Story
As a user whose device dies or loses the race mid-route, I want an Outcome to never come to rest on Next with no Action, so it does not sit in the re-clarification queue forever waiting for a step that already half-happened.
Context
Several surfaces route an Outcome to Next and then, in a second write, mirror the Outcome's title into its current Action when the Outcome is Actionless (the title-as-action fallback — an empty next-action phrase means the Outcome's title, written only while the Outcome is Actionless; issue #691). The two writes are not atomic:
await _commit(RoutingKind.nextAction, actionText: …); // write 1: intent, clarified, stamp
await …mirrorTitleIfActionless(id, title); // write 2: the Action
A throw, a crash, or an unlucky interleaving between them leaves the row on Next and Actionless. That state is not self-correcting in a benign way: TodoDao._needsReviewWhere's Actionless branch has no freshness gate, so the Outcome re-surfaces in the Daily Planning re-clarification queue every morning until it acquires an Action.
Raised by CodeRabbit on PR #707 and deferred there deliberately: it is a class, not a single site, and the repo's policy is that class-wide surveys get their own PR.
Known sites (verify and extend — this list is a starting point, not a survey)
Scope
- Survey the class: find every site that routes and then mirrors (or otherwise completes) in a second write.
- Make the route and its Action write commit together, so the row is never observably on Next while Actionless.
TodoDao.setCurrentActionTextIfActionless is already atomic in itself; what is missing is a primitive that commits it with the routing.
- Prefer one new DAO/service primitive that all sites adopt over per-site patching — two independent fixes would re-create the drift this issue exists to remove.
- Update any fakes/test doubles that stand in for the service, and cover the interleaving with a test rather than only the happy path.
Acceptance Criteria
Out of Scope
- The Waiting For half of the title-as-action policy. It is currently applied only on clarify surfaces (
ClarifyDraft.assemble, ClarifyCard's mirror) and not by ProcessToHandlers._waitingFor, which is documented as intent-only. That mismatch is recorded as a Flagged ambiguity in CONTEXT.md § GTD Core and wants its own decision, not a fix bundled here.
_needsReviewWhere's missing freshness gate on the Actionless branch — that is the behaviour that makes this bug persistent, but it is deliberate (an Actionless Outcome genuinely does need attention).
Related
User Story
As a user whose device dies or loses the race mid-route, I want an Outcome to never come to rest on Next with no Action, so it does not sit in the re-clarification queue forever waiting for a step that already half-happened.
Context
Several surfaces route an Outcome to Next and then, in a second write, mirror the Outcome's title into its current Action when the Outcome is Actionless (the title-as-action fallback — an empty next-action phrase means the Outcome's title, written only while the Outcome is Actionless; issue #691). The two writes are not atomic:
A throw, a crash, or an unlucky interleaving between them leaves the row on Next and Actionless. That state is not self-correcting in a benign way:
TodoDao._needsReviewWhere's Actionless branch has no freshness gate, so the Outcome re-surfaces in the Daily Planning re-clarification queue every morning until it acquires an Action.Raised by CodeRabbit on PR #707 and deferred there deliberately: it is a class, not a single site, and the repo's policy is that class-wide surveys get their own PR.
Known sites (verify and extend — this list is a starting point, not a survey)
app/lib/widgets/process_to_handlers.dart—_nextWithDialog's blank-save branch, viaClarificationService.mirrorTitleIfActionless.app/lib/widgets/clarify_card.dart—_onAfterRouteships the identical route-then-mirror shape and predates PR fix(clarify): fall back to the title on an empty next-action save (#691) #707.Scope
TodoDao.setCurrentActionTextIfActionlessis already atomic in itself; what is missing is a primitive that commits it with the routing.Acceptance Criteria
Out of Scope
ClarifyDraft.assemble,ClarifyCard's mirror) and not byProcessToHandlers._waitingFor, which is documented as intent-only. That mismatch is recorded as a Flagged ambiguity inCONTEXT.md§ GTD Core and wants its own decision, not a fix bundled here._needsReviewWhere's missing freshness gate on the Actionless branch — that is the behaviour that makes this bug persistent, but it is deliberate (an Actionless Outcome genuinely does need attention).Related
mirrorTitleIfActionlessand the two-write shape at this site; deferred this finding.app/lib/widgets/process_to_handlers.dart(_nextWithDialog's dartdoc — cancel vs blank save, and the blank-title stall) andTodoDao.setCurrentActionTextIfActionless. It had an ADR,0049-empty-next-action-means-the-title.md, removed in816381a6as one of the bug-fix ADRs; its reasoning is recoverable withgit show 816381a6^:docs/adr/0049-empty-next-action-means-the-title.md.app/lib/database/daos/todo_dao.dart—setCurrentActionTextIfActionless,_needsReviewWhere.