Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
3610a91
docs: add ADR 0057 — replay outbox messages on inbox duplicate detection
iancooper Apr 17, 2026
6123133
docs: add approved tasks for spec 0027 — replay outbox on inbox dupli…
iancooper Apr 17, 2026
7dead7b
chore: add PROMPT.md to .gitignore
iancooper Apr 20, 2026
d19362c
update progress files, switching branches
iancooper Apr 20, 2026
0daaadb
Merge branch 'master' into replay_on_seen
iancooper Apr 20, 2026
fafc28c
Merge branch 'master' into replay_on_seen
iancooper Jun 17, 2026
9cc9449
Merge branch 'master' into replay_on_seen
iancooper Jun 23, 2026
803950a
docs(spec-0027): adopt BoxProvisioning for CausationId schema; split …
iancooper Jun 17, 2026
4dd52fb
refactor(spec-0027): UseInboxHandler uses pipeline Context; expose In…
iancooper Jun 17, 2026
666a225
refactor(spec-0027): add Attribute property to PipelineStepDescription
iancooper Jun 17, 2026
41c83b7
refactor(spec-0027): Describe() injects global inbox attributes for p…
iancooper Jun 17, 2026
3b514c6
refactor(spec-0027): expose Outbox from IAmAnOutboxProducerMediator
iancooper Jun 23, 2026
ccb257d
feat(spec-0027): add OnceOnlyAction.Replay, CausationId constants, ro…
iancooper Jun 17, 2026
2c7ee9f
feat(spec-0027): InMemoryInbox stores and retrieves CausationId
iancooper Jun 17, 2026
8f79f16
feat(spec-0027): InMemoryOutbox stores CausationId and replays by cle…
iancooper Jun 17, 2026
1faf00a
feat(spec-0027): sync UseInboxHandler generates CausationId on first …
iancooper Jun 17, 2026
e1a9042
feat(spec-0027): async UseInboxHandlerAsync generates CausationId on …
iancooper Jun 17, 2026
113441a
feat(spec-0027): sync UseInboxHandler replays outbox on duplicate (Re…
iancooper Jun 17, 2026
2fd1e5d
feat(spec-0027): async UseInboxHandlerAsync replays outbox on duplica…
iancooper Jun 17, 2026
8914306
test(spec-0027): Replay with no outbox is a graceful terminal step (s…
iancooper Jun 18, 2026
49f2c9b
feat(spec-0027): pipeline validation rejects Replay without causation…
iancooper Jun 18, 2026
999a367
feat(spec-0027): UseInboxHandler adds Replay telemetry event to pipel…
iancooper Jun 18, 2026
6085d5b
refactor(spec-0027): remove redundant protected InstrumentationOption…
iancooper Jun 18, 2026
a5bacc7
feat(spec-0027): UseInboxHandler adds Throw/Warn/Add telemetry events…
iancooper Jun 18, 2026
46fc06d
feat(spec-0027): register IAmACausationTrackingOutbox under role inte…
iancooper Jun 18, 2026
87650c5
docs(spec-0027): update PROMPT.md resume state — Task 17 done, next T…
iancooper Jun 18, 2026
ee60b2b
feat(spec-0027): base test classes for causation tracking (Task 18)
iancooper Jun 18, 2026
647038a
feat(spec-0027): relational inbox CausationId schema via BoxProvision…
iancooper Jun 19, 2026
caceb9a
feat(spec-0027): MsSql inbox implements IAmACausationTrackingInbox (T…
iancooper Jun 19, 2026
16a90d6
docs(spec-0027): update PROMPT.md resume state — Task 19b done, next …
iancooper Jun 19, 2026
bca7cc0
feat(spec-0027): MySql inbox implements IAmACausationTrackingInbox (T…
iancooper Jun 19, 2026
949391b
docs(spec-0027): update PROMPT.md resume state — Task 19c done, next …
iancooper Jun 19, 2026
d32a5cd
chore: stop tracking PROMPT.md (AI session scratch, already in .gitig…
iancooper Jun 19, 2026
daffab0
feat(spec-0027): Postgres inbox implements IAmACausationTrackingInbox…
iancooper Jun 19, 2026
51f575e
feat(spec-0027): Sqlite inbox implements IAmACausationTrackingInbox (…
iancooper Jun 19, 2026
abf9cf8
feat(spec-0027): Spanner inbox implements IAmACausationTrackingInbox …
iancooper Jun 19, 2026
2c0f8e4
feat(spec-0027): NoSQL inbox stores implement IAmACausationTrackingIn…
iancooper Jun 19, 2026
c78f564
feat(spec-0027): relational outbox CausationId schema + replay index …
iancooper Jun 20, 2026
8efd836
feat(spec-0027): extend Liquid generator for causation-tracking outbo…
iancooper Jun 20, 2026
abbc71a
feat(spec-0027): MsSql outbox implements IAmACausationTrackingOutbox …
iancooper Jun 20, 2026
761ec68
feat(spec-0027): MySql outbox implements IAmACausationTrackingOutbox …
iancooper Jun 20, 2026
4dcac41
feat(spec-0027): PostgreSql outbox implements IAmACausationTrackingOu…
iancooper Jun 20, 2026
7832669
feat(spec-0027): Sqlite outbox implements IAmACausationTrackingOutbox…
iancooper Jun 21, 2026
1ad8184
feat(spec-0027): Spanner outbox implements IAmACausationTrackingOutbo…
iancooper Jun 21, 2026
f212625
feat(spec-0027): MongoDb + DynamoDB(+V4) outbox implement IAmACausati…
iancooper Jun 22, 2026
52a1ef1
feat(spec-0027): Firestore outbox implements IAmACausationTrackingOut…
iancooper Jun 22, 2026
fbfea68
chore(spec-0027): Task 23 build verification — Core.Tests 818/818 bot…
iancooper Jun 22, 2026
f10cba5
chore(spec-0027): set active spec to replay-matching-outbox-events
iancooper Jun 22, 2026
81a5825
docs(spec-0027): backward-compat hardening — write-path gate + Dynamo…
iancooper Jun 22, 2026
47eb6b9
feat(spec-0027): Task 24a — gate causation write on memoized column p…
iancooper Jun 22, 2026
abbfeed
test(spec-0027): Task 24b — MsSql legacy-schema causation backward-co…
iancooper Jun 22, 2026
eab7a04
test(spec-0027): Task 24c — MySql legacy-schema causation backward-co…
iancooper Jun 22, 2026
0e246bb
test(spec-0027): Task 24d — Postgres legacy-schema causation backward…
iancooper Jun 22, 2026
8a39a38
test(spec-0027): Task 24e — Spanner legacy-schema causation backward-…
iancooper Jun 22, 2026
dee4aba
feat(spec-0027): Task 25 — DynamoDB outbox honestly probes the Causat…
iancooper Jun 22, 2026
87a45ae
docs(spec-0027): Task 26 — NoSQL backward-compat confidence satisfied…
iancooper Jun 22, 2026
cdb1bc1
chore(spec-0027): Task 27 — regression verification (backward-compat …
iancooper Jun 22, 2026
03a731a
fix(spec-0027): degrade causation-tracking probe failure to a Warning…
iancooper Jun 22, 2026
420ccfb
test(spec-0027): cover After-step Replay placement and Warn action in…
iancooper Jun 22, 2026
30bcc7f
chore(spec-0027): clear low-severity review findings #2/#3/#5/#6
iancooper Jun 23, 2026
004dd90
fix(spec-0027): thread CausationId through relational bulk Add (PR #4…
iancooper Jun 23, 2026
b207ca8
merge issue
iancooper Jun 23, 2026
62246e8
fix(spec-0027): thread CausationId through Mongo/Firestore bulk Add; …
iancooper Jun 24, 2026
995e8b2
docs(spec-0027): clarify memoized-probe non-synchronization and defer…
iancooper Jun 24, 2026
962cb30
test(spec-0027): end-to-end replay-on-seen over the InternalBus (PR #…
iancooper Jun 25, 2026
5f966d8
fix(spec-0027): address PR #4067 review (gitignore, pipeline reuse, s…
iancooper Jun 27, 2026
604bf45
docs(spec-0027): note IRequestContext.InstrumentationOptions as a sou…
iancooper Jun 27, 2026
98b4098
Merge branch 'master' into replay_on_seen
iancooper Jul 2, 2026
a669a2f
test(spec-0027): regenerate causation outbox tests after merge (add C…
iancooper Jul 2, 2026
82aecae
fix(spec-0027): bind causation outbox param as typed String on all pa…
iancooper Jul 3, 2026
5fa193b
fix(spec-0027): gate inbox GetCausationId on live column probe (PR #4…
iancooper Jul 3, 2026
778b36e
docs(spec-0027): document Replay RequestContext footgun and DynamoDB …
iancooper Jul 3, 2026
a838898
fix(spec-0027): gate relational ReplayCausation on live column probe …
iancooper Jul 4, 2026
2432f8a
fix(spec-0027): DynamoDB replay catch-and-continue on vanished item (…
iancooper Jul 4, 2026
55add15
test(spec-0027): cover causation-id preservation/chaining (PR #4067 r…
iancooper Jul 4, 2026
ef7a188
fix(spec-0027): make MsSql fresh-install causation index idempotent (…
iancooper Jul 4, 2026
7c70ae5
docs(spec-0027): document probe/timeout limitations and IRequestConte…
iancooper Jul 4, 2026
e4f5b8a
fix(spec-0027): capture inbox span before await, warn on custom-conte…
iancooper Jul 6, 2026
d9f871c
add review notes
iancooper Jul 6, 2026
0c9f75b
Merge branch 'master' into replay_on_seen
iancooper Jul 6, 2026
c530380
fix(spec-0027): guard DynamoDB replay against a missing causation GSI…
iancooper Jul 6, 2026
3f80a49
fix(spec-0027): report replay no-op distinctly in telemetry (PR #4067…
iancooper Jul 7, 2026
d4c2121
Apply suggestions from code review
iancooper Jul 8, 2026
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
6 changes: 4 additions & 2 deletions .agent_instructions/generated_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ tools/Paramore.Brighter.Test.Generator/
│ ├── IAmAMessageAssertion.cs.liquid ← Message assertion interface
│ ├── Outbox/
│ │ ├── Sync/ ← 13 Liquid templates for sync outbox tests (12 tests + 1 interface)
│ │ └── Async/ ← 13 Liquid templates for async outbox tests (12 tests + 1 interface)
│ │ ├── Async/ ← 13 Liquid templates for async outbox tests (12 tests + 1 interface)
│ │ └── Causation/ ← Liquid template for causation-tracking outbox tests (subclass of CausationTrackingOutboxBaseTests)
│ └── MessagingGateway/
│ ├── Reactor/ ← 13 Liquid templates for sync messaging gateway tests (12 tests + 1 interface)
│ └── Proactor/ ← 13 Liquid templates for async messaging gateway tests (12 tests + 1 interface)
Expand All @@ -37,7 +38,8 @@ tests/Paramore.Brighter.*.Tests/
├── Outbox/
│ └── [Prefix]/Generated/ ← Output directory (do not hand-edit)
│ ├── Sync/*.cs
│ └── Async/*.cs
│ ├── Async/*.cs
│ └── Causation/*.cs
└── MessagingGateway/
└── [Prefix]/Generated/ ← Output directory (do not hand-edit)
├── Reactor/*.cs
Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# Claude Code session state
PROMPT.md

# User-specific files
*.suo
*.user
Expand Down
661 changes: 661 additions & 0 deletions docs/adr/0057-replay-outbox-on-inbox-duplicate.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,45 @@ While applying the value-type pattern we corrected a latent null-safety bug in n

> Note: `Tenant` (in `Paramore.Brighter.Transformers.JustSaying`) is a `readonly record struct`, not a reference type — its receiver can never be null, so its `operator string` is intentionally left non-nullable.

### Replay Outbox Messages on Inbox Duplicate (spec 0027)

When an inbox detects a duplicate request, Brighter can now optionally **replay** the outbox messages that were produced under that request's causation, rather than silently dropping the duplicate. The feature is opt-in (`OnceOnlyAction.Replay` on the inbox attribute) and non-breaking: it requires a causation-tracking inbox and outbox (`IAmACausationTrackingInbox` / `IAmACausationTrackingOutbox`), and the relational stores gate the causation-aware write on a memoized column probe so un-migrated schemas keep depositing unchanged. Startup pipeline validation fails fast if a `Replay` pipeline is configured without causation tracking. See [ADR 0057](docs/adr/0057-replay-outbox-on-inbox-duplicate.md) and [spec 0027](specs/0027-replay-matching-outbox-events-when-inbox-has-already-seen/) for full details.

#### Usage requirement: thread your `RequestContext` through `Post` / `DepositPost`

Replay links a duplicate back to its original outbox messages through the **causation id**. `[UseInbox]` stamps that id into the pipeline's `RequestContext.Bag`, and the outbox `Add` reads it back from the bag — **but only if the handler that deposits the outbox message passes its own `Context` down**. If a handler calls the idiomatic `_commandProcessor.Post(evt)` (or `DepositPost`) *without* a request context, `CommandProcessor` creates a fresh context, the causation id is lost, the message is stored with a `null` `CausationId`, and a later `Replay` finds nothing to replay — a **silent no-op** with no error.

To use Replay, every handler that produces outbox messages must forward its handler `Context`:

```csharp
// ❌ Silent no-op under Replay — fresh context, causation id lost
public override MyCommand Handle(MyCommand command)
{
_commandProcessor.Post(new DownstreamEvent(...));
return base.Handle(command);
}

// ✅ Threads the causation id so Replay can match
public override MyCommand Handle(MyCommand command)
{
_commandProcessor.Post(new DownstreamEvent(...), Context);
return base.Handle(command);
}
```

This applies to the async equivalents (`PostAsync` / `DepositPostAsync`) as well.

#### Source-breaking change: `IRequestContext.InstrumentationOptions`

`IRequestContext` gains a new required `InstrumentationOptions InstrumentationOptions { get; set; }` member. It carries the instrumentation options configured for the pipeline that created the context, so middleware handlers can gate their own telemetry (for example on `InstrumentationOptions.Brighter`) without taking a dependency on how the processor was configured. `Paramore.Brighter` targets `netstandard2.0`, which does not support default interface members, so — as with the spec-0027/0029 box-provisioning interface additions — this is exposed as a plain abstract member.

The change is **source-breaking** for any third-party or test type that implements `IRequestContext`: such types will fail to compile until they add the new member. It affects more consumers than just those adopting Replay, hence its call-out here. The shipped `RequestContext` already implements it and defaults to `InstrumentationOptions.All`, so call sites that use the shipped context require no change.

```csharp
// Custom IRequestContext implementations must add:
public InstrumentationOptions InstrumentationOptions { get; set; } = InstrumentationOptions.All;
```

## Release 10.0.0

With V10 we have made a number of significant changes to Brighter. There are breaking changes that you will need to be aware of. However, most of the changes required are straightforward to make. A summary of the most important changes:
Expand Down
2 changes: 1 addition & 1 deletion specs/.current-spec
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0035-pipeline-cache-type-key
0027-replay-matching-outbox-events-when-inbox-has-already-seen
1 change: 0 additions & 1 deletion specs/.current_spec

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Review: tasks — 0022-Defer-Message-Action-Backstop-Handler

**Date**: 2026-04-17
**Threshold**: 60
**Verdict**: NEEDS WORK

> **Note**: This phase is already approved. Findings are informational — consider whether any warrant re-opening the phase.

1 finding at or above threshold 60. Address these before approving.

## Findings

### 1. Task 7 only tests Proactor path but implementation modifies both Reactor and Proactor (Score: 70)

Task 7 specifies a single test file in `tests/Paramore.Brighter.Core.Tests/MessageDispatch/Proactor/` and says the implementation touches both `Proactor.cs` and `Reactor.cs`. However, the task only has one test file covering the Proactor (async) path. There is no corresponding test for the Reactor (sync) path with the delay override.

**Evidence**: Task 7 says:
> "In `Proactor.cs`: Change `catch (DeferMessageAction)` to `catch (DeferMessageAction deferAction)`, extract `deferAction.Delay`, pass to `RequeueMessage`"
> "In `Reactor.cs`: Same change for the sync path"

But only one test file is listed: `When_a_command_handler_throws_a_defer_message_with_delay_Then_message_is_requeued_with_delay.cs` in the `Proactor/` directory.

**Recommendation**: Add a separate test task (or expand Task 7) to include a Reactor-path test: `tests/Paramore.Brighter.Core.Tests/MessageDispatch/Reactor/When_a_command_handler_throws_a_defer_message_with_delay_Then_message_is_requeued_with_delay.cs`.

---

### 2. Task 7 test verifies only the "with delay" case; the "null delay fallback" case is described but has no separate test file (Score: 55)

Task 7 bullet says to verify two behaviors: (1) when `Delay` is set, pump uses it, and (2) when `Delay` is null, pump falls back to subscription `RequeueDelay`. Only one test file is specified, which presumably covers both cases in a single test class.

**Evidence**: Task 7 lists:
> "When handler throws `DeferMessageAction` with a `Delay` value, the pump passes that delay to `Channel.RequeueAsync`"
> "When handler throws `DeferMessageAction` without a `Delay` (null), the pump falls back to subscription `RequeueDelay`"

But only one test file is named.

**Recommendation**: Consider splitting into two test files or at minimum ensure the single test file clearly has two separate test methods.

---

### 3. Task 1 describes work already completed in codebase (Score: 45)

All tasks appear to be already implemented in the codebase (all four types exist, all test files exist, pump changes are in place), but all checkboxes remain unchecked.

**Evidence**: `DeferMessageAction.cs` already has `TimeSpan? Delay`, all constructors, and all files referenced in Tasks 2-7 exist. All checkboxes are `- [ ]`.

**Recommendation**: Mark completed tasks with `[x]` to reflect actual state.

---

### 4. Branch name in header is stale (Score: 30)

The tasks.md header says `**Branch**: \`error_examples\` (or new feature branch)` but the current branch is `replay_on_seen`.

**Evidence**: Line 6: `**Branch**: \`error_examples\` (or new feature branch)`

**Recommendation**: Update the branch name or remove it.

---

## Summary

| Score Range | Count |
|-------------|-------|
| 90-100 (Critical) | 0 |
| 70-89 (High) | 1 |
| 50-69 (Medium) | 1 |
| 0-49 (Low) | 2 |

**Total findings**: 4
**Findings at or above threshold (60)**: 1
Empty file.
81 changes: 81 additions & 0 deletions specs/0026-timed-outbox-archiver-sync-fallback/review-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Review: tasks — 0026-timed-outbox-archiver-sync-fallback

**Date**: 2026-04-15
**Threshold**: 60
**Verdict**: NEEDS WORK

3 findings at or above threshold 60. Address these before approving.

## Findings

### 1. Implementation file path for TimedOutboxArchiver not specified (Score: 72)

Task 2 describes implementing a three-way branch in `TimedOutboxArchiver.Archive()`, adding a `NoOutboxConfigured` log message, and removing a duplicate `OutboxSweeperSleeping` call. However, the task only specifies the file path for the TIDY step (`src/Paramore.Brighter/OutboxArchiver.cs`). The actual implementation changes are in a different project/file: `src/Paramore.Brighter.Outbox.Hosting/TimedOutboxArchiver.cs`.

**Evidence**: Task 2 implementation bullet points reference `TimedOutboxArchiver.Archive()` and `Log` partial class changes but never state which file to edit. Task 1 explicitly lists `File: src/Paramore.Brighter/OutboxArchiver.cs` but no equivalent line appears for Tasks 2-4.

**Recommendation**: Add `File: src/Paramore.Brighter.Outbox.Hosting/TimedOutboxArchiver.cs` to the implementation section of Task 2.

---

### 2. FR1 (async path unchanged) has no dedicated test task (Score: 65)

FR1 states "When an async outbox is available, `TimedOutboxArchiver` calls `ArchiveAsync` (current behavior, unchanged)." Task 3 partially covers this with AC3 (prefers async when both are available), but there is no test for the scenario where ONLY an async outbox is registered (no sync). The three-way branch introduces a regression risk for async-only outboxes.

**Evidence**: FR1 says "current behavior, unchanged." Task 3 tests "both sync and async" but not "async only via TimedOutboxArchiver."

**Recommendation**: Either add a test task for async-only outbox through `TimedOutboxArchiver`, or explicitly document why existing coverage is sufficient.

---

### 3. NoOutboxConfigured log message location ambiguous (Score: 62)

Task 2 says "Add `NoOutboxConfigured` log message to the `Log` partial class" but both `TimedOutboxArchiver` and `OutboxArchiver` have their own `Log` partial class. The call site is in `TimedOutboxArchiver` but this is not stated explicitly.

**Evidence**: Task 2 implementation says `Log.NoOutboxConfigured(s_logger)` but `s_logger` exists in both classes.

**Recommendation**: Specify: "Add `NoOutboxConfigured` log message to the `Log` partial class in `TimedOutboxArchiver.cs`."

---

### 4. OutboxArchiver is generic but test setup doesn't mention type parameters (Score: 55)

`OutboxArchiver<TMessage, TTransaction>` requires two generic type parameters. The test descriptions don't mention what concrete types to use.

**Evidence**: `public partial class OutboxArchiver<TMessage, TTransaction> where TMessage : Message` at line 40 of `OutboxArchiver.cs`.

**Recommendation**: Specify the concrete generic types to use in tests based on existing archiving test patterns.

---

### 5. ADR decision to remove duplicate OutboxSweeperSleeping is bundled into behavioral task (Score: 45)

The duplicate `OutboxSweeperSleeping` log removal is a structural/tidy change bundled into behavioral Task 2, contrary to tidy-first philosophy.

**Evidence**: Task 2 implementation says "Remove duplicate `Log.OutboxSweeperSleeping` call (keep only the one in `finally`)."

**Recommendation**: Move to Step 1 as a separate TIDY task.

---

### 6. Task numbering inconsistency in dependency chain (Score: 40)

The dependency chain references "Task 1 → Task 2 → Task 3 → Task 4" but there are only 3 bullet-pointed tasks in Step 2.

**Evidence**: `Task 1 (TIDY) → Task 2 (sync fallback) → Task 3 (async preference) → Task 4 (no outbox warning)` but document has Step 1 (1 task) + Step 2 (3 tasks).

**Recommendation**: Number tasks explicitly or fix the dependency chain.

---

## Summary

| Score Range | Count |
|-------------|-------|
| 90-100 (Critical) | 0 |
| 70-89 (High) | 1 |
| 50-69 (Medium) | 3 |
| 0-49 (Low) | 2 |

**Total findings**: 6
**Findings at or above threshold (60)**: 3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0057-replay-outbox-on-inbox-duplicate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2541
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Approved: 2026-04-16
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 0027 - Replay Matching Outbox Events When Inbox Has Already Seen

**Created:** 2026-04-15

## Status

- [x] Requirements (`requirements.md`) - Approved 2026-04-16
- [x] Design (`docs/adr/0057-replay-outbox-on-inbox-duplicate.md`) - Ready for review
- [ ] Tasks (`tasks.md`)
- [ ] Implementation
- [ ] Review
Loading
Loading