Skip to content

Concurrent 'askr add' invocations race on shared registry files, silently dropping a registration while reporting success #91

Description

@smiggleworth

Summary

addPage, addAction, and addDatabase read shared project files, compute replacements, and write them without an atomic compare-and-write boundary. Concurrent commands can therefore report success while one silently overwrites another command's route, action registry, authorization, environment, or package-manifest edit.

Triage

Confirmed, Medium-High. The issue was reproduced with two concurrent add commands: both returned exit code 0 while only one registration survived. The generated but unregistered artifact is silent data loss. This affects ordinary parallel terminals, scripts, and retries.

Reproduction

await Promise.all([
  runAddCli(["page", "alpha", "--cwd", root], alphaIo),
  runAddCli(["page", "beta", "--cwd", root], betaIo),
]);

The same read-modify-write shape exists for action registries/authorizations and database manifest/environment edits.

Required implementation details

  • A fresh-content comparison alone is insufficient: two writers can compare the same value before either rename. Protect the expected-content comparison and the entire staged replacement transaction with deterministic, cross-process file locks acquired in sorted order.
  • Reject a stale plan before staging or replacing any file, so a failed command cannot leave an orphan page, action, database file, or partial shared-file edit.
  • add page must guard the route file content observed at plan time.
  • add action must snapshot both registries before descriptor discovery, then guard both snapshots. Capturing them after discovery leaves a second interleaving where stale computed actions can validate against a newer registry.
  • add database must guard both package.json and the optional .env.example content observed at plan time.
  • Locks must be released after success and failure, and abandoned locks must have a bounded recovery path.

Acceptance criteria

  • Verify the issue author is smiggleworth.
  • Shared-file edits carry the exact content observed at plan time, including an explicit absent-file state.
  • Expected-content checks and replacements execute under atomic, sorted per-file transaction locks so concurrent writers cannot both validate the same snapshot.
  • A stale transaction exits nonzero with a clear File changed before writing diagnostic before creating or replacing any transaction artifact.
  • add page guards _routes.tsx; a deterministic concurrent-invocation regression proves no registration is silently lost and no failed-command page is orphaned.
  • add action guards action-registry.ts and action-authorizations.ts; deterministic regressions cover concurrent invocations and a registry change during descriptor discovery.
  • add database guards package.json and .env.example; a regression proves external manifest changes survive and no database scaffold is partially written.
  • Guardrail (prevent this class of bug): Keep the generic stale-transaction regression plus a concurrent/shared-file regression for every add subcommand that performs read-modify-write mutation.
  • The focused red tests fail against the old implementation and pass after the minimal fix.
  • Full CLI release gate, packed-template integration, peer-floor integration, performance gate, dependency audit, and exact-head hosted CI all pass before merge.

Definition of done

  • All acceptance criteria above are checked with evidence and the release-prepared exact-head PR is ready for squash merge as @askrjs/cli@0.0.25.

Severity

Medium-High — silent generated-registration data loss with a false success signal.


Found via a deep adversarial pass on @askrjs/cli shared-file mutations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    fleaSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions