Skip to content

feat(decisions): durable decision log + overridable tracker receipts - #3

Merged
corrm merged 2 commits into
mainfrom
feat/decision-sink-hardening
Jun 21, 2026
Merged

feat(decisions): durable decision log + overridable tracker receipts#3
corrm merged 2 commits into
mainfrom
feat/decision-sink-hardening

Conversation

@corrm

@corrm corrm commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What

Two related changes to beflow's decision-sink layer (BEFLOW-10 governance roadmap), both behind the existing DecisionSink contract — no schema change.

BEFLOW-12 — durable decision log

The decision log is beflow's audit record; it must survive concurrent runs. LocalNdjsonSink.emit used unlocked read-modify-write, so two runs racing on decisions.ndjson could clobber each other's events.

  • Add atomic append() to the RunStoreFs contract; emit now does a single O_APPEND write.
  • Add readDecisionEvents() that recovers from a torn/partial trailing line without dropping earlier valid events.

BEFLOW-9 (PR1 slice) — surface decisions as tracker receipts

  • New TrackerCommentSink + CompositeSink pairing the durable NDJSON log with a best-effort tracker comment, ordered [local, tracker] so the audit write lands first.
  • The receipt body is a user-overridable prompt template (decision-receipt.md) riding the existing prompts.dir cascade — not a forced format.
  • Gated by decisions.comment (default on). Tracker and template failures are logged, never fatal.

Tests

The tests enforce the fixes rather than just smoke them:

  • a spy proving emit never read-then-writes (fails against the old code),
  • a real cross-process O_APPEND no-loss test,
  • torn-line recovery,
  • receipt render / override / gating coverage.

bun run check (oxfmt + oxlint + tsc + bun test): 1036 pass / 0 fail.

Not in this PR

Receipt-aware gate evaluation (using the receipt to influence allow/block) remains open on BEFLOW-9 as follow-up.

corrm added 2 commits June 21, 2026 01:55
BEFLOW-12 — harden the decision log against concurrent writers:
- Add atomic append() to the RunStoreFs contract; LocalNdjsonSink.emit now
  does a single O_APPEND write instead of read-modify-write, so two runs
  racing on decisions.ndjson can no longer clobber each other's events.
- Add readDecisionEvents() that recovers from a torn trailing line without
  dropping earlier valid events.

BEFLOW-9 — surface every governed decision as a receipt comment:
- New TrackerCommentSink (behind the existing DecisionSink contract) and
  CompositeSink pairing the durable NDJSON log with a best-effort tracker
  comment; ordered [local, tracker] so the audit write lands first.
- Receipt body is a user-overridable prompt template (decision-receipt.md)
  riding the existing prompts.dir cascade — not a forced format.
- Gated by decisions.comment (default on); tracker and template failures
  are logged, never fatal.

Tests enforce the fixes: a spy proving emit never read-then-writes, a
cross-process O_APPEND no-loss test, torn-line recovery, and receipt
render/override/gating coverage.
- comment readDecisionEvents as the durable read path consumed by
  predictive preflight (BEFLOW-18) so the not-yet-wired export reads as
  staged foundation, not dead code
- rename the single-process sink test to state what it verifies (two
  instances accumulate, never clobber); real concurrency is the
  cross-process test below it
- isDecisionEvent: drop the per-line shallow-copy allocation in favor of
  in-operator narrowing
@corrm
corrm merged commit 1806f44 into main Jun 21, 2026
1 check passed
@corrm
corrm deleted the feat/decision-sink-hardening branch June 21, 2026 00:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant