Skip to content

fix(responses): scope durable thought signatures to the upstream destination - #1929

Merged
lidge-jun merged 3 commits into
devfrom
codex/wave5-tsig-destination
Aug 18, 2026
Merged

fix(responses): scope durable thought signatures to the upstream destination#1929
lidge-jun merged 3 commits into
devfrom
codex/wave5-tsig-destination

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the cross-endpoint half of #1926.

The durable replay key used thread, provider name, adapter and model, while the sibling
in-memory cache (src/responses/reasoning-replay-cache.ts) used those plus destination
and credential identity. So one provider name serving two endpoints — a gateway and a
direct endpoint under one config entry — shared opaque signatures between them, and a
signature minted by one upstream is meaningless to the other.

Why the durable store left those fields out is a real constraint, not an oversight.
The sibling's identities run through an HMAC keyed by randomBytes(32) minted at module
load. Reusing them here would change every key on restart, so the store would stop
matching anything while still appearing to work — a worse failure than the over-broad key,
because nothing announces it.

Destination does not share that constraint. It is a configured endpoint rather than a
secret, so a plain digest of the same normalized URL is equally non-reversible for this
purpose and needs no persisted salt or new on-disk state. durableReplayDestinationIdentity
sits beside the process-local form rather than replacing it — the in-memory cache is
right to prefer the random-keyed version.

Credential scope is deliberately not included. OAuth has a restart-stable discriminator
in accountId + generation, key auth would need a persisted-salt digest of secret
material, and Codex pool auth rides a rotating bearer — three different answers that belong
with the emit-before-commit work in #1926 rather than smuggled in behind a destination fix.
Cross-account and rotation collisions therefore remain open, and they are the more serious
ones.

The store version is now read, not just written. It was written as 2 and never
checked, so a key-shape change could not be announced: old entries simply went dead against
the new keys and aged out on TTL — silent, and indistinguishable from a store that is not
working. STORE_VERSION = 3 drops them explicitly on load.

Verification

  • bun test --isolate tests12755 pass, 10 skip, 0 fail, 159251 expect() calls across 823 files (505s).
  • bun run typecheck — passed.
  • Focused: google-signature-history-roundtrip, reasoning-replay-identity, reasoning-replay-scope-source, google-antigravity-replay — 80 pass, 0 fail.
  • Ablation: removing the destination component from keyFor fails the new cross-endpoint test while the restart test stays green. That pair is the point — the naive fix (reusing the process-local HMAC) would have passed the first and broken the second.

Checklist

  • Tests added or updated (cross-endpoint isolation; durable-identity stability across a simulated restart)
  • Docs updated — devlog unit records the landed half and what stays open
  • No credentials, request bodies, or account identifiers logged
  • Targets dev

The audit could not find a caller of the remember API and reasonably concluded
the emit-before-commit defect might not be live. It is: src/bridge.ts calls it
three times and discards the durable promise with void, then emits the item.

The reason this does not land in the same cycle is structural rather than
effort. closeCurrentToolCall is a synchronous closure writing into a
ReadableStream controller, so there is no await to hang the durability on -
awaiting the write means an async close path or a pre-emit barrier, which is a
change to the streaming core. AGENTS.md gates that behind the full suite and it
sits beside the subagent-fallback synchrony invariant, so it deserves its own
work-phase and a maintainer's call on sequencing.

Filed as #1926 with both halves, the restart-stability constraint that kills
the naive key widening, the OAuth/key/local credential split, the version bump
requirement, and the exact caller lines.
…ination

The durable replay key used thread, provider name, adapter and model, while
the sibling in-memory cache used those plus destination and credential
identity. So one provider NAME serving two endpoints - a gateway and a direct
endpoint under one config entry - shared opaque signatures between them, and a
signature minted by one upstream is meaningless to the other.

The reason the durable store left those fields out is real and worth keeping
in view: the sibling's identities run through an HMAC keyed by randomBytes
minted at module load. Reusing them here would change every key on restart, so
the store would stop matching anything while still appearing to work - a worse
failure than the over-broad key, because nothing announces it.

Destination does not share that constraint. It is a configured endpoint rather
than a secret, so a plain digest of the same normalized URL is equally
non-reversible for this purpose and needs no persisted salt or new on-disk
state. That is durableReplayDestinationIdentity, and it sits beside the
process-local one rather than replacing it, because the in-memory cache is
right to prefer the random-keyed form.

Credential scope is deliberately NOT included here. OAuth has a restart-stable
discriminator in accountId and generation, key auth would need a persisted-salt
digest of secret material, and Codex pool auth rides a rotating bearer - three
different answers that belong with the emit-before-commit work in #1926 rather
than smuggled in behind a destination fix.

The store version is now read on load, not just written. It was written as 2
and never checked, so a key-shape change could not be announced: old entries
simply went dead and aged out on TTL, which is silent and looks exactly like a
store that is not working. v3 drops them explicitly instead.

Ablation: removing the destination component from the key fails the new
cross-endpoint test and leaves the restart test green, which is the pair that
matters - the fix must isolate endpoints without breaking restart replay.
The reviewer was right that deferring the whole key-scope fix was too much.
The restart-stability blocker binds to the credential component alone, and this
document had already sanctioned scoping the fix to destination only - then
deferred the branch it pre-authorized.

Also records the four errors the audit found in what I wrote: a third call site
in failCurrentToolCall, an inverted claim about local providers that already
never remember, a version mechanism that was written but never read, and a
missing fourth auth mode. Plus the correction to my own deferral reasoning -
the streaming sites are already inside an async loop, so the real blocker is
buildResponseJSON rather than the synchronous closure I named.
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

@github-actions
github-actions Bot marked this pull request as draft August 17, 2026 13:39
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 42632053-4f6a-4bb2-b0db-6ad56ba0827b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Aug 17, 2026
@lidge-jun
lidge-jun marked this pull request as ready for review August 18, 2026 00:04
@lidge-jun
lidge-jun merged commit 90fdc3a into dev Aug 18, 2026
40 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant