# Durable runtime facts use one atomic state boundary

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0169-durable-runtime-facts-use-one-atomic-state-boundary.md

---

# 0169 — Durable runtime facts use one atomic state boundary

## Decision

`stitchkit/application` exposes an async `StateStore` with atomic `update`, a
bounded process-run ledger, and an at-least-once notification outbox. The Node
adapter in `stitchkit/server` serializes cross-process updates with an owned
lock and commits files by unique temporary write, fsync and rename.

Lifecycle transitions address a run by both `runId` and `pid`. Facts leave the
ledger through typed subscriptions and the managed resource value. The outbox
claims records with a lease, keeps a stable idempotency key across retries, and
applies declared retry and retention bounds.

## Why

Separate read-then-write helpers lose records during zero-downtime overlap.
Calling a delivery callback directly loses the notification when the process
dies between recording the fact and sending it. Both problems need the same
small persistence boundary; neither needs a database or a transport in core.

## Consequences

- Persistence can be a file, a database or a test fake, but atomic update is a
  semantic requirement rather than an adapter detail.
- Delivery is at-least-once; consumers deduplicate by the stable key.
- Message rendering, Telegram/email APIs and owner identity stay outside core.
- Corrupt or unreadable files are reported by policy rather than guessed clean.
- A run's termination says who recorded it: `clean` and `forced` by the process,
  `hot-reload` and `abnormal` by its successor. The ledger never rewrites a
  crash as a kill, and it does not decide alone whether two processes of one
  build overlapping is a crash — that is a deployment fact the application
  declares (`sameVersionOverlap`), defaulting to one process per build.
