release: v0.14.0-rc3 - #811
Merged
Merged
Conversation
The upstream ethereum-node chart's reth subchart defaults to a tcp liveness probe on p2p (30303) with failureThreshold 3 (~12 min grace). Reth keeps that port closed while running post-crash check_consistency recovery, which can take far longer on a large datadir — so the kubelet SIGKILLs it mid-recovery, each kill adds more drift to heal on the next start, and the node crash-loops permanently (observed: 637 restarts, ~129k blocks of StoragesHistory drift). Override the probe with failureThreshold 10000 so it effectively never fires; a genuinely dead reth exits the container on its own. Scoped to reth only — geth/nethermind/besu probe http-rpc and erigon probes metrics, none of which close during recovery. Claude-Session: https://claude.ai/code/session_01VLQSsnH9WdAsnVYGTd2omr
buildResourceURL keyed the scheme off X-Forwarded-Proto alone, so behind a TLS-terminating tunnel (edge https -> plaintext to Traefik -> verifier) any route without an explicit X-Forwarded-Proto:https RequestHeaderModifier produced http:// resource URLs in 402 challenges. The controller's host-bound so-<name>-host routes carry that filter but the shared-origin so-<name> routes do not — and being more path-specific for /services/<name>, they win the match even on dedicated-hostname origins, so challenges on those origins advertised http:// resources on an https endpoint (strict v2 payment clients and discovery crawlers see a scheme mismatch; observed live on 5 of 8 host-bound offers, #679). resolveSiteURL already solved exactly this for 402-page links: default https, downgrade only for hosts the stack serves locally over plain HTTP (obol.stack, loopback, *.localhost/*.local), explicit signals always win. Extract that resolution into resolveScheme and use it in both places, so challenge resource URLs and page links can never disagree again. Refs #679 Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
… openapi for inference/agent Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Reported by a teammate during v0.14.0-rc0 field testing. https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
Inline first-message fee capture for gate:auth offers: the SIWX session is minted by settling an x402 auth-capture charge (EIP-3009 single-shot, autoCapture) instead of a plain signature; subsequent requests ride the session cookie free. The escrow enforces the client-signed fee split (feeBps -> feeRecipient) on-chain at charge() time. - internal/x402/authcapture.go: auth-capture requirement builder + signed payload validation (validateSignedUnlockRequirement pins every economic field of the client-signed requirement against config) - internal/x402/unlockgate.go: unlock flow — 402 (x402Version 2) -> verify+settle against the signed payload -> mint SIWX cookie - internal/x402/metrics.go: obol_x402_verifier_fee_revenue_atomic_total + settled_volume_atomic_total (network/asset/fee_recipient), excluded from route-pruning - config: global authCaptureUnlock block (offerPrefix, price, payTo, feeRecipient, min/maxFeeBps, captureAuthorizer) Config-gated, off by default. Settlement must use the client's signed 'accepted' requirement verbatim (PaymentInfo hash commits server-issued deadlines; rebuilding drifts them and breaks the signature). Proven end-to-end on Base Sepolia and Base mainnet (on-chain fee split, cookie mint, free-ride, metrics materialization). Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
… integration/v0.14.0-rc1
…-api-1.x' into integration/v0.14.0-rc1
… into integration/v0.14.0-rc1
… into integration/v0.14.0-rc1
…' into integration/v0.14.0-rc1
…' into integration/v0.14.0-rc1
…into integration/v0.14.0-rc1
…to integration/v0.14.0-rc1
…dates' into integration/v0.14.0-rc1
…integration/v0.14.0-rc1
…dates' into integration/v0.14.0-rc1
…tegration/v0.14.0-rc1
…nto integration/v0.14.0-rc1
An Obol Agent (type=agent) runs its own model, skills, and memory — the buyer
never selects one, and Hermes ignores the chat-completions `model` field
(resolved from its own config). Surfacing the underlying model in the 402 page
is noise and exposes an implementation detail, and pay-agent's required
`--model` flag had no effect on agent calls.
- paymentrequired.go agentCopy: remove '(running <model>)', the '"model":' line
in the example body, and '--model <id>' from the pay-agent example. The agent
copy is now model-free.
- buy-x402 buy.py: pay-agent no longer accepts/sends --model (synthesised body
is just {messages, stream}); usage strings updated.
- buy-x402 SKILL.md: pay-agent documented without --model.
go build + x402/buyer/embed tests green.
Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD
(cherry picked from commit e605f55)
mergeAgentExtras no longer adds extra.agentModel — an Obol Agent runs its own model and the buyer never selects one, so the model id is an internal detail, not buyer-facing info (it also rendered in the HTML 402 page's raw-JSON card). agentSkills/agentRuntime still surface so clients can tell it's an agent. Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit 92716d6)
…t offers For agent offers the buyer never selects a model — the agent runs its own and ignores the chat-completions `model` field — so the bazaar discovery example now seeds the neutral 'your-model-id' placeholder instead of the real upstream id (which also rendered in the HTML 402 page's embedded raw-JSON card). Inference offers are unchanged: there the model IS buyer-selectable, so the real id stays. Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit 0f86017)
…for agent offers The /skill.md catalog (and its Service Details section) showed the agent's underlying model in the Model column / **Model** bullet. An agent runs its own model and ignores the request `model` field, so the id is an internal detail — agent rows now render '—' and omit the **Model** bullet. Inference offers keep their model (there the buyer selects it). Mirrors the 402 page/extra/bazaar model-strip in internal/x402. The /api/services.json feed still resolves the agent model on purpose (drives the storefront UI) — left untouched. Claude-Session: https://claude.ai/code/session_01XgUndZjSoxr2jNNGG5sVYD (cherry picked from commit 2a88c3f)
`obol stack up` fails on the k3s backend with: UPGRADE FAILED: cannot patch "ollama" with kind Endpoints: Endpoints "ollama" is invalid: subsets[0].addresses[0].ip: Invalid value: "127.0.0.1": may not be in the loopback range k3s runs directly on the host, so OllamaHostForBackend returns 127.0.0.1 and that value is stamped straight into the ollama Endpoints. Kubernetes has rejected loopback addresses in Endpoints since v1.33, and rightly so: inside a pod's network namespace 127.0.0.1 is the pod itself, not the host, so the endpoint could never have routed anywhere useful. OllamaHostIPForBackend now substitutes the host's primary routable IPv4 address whenever resolution lands on loopback. The guard sits in the shared resolver rather than in a k3s branch, so it also covers Docker runtimes that map host.docker.internal to loopback. resolveHostIP in `obol sell` was a second copy of the same strategy and had drifted with the same bug — it returned 127.0.0.1 for k3s and fed it to createHostService, which builds an Endpoints object too. It now delegates to the shared resolver instead of duplicating it. Three existing tests asserted the old loopback values; they now assert the contract that actually matters — the resolved address is a valid, non-loopback IP.
The /storefront branding editor iframes storefront-preview.obol.stack, so that name has to resolve locally. `obol stack up` appended it to the hostname list at one call site — but EnsureHostsEntries replaces the managed /etc/hosts block wholesale, and the four other call sites in internal/hermes and internal/openclaw pass only agent hostnames. `stack up` resumes agents after syncing defaults, so the hermes path runs last and rewrites the block without the preview origin. Observed on a real k3s stack: the block ended up with obol.stack and the two agent hosts, the preview name did not resolve, and the editor's iframe had nowhere to load from — while the HTTPRoute and its backend were healthy and answered fine on a Host header. The origin is a fixed property of every local stack, exactly like the base domain, so it is now emitted unconditionally rather than depending on which caller happens to write last. The constant moves to internal/dns (a leaf package that owns the managed block) and internal/tunnel points at it, keeping one source of truth. Block rendering is split into buildHostsBlock so the guarantee is testable without root.
Skip settle on client disconnect/write errors, classify facilitator rejections, and teach HTTP buyers Bankr chat auto-pay while agent/inference use wallet-sign plus long curl after live timeout and voucher failures. Co-authored-by: Cursor <cursoragent@cursor.com>
Publish type-specific Poncho chat prompts alongside AgentCash/Bankr and surface them in the storefront pill selector after a successful live pay test. Co-authored-by: Cursor <cursoragent@cursor.com>
Record that a Bankr App with pay:x402 + allowedHosts still hit rpc timeout on Base mainnet agent offers; drop the Sepolia digression and clarify HandleProxy settle-skip vs ForwardAuth verify-only. Co-authored-by: Cursor <cursoragent@cursor.com>
…njection) CodeQL flagged four new go/log-injection alerts introduced by this PR in internal/x402/forwardauth.go: the buyer's payment payload and the facilitator's reject reason reach log.Printf unescaped, so a crafted value containing CRLF can forge additional lines in the operator's log (e.g. a fake "payment settled successfully" entry). Fix at the producers rather than at each log call, so future call sites inherit it: paymentPayloadSummary, facilitatorRejectDetail and the normalize note now return CR/LF-stripped strings, and truncateForLog sanitizes too (it carries the raw facilitator /verify body). Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…-rc2
Brings the aggregate /.well-known/x402 + external buyer-tool compat work
along with its CodeQL go/log-injection fix.
Conflict resolutions (both additive — kept BOTH sides, not one over the
other):
* catalog.go — the static-site match and content hash now fold in both
the widget's embedded chat-vendor.js (rc2) and the new x402.json
(#806). Dropping either side would silently pin a stale copy of that
asset across controller upgrades via the skip-when-unchanged path.
* forwardauth_test.go — kept rc2's TestBuildResourceURL_Scheme
alongside #806's brokenPipeWriter settle-skip tests.
Also updates four widget-era test call sites in catalog_test.go and
hostoffer_test.go for #806's new wellKnownX402JSON parameter (they pass
"" — those tests do not exercise the aggregate x402 document). Without
this the package does not build its tests.
Replaces the unsigned a128bdf/dd9c5a1d/cdee3a22 with a single signed
merge; the resulting tree is byte-identical to cdee3a2.
Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…ecks Drop chatty framing, require AgentCash/Poncho tx receipts, and teach Bankr agent buyers to reuse the full accepts[] entry to avoid unsupported_scheme. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…dates' into rc3-signed # Conflicts: # obolup.sh
defaultPaidMethod flipped `type: http` offers without declared `methods`
from POST to GET. For a root-priced route that makes the advertised
operation unpayable.
renderStaticSite publishes the offer root as exactTo("/", "index.html"),
a GET-scoped Exact match, and its comment states the contract:
Method-scoped to GET: discovery documents are read-only, and a
root-priced offer (route pattern "/") advertises POST <origin>/ as
its paid resource — an unscoped Exact "/" match would shadow that
POST into the static httpd (501) instead of the payment gate.
So at the offer root GET is served by the landing page and only POST
falls through to the gate. The previous hardcoded POST was one half of
that two-sided contract; changing the advertised method without changing
the routing left discovery pointing at a 200 HTML page that can never
return 402.
Observed live on a deployed offer before this fix:
GET https://<offer-host>/ -> 200 text/html
POST https://<offer-host>/ -> 402
with /.well-known/x402, openapi.json and skill.md all advertising GET.
defaultPaidMethod now takes the paid route's path and returns POST when
that route is root-priced ("", "/" or "/*"), GET otherwise. Sub-path paid
routes are unaffected and keep the GET default that stops OpenAPI and
AgentCash clients POSTing into a GET-only upstream (405), which is what
the GET default was introduced for.
The blast radius is any paid route declared WITHOUT `methods`, not only
offers with no route table at all — primaryPaidMethod checks
len(rt.Methods) > 0. A route table whose paid catch-all omits `methods`
was affected too.
Two existing assertions encoded the old behaviour for the root-priced
case and are updated with the reason. paidmethod_root_test.go adds the
check the route-surface suite was missing: the advertised method must not
be one the static site claims first. The release smoke could not catch
this because the payment gate matches on path only, ignoring method, so a
bare GET probe returns 402 regardless of what discovery advertises.
Carries d359a14: root-priced type:http offers advertise POST again, so the advertised paid operation reaches the payment gate instead of the static index.html. Adds paidmethod_root_test.go.
upstreamOpenAPICache.refresh keys on offer.Generation and short-circuits once an entry exists for that generation. It recorded the fetch result unconditionally — including nil — so a single failed probe pinned the offer to its route-table fallback until someone edited the CR. The blast radius is not one offer. reconcileStaticSite rebuilds the SHARED bundle from this cache on every offer's reconcile, so one cached nil overwrites a good document for the whole stack. A controller restart empties the cache and gives each offer exactly one attempt, which is where this is most likely to bite: any offer whose single post-restart probe misses — upstream still rolling, cold start, transient 5xx, or the probe's short timeout — silently degrades from one resource per real paid route to a single root entry. The cache doc comment already described it as holding "the last-good upstream fetch"; treating a nil as good is what broke that contract. refresh now leaves the generation unrecorded when a probe misses on an offer that COULD serve a document, so the next reconcile retries and any last-good doc survives: stale beats silently collapsed. The distinction matters, because fetchUpstreamOpenAPI also returns nil by design for agent and inference offers and for offers with no upstream Service. Those can never serve a document, so their nil is terminal and must still be cached — otherwise they would be probed on every single reconcile forever. offerHasProbeableUpstream is extracted so refresh and fetchUpstreamOpenAPI cannot disagree about which case they are in. Observed on a live stack: after a controller image swap, three offers dropped from 12/12/16 advertised resources to 1 while two others were unaffected, then recovered as their reconciles landed. Verified the new test fails without the guard (fetch called once instead of twice, and the later success never cached) and passes with it.
…cting it
Step 13 required agentModel in the 402 body. The verifier deliberately
omits it: mergeAgentExtras (internal/x402/verifier.go) writes only
agentSkills and agentRuntime, and TestMergeAgentExtras_AddsAgentFieldsButNotModel
asserts "agentModel must not be surfaced — the underlying model is an
internal detail, not buyer-facing". That is the rc1 agent model-strip.
So the flow has been asserting the inverse of a shipped, unit-tested
contract and failing ever since. It failed identically on two independent
hosts in the v0.14.0-rc3 validation, with everything around it green:
step 12 logged ModelReady=True UpstreamHealthy=True PaymentGateReady=True
RoutePublished=True, and the 402 carried a well-formed accepts[] with
extra={"agentRuntime":"hermes","agentSkills":[...]}.
The step now asserts the contract in BOTH directions: agentSkills and
agentRuntime must be present (a buyer needs them to choose the offer), and
agentModel must be absent (a leak is a regression). Retitled to match what
it checks.
Verified against the literal extra{} block both hosts produced: passes as
shipped, and fails with "agentModel leaked" if the id is ever surfaced.
…ettled Closes the restart window that #810 left open. The upstream-OpenAPI cache is process-local, so every controller restart begins with no entry for any offer. reconcileStaticSite rebuilds the SHARED bundle from that cache on EVERY offer's reconcile — including reconciles belonging to other offers — so an offer that has not reconciled yet was re-rendered from the route-table fallback and visibly lost its advertised routes until its own reconcile landed. Measured on a live nine-offer stack during the v0.14.0-rc3 validation: after a controller image swap, five offers dropped from 11/12/12/12/16 advertised resources to 1 at ~t+30s and were fully recovered by ~t+90s. Buyers crawling discovery inside that window see a single root entry instead of the real paid routes. #810 stopped a FAILED probe being pinned for the whole generation. It could not fix this, because get() returns nil for "never fetched" and "fetched, no document" alike, and the caller cannot tell them apart. getSettled now returns that distinction, and buildOfferBundles takes the currently-published ConfigMap data. While a probe is unsettled the already served openapi.json and x402.json are kept — the ConfigMap survives the restart even though the cache does not, so there is something correct to hold on to. Stale beats thinner. The other direction matters just as much and is tested: once a probe HAS settled with no document, the fallback is the correct final answer and must re-render, or an offer that legitimately drops its upstream OpenAPI would serve the old document forever. A first-ever reconcile with nothing published still renders the fallback rather than an empty document. Verified TestBuildOfferBundles_UnsettledPreservesPublished fails with the preserve branch disabled and passes with it.
…be killing a flow
Two harness defects found while validating v0.14.0-rc3 — a release whose
entire content was two k3s blockers.
1. The smoke could never exercise OBOL_BACKEND=k3s.
flows/lib.sh and flows/release-smoke.sh relink tools into the flow
workspace with a hard-coded list that omitted k3s, and
internal/stack/backend_k3s.go resolves k3s at cfg.BinDir/k3s with no
PATH fallback. The failure was deterministic and immediate:
✗ prerequisites check failed: k3s not found at
/home/claude/obol-stack-rc3/.workspace/bin/k3s
which cascade-failed every flow in the run. So the one backend rc3
existed to fix was the one backend the release smoke could not touch.
k3s now joins the relink loop. It is linked only when present, so
k3d-only hosts are unaffected and it is deliberately NOT added to the
required-tools list. release-smoke.sh instead fails fast with an
actionable message when OBOL_BACKEND=k3s is requested and k3s is
genuinely absent, rather than letting the first flow discover it.
2. flow-04 aborted the whole flow when loopback ingress was down.
lib.sh runs under `set -euo pipefail`. dash_code() and
dash_code_follow() are called inside command substitutions with no
guard, so a curl exit 7 (CURLE_COULDNT_CONNECT) killed the script at
whichever probe hit it first — losing every remaining step and leaving
a truncated log instead of a failed assertion. Observed on spark1: the
flow-04 log simply stops mid-step.
Both helpers now swallow curl's status and emit "000", which is what
curl's own -w already writes on a connection failure, so a dead ingress
FAILS the step instead of ending the run.
The other curl-in-substitution sites were checked and are already
guarded (flow-04-agent.sh:130 with `|| true`, lib-dual-stack.sh:466
with `|| rc=$?`); these two were the only unguarded ones.
Verified: bash -n clean on all three files; the old dash_code form aborts
under set -euo pipefail against an unreachable host while the new one
returns 000 and continues, and a healthy endpoint still returns its real
status code.
fix(flows): flow-16 asserts the agent model-strip instead of contradicting it
fix(discovery): hold published bundles while an upstream probe is unsettled
…uard fix(flows): let the smoke test the k3s backend, and stop one dead probe killing a flow
This was referenced Aug 8, 2026
Closed
Closed
OisinKyne
approved these changes
Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rolls the
v0.14.0release train up tomain. This supersedes #809 (rc2 → main) and #800 (rc1 → main), both of which are fully contained here.mainhas zero commits that are not already in this branch, so this is a clean fast-forwardable roll-up: 62 commits, 69 files, +5,881 / −389.Tagged and published as
v0.14.0-rc3(pre-release;releases/latestdeliberately still reportsv0.13.0, so the installer default is untouched).What's in it
rc3 — two k3s blockers and two discovery fixes
obol stack upfailed outright on the k3s backend. Kubernetes has rejected loopback addresses inEndpointssince v1.33, and that is exactly what host resolution handed theollamaEndpoints. The guard now lives in the shared resolver, so it also covers Docker runtimes that maphost.docker.internalto loopback;obol sellcarried a drifted second copy of the same logic and now delegates. (fix(stack): two k3s blockers — loopback Endpoints, and the storefront preview host missing from /etc/hosts #808)storefront-preview.obol.stack, which never made it into the managed/etc/hostsblock — the agent-resume path rewrote the block last and dropped it. Now emitted unconditionally. (fix(stack): two k3s blockers — loopback Endpoints, and the storefront preview host missing from /etc/hosts #808)type: httpoffers advertised an unpayable method. rc2 flipped them toGET, but the offer root servesindex.htmlon GET and only POST reaches the payment gate, so discovery pointed buyers at a 200 HTML page that could never return 402. The advertised method is now derived from the route's shape. (feat: add aggregate /.well-known/x402 and external buyer-tool compat #806)rc2 — discovery and external buyer compatibility
Aggregate
/.well-known/x402for the whole stack; facilitator failures classified so buyer agents stop retrying unretryable errors; legacy network aliases alongside CAIP-2; SSE settlement deferred so a buyer whose connection dies mid-stream is not debited; secure operator branding previews; CLI version in the frontend footer; CodeQLgo/log-injectionremediation in the ForwardAuth middleware.rc1 — agent chat widget, opt-in auth-capture unlock gate, agent model-strip.
Validation
This branch was put through pre-publish testing on three hosts before the tag was published.
flow-14-live-obol-base-sepoliaandflow-13-dual-stack-obol.kube-state-metricspod, one is the stale assertion below.status = 0x1), deliberately not the endpoint the flows themselves used.ollamaEndpoints holds a routable IP, andstorefront-preview.obol.stacksurvives repeated hermes rewrites of the managed hosts block.Known issues carried into this PR
Recording these so they are not rediscovered later. None were judged release-blocking, and all are pre-existing rather than introduced here.
obol stack downon k3s leaves host netfilter dirty.internal/stack/backend_k3s.golooks for/usr/local/bin/k3s-killall.sh, which never exists because obol installs k3s into its ownBinDirrather than via the official installer. The fallback only kills containerd shims, leavingcni0,flannel.1and CNI hostport DNAT rules behind — and because those rules matchdst-type LOCAL, all host loopback :80/:443 is hijacked into a dead pod. Latent before rc3 only because k3sstack upfailed earlier; making the lifecycle work exposes it.release-smoke.shcannot test the k3s backend at all.flows/lib.shandflows/release-smoke.shrelink tools withfor tool in kubectl helm helmfile k3d k9s openclaw—k3sis omitted, andbackend_k3s.gohas no PATH fallback. Given rc3 is entirely about k3s blockers, this is worth a one-word fix.flow-16-sell-agentstep 13 asserts a contract that was deliberately removed. It requiresagentModelin the 402 body, butmergeAgentExtrasomits it on purpose andTestMergeAgentExtras_AddsAgentFieldsButNotModelasserts "agentModel must not be surfaced" — the rc1 agent model-strip. It fails identically on both test hosts, which is what confirms it is the flow that is stale. Fix the flow, not the verifier.flow-07-sell-verifysteps 13, 15 and 16 pass for the wrong reason. Step 13 greps for"402"inkubectl logs … 2>&1 || true, and402is a substring of everyx402token in that output — including kubectl's own error text — so it cannot fail. Step 15 greps fortunnelinobol tunnel logs, which any failure message satisfies. Step 16 claims to verify all six conditions but runs two uncorrelated greps, sostatus: "True"fromModelReadysatisfies it whileReadyis False. Independently re-derived on both hosts.obol sell register's metadata write reverts witherc8004: setMetadata tx: execution reverted (custom error 0x7e273289), emitted only as a warning sosteps_failed=0hides it. Both live flows also leavetransactions.registrationempty inreceipt-summary.json.X-PAYMENT-RESPONSEships as a trailer, which many proxies and clients drop. The deferral itself is correct — it is what fixes debit-after-disconnect — only the delivery mechanism is awkward.Signing
All commits are signed except
d7f191b9, the rc1 chat-widget merge, which is left as-is deliberately: it is baked into the already-publishedv0.14.0-rc1tag, so re-signing it would fork released history.Merging
Please merge, do not squash — the integration branch's merge structure is what maps released tags to their content.