Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/drop-precondition-guard-capability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@workflow/world': patch
'@workflow/core': patch
'@workflow/world-vercel': patch
---

Remove the `preconditionGuard` World capability. A stale replay-context write no longer needs to be rejected: a reader holds a prefix of the log, replay is deterministic on a prefix, and the writer's next write reports the events it was pushed past.
6 changes: 6 additions & 0 deletions .changeset/require-slot-event-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@workflow/world': patch
'@workflow/core': patch
---

Require every event id the runtime reads to be a log position. `requireEventSlot` replaces the lenient decode that returned "no position" for an id that is not a slot.
5 changes: 5 additions & 0 deletions .changeset/resilient-step-dispatch-off.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/core': patch
---

Turn resilient step dispatch off by default. Set `WORKFLOW_RESILIENT_STEP_DISPATCH=1` to opt back in.
9 changes: 9 additions & 0 deletions .changeset/slot-ids-are-required.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@workflow/world': patch
'@workflow/world-testing': patch
'@workflow/world-local': patch
'@workflow/world-postgres': patch
'@workflow/world-vercel': patch
---

Slot-numbered event ids are a requirement of the World contract, not a capability. The `slotEventIds` flag is gone, and the conformance suite now fails a World whose event ids are not positions.
5 changes: 0 additions & 5 deletions .changeset/windows-preload-timeout.md

This file was deleted.

18 changes: 16 additions & 2 deletions .github/scripts/render-event-log-race-repro-results.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ let previousCommentPath = '';
let timestamp = new Date().toISOString();
let runAttempt = '';
let check = false;
// Which lane this run belongs to (e.g. `world-local`, `world-postgres`).
// Suffixes the results marker and the heading so each lane owns its own sticky
// comment: the lanes run as parallel jobs, and a shared marker would make one
// lane's "previous comment" fetch pick up another lane's history. The Vercel
// lane passes no label and keeps the original marker, so its comment history
// survives this flag's introduction. Marker matching is exact-substring
// (`<!-- event-log-race-repro-results -->` does not match the `-world-local`
// variant because of the closing ` -->`), which is what keeps the lanes apart.
let label = '';

for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
Expand All @@ -24,6 +33,9 @@ for (let index = 0; index < args.length; index += 1) {
} else if (arg === '--run-attempt' && args[index + 1]) {
runAttempt = args[index + 1];
index += 1;
} else if (arg === '--label' && args[index + 1]) {
label = args[index + 1];
index += 1;
} else if (arg === '--check') {
check = true;
} else if (!arg.startsWith('--')) {
Expand Down Expand Up @@ -432,8 +444,10 @@ function render(resultsFile, previousComment) {
'. Rates are still comparable; totals are not.'
: '';

console.log('<!-- event-log-race-repro-results -->');
console.log('## Event Log Race Repro\n');
console.log(
`<!-- event-log-race-repro-results${label ? `-${label}` : ''} -->`
);
console.log(`## Event Log Race Repro${label ? ` (${label})` : ''}\n`);
console.log(
latest.missingResults
? 'No result file was produced by the latest repro job.'
Expand Down
128 changes: 128 additions & 0 deletions .github/workflows/event-log-race-repro.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,131 @@ jobs:
- name: Fail on corrupted, failed, or stuck runs
if: always()
run: node .github/scripts/render-event-log-race-repro-results.js event-log-race-repro-results.json --check

# The same harness against the self-hosted Worlds, one parallel job per World.
# `scripts/event-log-race-repro-local.sh` owns the whole lifecycle: it builds
# the packages and the workbench app with WORKFLOW_TARGET_WORLD and
# WORKFLOW_PUBLIC_MANIFEST=1 set at *build* time (both are build-time inputs),
# brings up Postgres and applies migrations for `--world postgres`, starts the
# app, runs the harness, and tears the server down.
#
# Unlike the Vercel lane above, these lanes publish numbers instead of a
# verdict. The local storms bite much harder than the Vercel preview does —
# the world-postgres step-storm has red baselines at the default scale (see
# "Event Log Race Repro" in CLAUDE.md) — so a gate here would be red on PRs
# that changed nothing, and a gate that is always red is a gate everyone
# learns to ignore. Each lane posts its own sticky PR comment
# (`world-local` / `world-postgres` suffixed markers, so the three lanes'
# histories never mix), and the number to watch is in the comment. The only
# thing that fails these jobs is plumbing: a harness that produced no result
# file at all.
#
# A soak dispatch that raises `budget_ms` has to raise `timeout-minutes` here
# too, same as the Vercel lane.
event-log-race-repro-local-worlds:
name: Event Log Race Repro (world-${{ matrix.world }})
runs-on: ubuntu-latest
# The Vercel lane's 25 minutes, plus room for what it never pays for:
# pnpm build, the app build, and (postgres) container startup + migrations.
timeout-minutes: 45
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'event-log-race-repro') }}
strategy:
fail-fast: false
matrix:
world: [local, postgres]
permissions:
contents: read
issues: write
pull-requests: write
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Setup environment
uses: ./.github/actions/setup-workflow-dev
with:
# The script runs its own `pnpm build` so the packages are built in
# the same environment as the app.
build-packages: 'false'

- name: Run event log race repro (world-${{ matrix.world }})
id: repro
# The script exits with the harness' own status, which fails on any
# regression outcome — expected on some baselines, see the job comment.
continue-on-error: true
run: ./scripts/event-log-race-repro-local.sh --world ${{ matrix.world }}
env:
# Deliberately unguarded pass-through: on a `pull_request` event every
# `inputs.*` is empty, and the harness reads an empty variable as unset
# and falls back to its own default. See the note on the inputs above.
EVENT_LOG_RACE_REPRO_STEP_STORM_ATTEMPTS: ${{ inputs.step_storm_attempts }}
EVENT_LOG_RACE_REPRO_HOOK_STORM_ATTEMPTS: ${{ inputs.hook_storm_attempts }}
EVENT_LOG_RACE_REPRO_ATTEMPTS: ${{ inputs.attempts }}
EVENT_LOG_RACE_REPRO_CONCURRENCY: ${{ inputs.concurrency }}
EVENT_LOG_RACE_REPRO_ROUNDS: ${{ inputs.rounds }}
EVENT_LOG_RACE_REPRO_WIDTH: ${{ inputs.width }}
EVENT_LOG_RACE_REPRO_WATCHDOG_MS: ${{ inputs.watchdog_ms }}
EVENT_LOG_RACE_REPRO_STEP_DELAY_MS: ${{ inputs.step_delay_ms }}
EVENT_LOG_RACE_REPRO_HOOK_RESUME_STAGGER_MS: ${{ inputs.hook_resume_stagger_ms }}
EVENT_LOG_RACE_REPRO_POKE_INTERVAL_MS: ${{ inputs.poke_interval_ms }}
EVENT_LOG_RACE_REPRO_RUN_TIMEOUT_MS: ${{ inputs.run_timeout_ms }}
EVENT_LOG_RACE_REPRO_BUDGET_MS: ${{ inputs.budget_ms }}

- name: Fetch previous repro comment (world-${{ matrix.world }})
if: always() && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api \
"repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments?per_page=100" \
--jq '[.[] | select(.body | contains("<!-- event-log-race-repro-results-world-${{ matrix.world }} -->"))][-1].body // ""' \
> event-log-race-repro-previous-comment.md

- name: Render repro summary
if: always()
run: |
previous_comment_args=()
if [ -f event-log-race-repro-previous-comment.md ]; then
previous_comment_args=(--previous-comment event-log-race-repro-previous-comment.md)
fi

node .github/scripts/render-event-log-race-repro-results.js \
event-log-race-repro-results.json \
--label "world-${{ matrix.world }}" \
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--run-attempt "${{ github.run_attempt }}" \
--timestamp "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
"${previous_comment_args[@]}" \
| tee event-log-race-repro-summary.md >> "$GITHUB_STEP_SUMMARY"

- name: Update PR comment
if: always() && github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
with:
header: event-log-race-repro-results-world-${{ matrix.world }}
path: event-log-race-repro-summary.md

- name: Upload repro results
if: always()
uses: actions/upload-artifact@v4
with:
name: event-log-race-repro-results-world-${{ matrix.world }}
path: |
event-log-race-repro-results.json
event-log-race-repro-summary.md
event-log-race-repro-server.log
retention-days: 7
if-no-files-found: ignore

# Plumbing gate only: regressions are reported, not gated (see the job
# comment), but a run that produced no result file at all is a broken
# lane, and a broken lane that stays green never gets fixed.
- name: Fail if the harness produced no results
if: always()
run: test -f event-log-race-repro-results.json
10 changes: 5 additions & 5 deletions .github/workflows/world-sim.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ name: World Sim
# Plays the deterministic scenario book (`workbench/sim-world`) against the
# runtime in this commit, once per log world, and publishes the two summaries.
#
# This lane never blocks a merge, by design. Six scenarios in the book fail on
# This lane never blocks a merge, by design. Three scenarios in the book fail on
# purpose: each one is a reproduction of a corruption the runtime can still
# produce, stating the outcome its own durable log implies, and staying red
# until the runtime gets there. A gate that goes red on every PR is a gate
# everyone learns to ignore, so the job publishes numbers instead of verdicts —
# and the number to watch is in the comment, not the check mark.
#
# mint-ordered (production): 35 passed, 6 failed, 6 violations
# mint-ordered (production): 38 passed, 3 failed, 3 violations
# append-only: 41 passed, 0 failed, 0 violations
#
# A seventh red is a regression. Five means something got fixed and a scenario
# A fourth red is a regression. Two means something got fixed and a scenario
# is ready to retire. The append-only column is the measurement the pair exists
# for: it says which of the six would close if event positions were assigned at
# commit instead of at the handler's mint.
# for: it says which of the three would close if event positions were assigned
# at commit instead of at the handler's mint.

on:
push:
Expand Down
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,13 @@ In CI the same harness runs from `.github/workflows/event-log-race-repro.yml`,
triggered by adding the `event-log-race-repro` label to a PR (or by
`workflow_dispatch`, whose inputs are the soak dial — raise `timeout-minutes` in
that dispatch's branch if you raise `budget_ms`). Results land in a sticky PR
comment that keeps a history of previous runs and their configs.
comment that keeps a history of previous runs and their configs. Alongside the
Vercel lane, the workflow runs the local script against world-local and
world-postgres as parallel lanes, each with its own sticky comment. Those two
lanes are report-only — the local storms have red baselines at the default
scale (see above), so they publish numbers rather than a verdict and fail only
when the harness produced no result file at all; the Vercel lane remains the
gate.

To poke at a run afterwards, the CLI reads the same world from the environment:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ related:

`PreconditionFailedError` is thrown by world implementations when an event creation is rejected because the client's event-log snapshot is stale: the log already held more events than the position the creation named. It corresponds to HTTP 412 Precondition Failed semantics.

This only occurs against a world that fences on that position (`capabilities.preconditionGuard` — see [Stale-write rejection](/docs/configuration/runtime-tuning#stale-write-rejection)); event creations that carry no position are never rejected with this error.
No world in this repository throws it. A stale replay does not need to be refused: its log is a prefix rather than a prefix with a hole in it, replay is deterministic on a prefix, and the write it makes next comes back carrying the events it was pushed past (see [Stale reads](/docs/configuration/runtime-tuning#stale-reads-and-why-nothing-has-to-be-rejected)). The error and the runtime's handling of it remain for a world that would rather refuse than report — one that allocates positions somewhere other than the commit, and so cannot report a gap reliably. Event creations that carry no position are never rejected with it.

A world rejects only on evidence and accepts the creation whenever it cannot decide, so this error always means the snapshot really was stale — but not receiving it does not prove the snapshot was current.

Expand Down
Loading
Loading