Skip to content

Route-then-mirror is not atomic: a failure between the two writes strands an Outcome on Next while Actionless #709

Description

@capo-the-ai-bot

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
awaitmirrorTitleIfActionless(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

  • Routing to Next with a title-as-action fallback commits the intent and the Action in one transaction.
  • A failure injected between the old two writes cannot leave the row on Next and Actionless.
  • Every site in the surveyed class uses the same primitive; none retains a bespoke route-then-mirror pair.
  • Regression test drives the interleaving directly and is proven to fail against the current two-write shape.
  • No behavioural change to the cancel path or the blank-title stall (both must still leave the item unresolved).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or silently wrongengineeringtech-debtInternal refactor / cleanup, no direct user payoff

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions