Repository navigation
Replay Outgoing Messages When Inbox Receives Duplicate - #4067
Conversation
Introduces the design for CausationId tracking and OnceOnlyAction.Replay, enabling automatic replay of downstream outbox messages when the inbox detects a duplicate command. Accepted after 7 rounds of adversarial review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cate Create and approve implementation task list (23 tasks) for the replay outbox messages feature. Tasks cover structural prerequisites, core behavior (CausationId propagation, UseInboxHandler replay logic, pipeline validation), telemetry, DI registration, and all 18 persistent store implementations. Minor ADR clarification for ReplayCausation semantics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review — PR #4067: Replay Outgoing Messages When Inbox Receives DuplicateThis PR contains design and specification documents only — no implementation code yet. The changes are: 🔴 Critical:
|
| Area | Status |
|---|---|
PROMPT.md in repo root |
❌ Should be removed |
| ADR quality and completeness | ✅ Well-written |
| Interface design | SupportsCausationTracking async variant worth revisiting |
| Tidy-first discipline | ✅ Correctly scoped |
| Task list TDD discipline | ✅ Follows project /test-first conventions |
| Scope creep (Task 16) | |
| Persistent store migration dependency |
The core design is sound. The main ask is removing PROMPT.md before merge; the interface and scope observations are lower priority suggestions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review — PR #4067: Replay Outgoing Messages When Inbox Receives DuplicateOverviewThis PR is a specification and design phase PR only — it contains requirements, ADR 0057, and task definitions for the Replay-on-Inbox-Duplicate feature. No production code has been written yet (the PROMPT.md explicitly states "Implementation — not started"). The 1606 additions are almost entirely documentation and spec artefacts. Issues🔴 PROMPT.md Should Not Be Merged
Recommendation: Remove 🟡 README.md for Spec 0027 Shows Tasks as Incomplete
...but Recommendation: Update the README checkbox to 🟡 Inconsistent
|
| Area | Status |
|---|---|
PROMPT.md in commit history |
❌ Remove before merge |
specs/0027/README.md tasks checkbox |
[x] |
| ADR 0057 design quality | ✅ Solid |
SupportsCausationTracking() startup I/O cost |
|
| Requirements completeness | ✅ Good — minor typos only |
| Non-breaking / opt-in design | ✅ Correct approach |
The design is sound and the specification artefacts are high quality. The main blocker before merging is removing PROMPT.md from the branch history.
Review generated by Claude Code
Code Review — PR #4067: Replay Outgoing Messages When Inbox Receives Duplicate
What's in this PR
Issues
|
| Area | Status |
|---|---|
PROMPT.md in repo |
Must fix — remove from git tracking |
| ADR structure & completeness | Good — minor gaps noted |
DescribePipelines() gap |
Low priority — fix in Task 3 |
SupportsCausationTracking() startup cost |
Needs documentation |
| Race condition clarity | Needs a note on downstream idempotency requirement |
| Requirements typos | Trivial — worth fixing |
| Task 14 scenario structure | Clarify [Theory] expectation in task |
| Task 23 test scope | Enumerate specific suites |
| Schema migration PR dependency | Track explicitly |
The design itself is sound — the role interface approach (IAmACausationTrackingInbox/Outbox) is non-breaking and consistent with Brighter's existing patterns. The decision to use the outbox sweeper for re-dispatch rather than building a new dispatch path is the right call. The CausationId concept is well-scoped and distinct from CorrelationId/JobId.
# Conflicts: # .gitignore # specs/.current-spec # specs/.current_spec
Review — PR #4067: Replay Outgoing Messages When Inbox Receives DuplicateThanks for the very thorough design work — ADR 0057 is well structured (clear Forces, Alternatives Considered, Consequences, Risks table), the role-interface approach ( Most of my feedback is about scope/hygiene of this PR, since it ships only design+spec docs — no production code or tests yet. 🔴 Blocking / should-resolve-before-merge
🟡 Design observations (for the implementation PR)These don't block a docs PR but are worth tracking against the tasks:
🟢 Nits
Test coverageN/A for this PR (docs only). The task list correctly front-loads in-memory Summary: The design is strong and review-ready. Before merge I'd (1) fix 🤖 Generated with Claude Code |
PR Review — #4067 Replay Outgoing Messages When Inbox Receives DuplicateReviewed against the repo's Scope of the changeThis PR touches 0 files under
That's consistent with the spec workflow (Requirements → ADR → Tasks, all approved), but the PR is framed as the feature itself. See the first blocking point. 🔴 Blocking / should-fix
🟡 Minor
Design assessment (ADR 0057)The ADR is genuinely strong — 7 review rounds show. I spot-checked its claims against the current code and they hold up:
The opt-in / non-breaking strategy, role interfaces ( Two things to watch during implementation:
Summary: The design is approved-quality and the task breakdown is thorough. Before merge, please address the four blocking items — most importantly, decide whether this is the spec-only PR (then don't 🤖 Generated with Claude Code |
…relational store tasks
Fold the inbox/outbox CausationId schema change into this spec via
BoxProvisioning instead of a deferred separate PR:
- requirements.md: schema evolution is in-scope via BoxProvisioning
(data backfill stays out of scope); AC9 + Performance NFR now assert
the column, the outbox CausationId index, and SupportsCausationTracking().
- ADR 0057: new "Schema Evolution via BoxProvisioning" section covering the
three store classes (catalog-based relational, Spanner provisioner, NoSQL
schemaless); per-backend catalog versions (outbox V8; inbox V3 for
MsSql/MySql/Sqlite, V2 for the V1-only Postgres inbox); Spanner
VLatestOutbox/VLatestInbox constant bumps + cross-backend drift test;
Spanner SupportsCausationTracking() as a live column probe.
- tasks.md: split Task 19 -> 19a (atomic structural schema) + 19b-19f
(per-backend IAmACausationTrackingInbox, test-first); Task 21 -> 21a
(atomic structural schema + index) + 21b (Liquid generator) + 21c-21g
(per-backend IAmACausationTrackingOutbox, test-first). The schema layer is
atomic because SpannerVLatestDriftAgainstRelationalCatalogTests pins
VLatest* to every relational catalog count.
Includes the 2026-06-17 focused re-review findings
(review-{requirements,design,tasks}.md); all threshold findings addressed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…strumentationOptions Task 1 (structural/tidy-first): - Part A: UseInboxHandler/UseInboxHandlerAsync use the pipeline's shared IRequestContext (this.Context) instead of a private InitRequestContext(), so RequestContext.Bag is shared across the pipeline (prereq for CausationId). - Part B: expose instrumentationOptions as a protected InstrumentationOptions property on RequestHandler<T>/RequestHandlerAsync<T> so derived handlers can gate telemetry on InstrumentationOptions.Brighter. - Tidy: remove duplicate base.InitializeFromAttributeParams() call in async handler. Behaviour-preserving: OnceOnly tests 10/10 green (net9.0 + net10.0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 2 (structural/tidy-first): non-positional RequestHandlerAttribute? Attribute init-property on PipelineStepDescription, populated at both Describe() construction sites, so validation rules can inspect attribute properties (e.g. OnceOnlyAction). Behaviour-preserving: Validation tests 90/90 green (net9.0 + net10.0). Also ticks Task 1 in tasks.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arity with Build() Task 3 (structural/tidy-first): - PipelineBuilder.Describe() now injects the global inbox UseInbox(Async)Attribute for handlers, using the same guards as Build()'s AddGlobalInboxAttributes (HasNoInboxAttributesInPipeline / HasExistingUseInboxAttributesInPipeline) via a reflection-only TryCreateGlobalInboxAttribute helper, so descriptions don't drift. - BrighterPipelineValidationExtensions resolves InboxConfiguration from IAmConsumerOptions and passes it to the validate/describe PipelineBuilder (null when no consumer options, matching the runtime). Behaviour-preserving: Validation + Pipeline tests 131/131 green (net9.0 + net10.0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 4 (structural/tidy-first): add read-only IAmAnOutbox? Outbox to the mediator interface and implement it in OutboxProducerMediator as the sync or async outbox, so pipeline validation can inspect the outbox (e.g. for causation-tracking support). Behaviour-preserving: core + core-tests build clean (net9.0 + net10.0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> # Conflicts: # src/Paramore.Brighter/OutboxProducerMediator.cs
#4067 review) SqlOutboxBuilder.GetDDL emitted a bare CREATE INDEX [idx_..._CausationId] with no guard, whereas the V8 migration catalog uses a sys.indexes IF NOT EXISTS + EXEC('CREATE INDEX ...') form and the Sqlite/Postgres/Spanner builders use CREATE INDEX IF NOT EXISTS. Safe today only because the MsSql CREATE TABLE is itself unguarded (so GetDDL runs on a true fresh install), but it was the odd one out. Wrap the index in the same sys.indexes guard for parity so re-running the DDL is idempotent; EXEC-wrapped so it compiles at run time. Closes review finding #7. Existing builder-vs-migration assertions are substring (Contains idx_..._ CausationId) and stay green. MsSql outbox project builds clean net9.0; DB-backed run deferred (no MSSQL container this session). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xt break (PR #4067 review) Documentation-only follow-ups from the review: - #6: note on IRelationalDatabaseOutboxCausationQueries.CausationColumnExistsCommand that the backend probes (OBJECT_ID / to_regclass / information_schema) resolve against the connection default schema, so a box table in a non-default schema can read as "column absent" -- a shared limitation of the unqualified box SQL, not a regression. - #8: remarks on DynamoDbInbox.GetCausationId(Async) that timeoutInMilliseconds is accepted for interface compatibility but ignored (DynamoDB handles timeout/retries), consistent with the other methods on the store. - #9: remarks on IRequestContext.InstrumentationOptions + an ADR 0057 Negative- consequences note that adding the member is source/binary-breaking for external implementers (no default interface impl) and should ship in a minor bump. #5 (ADR context_key tag) needed no change: the observability table already lists only request.id / causation_id, matching the emitted tags. No behaviour change. Core + DynamoDB inbox projects build clean net9.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review: Replay Outbox Messages on Inbox Duplicate (#4067)Reviewed the core mechanism ( A few things worth a look before merge. 1. Async telemetry events may be silently dropped on the duplicate paths (correctness, low severity — telemetry only)
Note the Add path deliberately captures 2. Default causation id is not unique per handler invocation (design)On first handling the causation id defaults to This is at odds with the ADR's framing that causation is "scoped to a single handler invocation." It is fine as long as callers who need isolation set a unique causation id in the bag themselves — but the default does not guarantee that scoping. Worth either documenting the caveat prominently or scoping the default (e.g. incorporating the context key) so the out-of-the-box behaviour matches the stated design. 3. Custom
|
…xt replay (PR #4067 review) Addresses three points from the PR #4067 code review: 1. Async telemetry events were silently dropped on the Throw/Warn/Replay duplicate paths: RequestContext.Span is thread-affine, and the branches re-read Context?.Span after the ExistsAsync await, so on a thread-pool continuation (ContinueOnCapturedContext == false) the span was null and the AddEvent never fired. Capture the span once before the first await and reuse it on every path (Throw/Warn/Replay/Add). Mirror the single capture in the sync handler for parity. 2. Default causation id is command.Id, so a message id handled by multiple [UseInbox] handlers shares it and a replay resends all of their outbox messages. This is intended (replay all downstream effects to recreate the original state); documented the caveat in ADR 0057, noting UseInbox is in practice applied to a single-handler Command, and the Bag escape hatch. 3. A custom IRequestContext (not a RequestContext) degrades Replay to a silent no-op. Emit a one-time Warning (CustomContextDisablesReplay) the first time the fallback is hit while OnceOnlyAction.Replay is configured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PR #4067: Replay outbox messages on inbox duplicate detectionReviewed the core feature ( A few points below, one worth addressing before merge. 1. (Medium) DynamoDB
|
Code Review — Replay Outgoing Messages When Inbox Receives Duplicate (#4067)Thanks for a substantial, well-structured feature. I reviewed the core mechanics (the sync/async What's strong
Things worth confirming / addressing1. Binary-breaking interface changes — release/versioning. 2. Log-level change is a behavior change beyond feature scope. 3. Causation-inheritance / replay-scope semantics. 4. Replay is unconditional and untransacted. 5. Validation runs a live DB probe at startup. Nits
None of these are blockers on correctness of the happy path, which looks sound end-to-end (bag stamped before Automated review — I built understanding from the diff and source; I was not able to run a local build in this environment, so I'm relying on CI for compile/test verification. |
Code Review: Replay Outgoing Messages When Inbox Receives Duplicate (#4067)I reviewed the core design and implementation (inbox/outbox handlers, the causation-tracking role interfaces, the relational + DynamoDB + in-memory stores, DI registration, and pipeline validation). This is a large but very well-executed change — the ADR-0057 write-up, the backward-compatibility gating, the telemetry, and the test coverage (base tests + generated + a genuine end-to-end pump test) are all excellent. Feedback below is mostly nuance and a couple of footguns worth surfacing. Strengths
Concerns worth a look1. The biggest footgun — silent no-op if 2. 3. Extra probe round-trip on first deposit for all relational users. 4. 5. Multi-subscriber causation collision. As the ADR notes, because the default causation id is Minor
TestsCoverage looks strong: base tests exercised across every persistent backend, generated causation outbox tests, validation tests (including the "replay configured without causation tracking" error case), telemetry assertions, and a real end-to-end pump test that posts a duplicate and asserts the outbox replays. The DynamoDB "vanished item" and legacy/un-migrated-schema paths are explicitly covered too. Nice. Overall: high-quality, well-documented work. Nothing here is a blocker; items 1 and 2 are the ones I'd most want addressed (or explicitly accepted) before merge. Reviewed with Claude Code. |
… (PR #4067 review) Addresses the second PR #4067 review: 1. DynamoDB ReplayCausationAsync (V1 and V4) queried the Causation GSI unconditionally. In the "inbox migrated, outbox not yet" mixed state (AC10) the Dynamo inbox is schemaless so GetCausationId returns a non-null id, the handler reaches ReplayCausationAsync, and a table provisioned before the Replay feature has no Causation GSI — so the query threw a ValidationException that unwound the handler pipeline on every duplicate. Gate replay on the memoized SupportsCausationTrackingAsync probe and degrade to a no-op, matching the relational path. Adds a regression test in both V1 and V4 that drives replay against a GSI-less table (6/6 green against DynamoDB Local). 3. Documented the sync-over-async in the NoSQL replay entry points (DynamoDbOutbox.ReplayCausation x2, FirestoreOutbox.ReplayCausation) with a comment explaining why they block on the async-only SDKs and that this matches the existing convention in those classes. (#2 no change: PROMPT.md is still ignored via the "AI agent session state" .gitignore entry; the branch only dropped a duplicate copy.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review: Replay outbox messages on inbox duplicate detection (#4067)Reviewed against the repo's Strengths
Observations / things to confirm
Correctness spot-checks that look right
Nice work — this is a substantial, careful feature. The main thing I'd want addressed before merge is item #1 (the telemetry can misreport a no-op replay as a successful one in the mixed-migration state); the rest are minor or confirm-and-document. Reviewed with Claude Code. |
…review #1) WriteReplayEvent chose "Duplicate Replay" vs "Duplicate Replay Skipped" solely on causationId being null. In the mixed-migration state (inbox migrated, outbox not) the inbox still returns a non-null causation id while ReplayCausation silently no-ops against the absent column/index, so the span recorded a successful replay when nothing was resent. Make ReplayCausation / ReplayCausationAsync return bool signalling whether a replay was actually performed (false = no-op because the live schema lacks causation tracking). Both handlers pass the result to WriteReplayEvent, which now emits the Skipped event when nothing was replayed. Updated all 6 outbox implementations and the validation test double; added sync+async handler tests for the mixed-state no-op and strengthened both DynamoDB mixed-state tests to assert the false return. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PR #4067: Replay outbox messages on inbox duplicate detectionReviewed the core library changes (I focused on Below are observations, mostly minor — nothing I'd consider a blocker. Design & correctness
Minor / nits
TestsTest coverage is a real strength — the base-test pattern ( Nice work overall — the concerns above are polish, not correctness blockers. |
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
|
Code Review — PR #4067: Replay outbox messages on inbox duplicate detection Reviewed against the design in One blocking concern and a few smaller notes. 🔴 Blocking — likely compile error in both inbox handlers
private static bool s_warnedAboutCustomContext; // line 51
...
if (_onceOnlyAction is OnceOnlyAction.Replay
&& Interlocked.CompareExchange(ref s_warnedAboutCustomContext, 1, 0) == 0)
The usual idiom is to make the flag an private static int s_warnedAboutCustomContext;(Note: I could not run 🟡 Minor / worth a look
✅ Things I checked and liked
Once the Automated review by Claude Code. |
| { | ||
| // Silent replay degradation is a PITA to diagnose, so warn (once) when a custom context means | ||
| // Replay cannot flow the causation id to the outbox and will therefore be a no-op. | ||
| if (_onceOnlyAction is OnceOnlyAction.Replay && Interlocked.CompareExchange(ref s_warnedAboutCustomContext, 1, 0) == 0) |
| // Silent replay degradation is a PITA to diagnose, so warn (once) when a custom context means | ||
| // Replay cannot flow the causation id to the outbox and will therefore be a no-op. | ||
| if (_onceOnlyAction is OnceOnlyAction.Replay && | ||
| Interlocked.CompareExchange(ref s_warnedAboutCustomContext, 1, 0) == 0) |
…t, not bool s_warnedAboutCustomContext was declared as bool but used with Interlocked.CompareExchange(ref field, 1, 0), which has no bool overload, so the Paramore.Brighter project failed to compile on every target framework. Change the field to int (0 = not yet warned, 1 = warned), keeping the exact CompareExchange one-shot idiom. Behaviour is identical to what the code intended; it now compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZxNrBK31PdW61gnY47RFv
Reduce nesting in Handle/HandleAsync (was 5 levels deep on the Replay path): - Extract ResolveRequestContext() — the "prefer shared context, else fall back and warn once" block. - Extract ReplayCausation()/ReplayCausationAsync() — flatten the causation lookup with guard clauses, returning (causationId, replayed) for telemetry. - Replace the three `if (exists && _onceOnlyAction is ...)` guards with a single exists gate + switch on _onceOnlyAction. The switch keeps a defensive fall-through (no default) so an unrecognised action continues to base.Handle, exactly as the prior sequence-of-ifs did. Pure structural change; behaviour preserved. OnceOnly (43) and Pipeline (154) Core tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZxNrBK31PdW61gnY47RFv
Resolves three conflicts, all where master and this branch changed the same constructor or its call site: * PipelineValidator — master reworked it to use its primary-constructor parameters directly (dropping the private backing fields), made it IDisposable, and swapped `MessageMapperRegistry` for a lazily-invoked `Func<MessageMapperRegistry>` it owns. This branch had added `inbox` and `outbox` parameters for the Replay causation-tracking rule. Kept both: the two new parameters stay in the signature, and the rule now reads them as primary-constructor parameters to match master's style. * BrighterPipelineValidationExtensions — the PipelineValidator call site had to take master's `mapperRegistryFactory` *and* this branch's `inbox`/`outbox` arguments. * release_notes.md — both sides appended a section; kept both. OutboxProducerMediator and ServiceCollectionExtensions auto-merged; verified both sides survived (this branch's `Outbox` property and master's `ownsRegistry`/`ownsTransformerFactories` ownership arguments). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d test The test read the inbox causation id straight after the handler signalled, and intermittently saw null. The handler signals from inside the pipeline, but UseInboxHandler — wrapped around it — writes to the inbox only after the inner handler returns (UseInboxHandler.Handle calls _inbox.Add after base.Handle). The signal means "the handler ran", not "the inbox entry exists", so the assertion raced the pump thread's write. Gate the read on the entry existing, mirroring the existing WaitForMessageToBecomeOutstanding poll. Waiting on existence rather than on the causation id keeps the assertion meaningful: a missing entry times out, while an entry stamped with a wrong or null causation id still fails as an assertion rather than as a timeout. Pre-existing on this branch; it began failing once merging master grew the suite enough to change the timing. Core.Tests green 3/3 full runs on net9.0 and on net10.0 (978 passed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed the core plumbing ( This is a well-executed feature. The things I would normally have to ask for are already here: an ADR, migrations for all eight backends with idempotent guards and Findings roughly by severity. 1. Firestore replay commits an unbounded write batch
Compare the DynamoDB implementation ( 2. Mongo sync replay reports the wrong DB operation in telemetry
The sync 3. The schema probe now runs a read on a second connection inside
Every other Related and worth a release-note line: the probe fires on every relational outbox 4.
Given the release notes correctly identify "silent no-op" as the failure mode of this feature, surfacing the affected row count would make it diagnosable. Right now the telemetry answers "were we allowed to try?" rather than "did we replay?". Relatedly, in 5. Memoized probe caches "absent" forever, and validation may be what populates it
The Minor, same area: (continued in next comment) |
|
(review 2 of 2) 6. DI factory that can return
sp => (sp.GetRequiredService<IAmAnOutbox>() as IAmACausationTrackingOutbox)!The comment is admirably honest about this, and it does work with Also on the eager path ( 7. Schema-qualified table names silently disable the feature on MySQL and Spanner
Also worth noting: these probes embed the configured table name inside a SQL string literal, so a name containing a single quote breaks out. Not user input, and the migration catalogs already guard with 8. Inbox and outbox thread the probe result differently
It is safe today — the memo only ever goes null to true or null to false, never flips — but "parameters and SQL derived from two different reads of a mutable field" is the kind of coupling that breaks silently under a future edit, and the failure is a parameter-count mismatch at runtime. Prefer the outboxs explicit thread-through in both. 9. Duplicated global-inbox logic in
Tests Coverage is strong — generated causation base tests across every backend, legacy un-migrated seeders, telemetry assertions, the validation rule, and the DynamoDB vanished-item case. Two gaps that map onto findings above:
Nits
Nothing here blocks the design, which I think is right: an optional role interface, a runtime schema probe rather than a static claim, and graceful degradation in the mixed-migration state. Findings 1-3 are the ones I would want addressed before merge; the rest are follow-ups. |
…g Kafka entries (#4326) The "Replay Outbox Messages on Inbox Duplicate (spec 0027)" section sat under ## 10.7.0, but #4067 merged on 2026-08-01 — three days after the 10.7.0 tag — so it is unreleased. Move it verbatim to ## Master; the block is byte-identical to the original, this is a pure move. Two user-visible changes since 10.7.0 had no hand-written notes at all: - Kafka ErrorLogLevel (#4264): new public surface on KafkaSubscription. Covers what the hook is for, a worked example, and the caveat that it classifies logging only — Error.IsFatal still latches the consumer whatever the hook returns, so suppressing a fatal error's log entry does not suppress the error. - Kafka timestamp time zone (#4283): this one changes what goes on the wire, so it needs a note more than most. Records why the instant drifted by the host UTC offset on every hop, the move to RFC 3339 normalised to UTC, the same defect in the Unix-milliseconds fallback, and what happens in both directions while producers and consumers are on different versions. Claude-Session: https://claude.ai/code/session_012eBLwaQ4shgv3Hq9SuJSAm Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
When an Inbox receives a duplicate message, we may choose to replay any outgoing messages that it triggered when it was originally called. This makes us idempotent, in that our behavior matches that of the original call. It also allows us to recreate messages for downstream consumers who might have failed, by triggering from upstream, thus allowing us to replay a whole workflow until we reach the point where it failed.
Closes #2541