@openwop/openwop-conformance 2.43.2 → 2.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.44.1] — 2026-09-28 — the webhook retry legs outlast an advertised maxElapsedMs, and four families get v2 normative homes
4
+
5
+ - **The 2.44.1 cycle opens.** `workspace`, `content`, `triggerBridge` and `uiPlugins` get v2 normative homes (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree (`spec/v2/core/host-services.md`, `i18n.md`, `webhooks.md`, `packs.md`, `spec/v2/declaration.json`) after `v2.44.0` was tagged. No scenario changes: the rules are restated from the v1 text, none added or strengthened.
6
+ - New `spec/v2/core/storage.md` also changes the packed tree (`spec/v2/core/storage.md`, `spec/v2/core/overview.md`, `spec/v2/README.md`, `spec/v2/declaration.json`). It restates the v1 rules for `fs`, `kvStorage`, `tableStorage`, `sql`, `nosql`, `vectorStore`, `searchIndex`, `blobStorage` and `cache` (RFC 0189 §D). No rule and no wire change, and no scenario change.
7
+ - **A webhook retry leg is no longer killed before its own wait elapses** (RFC 0225 follow-up). 2.44.0 made `retryWaitFor` follow an advertised `retryPolicy.maxElapsedMs` plus the 30 s grace (up to 1 h), but the test timeouts were still fixed at registration from the operator cap (90 s by default). A host advertising `maxElapsedMs: 600000` waits 630 s, and its dead-letter leg died at 210 s ("Test timed out in 210000ms"). MyndHyve `00821-qec` recorded `0173.webhook-durable-delivery.dead-letter` and `0188.dead-letter-content-free` `executed-fail` after both passed on 2.43.1. It is the 2.0.2 defect again: a wait longer than the timeout governing it. vitest fixes a timeout before discovery is read, so every test that awaits a retry window now takes its timeout from `retryTestTimeoutMs(waits, slack)`, derived from the largest wait `retryWaitFor` can return (`MAX_RETRY_WAIT_MS`, the 1 h clamp). The timeout is a backstop: each wait is still bounded by its own window, so a host finishes no later than before. Fixed in `v2-webhook-durable-delivery` (retry leg; dead-letter legs), `v2-webhook-unregister-stops-delivery` and `v2-webhook-message-id-stable`. Workaround on 2.44.0: `OPENWOP_WEBHOOK_RETRY_WAIT_MS=600000`.
8
+ - **A self-test holds every call site to it.** `lib/webhook-retry-window.test.ts` scans the scenarios: each `it` that awaits a retry window must register a `retryTestTimeoutMs(N, …)` timeout with N at least its number of waits, and every scenario that computes a window must be seen by the scan. Sabotage-checked: restoring any of the four cap-derived timeouts fails it.
9
+ - **`0215.no-head-of-line` is not this bug.** `v2-webhook-delivery-isolation` waits the fixed 20 s floor under a timeout that covers it, and never calls `retryWaitFor`.
10
+ - **Suite `2.44.1`**. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
11
+
12
+ ## [2.44.0] — 2026-09-28 — three run-failure codes registered, a webhook retry bound hosts can advertise, and RFC 0219 Accepted
13
+
14
+ - **RFC 0219 is `Accepted`** (corpus only; no scenario change). The flip changes the packed `@openwop/spec-artifacts` tree through `spec/v1/gaps.json`: 0219 gap G6 is closed (SDK 2.5.0 sends the header), G7 is declared (`no-floor-no-refusal` needs a host with no `minClientVersion`), and G8 is transferred to #1763 (CORS).
15
+ - **The 2.44.0 cycle opens.** RFC 0226 (`Active`) changes the packed `@openwop/spec-artifacts` tree after `v2.43.2` was tagged: `spec/v2/errors.json` gains `node_config_invalid` (422), `sandbox_invocation_error` (422) and `mcp_error` (502), `since` 2.44, and `capability_not_provided`'s `meaning` widens. The generated `schemas/v2/error-envelope.schema.json` and the `errors.md` table follow (108 → 111 codes). A minor, not a patch: the registry gains members. No scenario changes: `openwop.requirement.errors.event-code-registered` already reads every run-failure code against the registry, so a host emitting the three codes passes it once they are registered.
16
+ - **RFC 0225 (`Active`; window waived by steward override of RFC 0147 §A.6): a webhook host may advertise `retryPolicy.maxElapsedMs`** (#1736). `lib/webhook-retry-window.ts` waits for an advertised bound plus a 30 s grace (the env var can still only raise it; 1 h ceiling). `v2-webhook-durable-delivery`'s dead-letter leg convicts only past that bound, on its existing row `0173.webhook-durable-delivery.dead-letter`. Without the member the leg stays `blocked`, as before.
17
+ - **Suite `2.44.0`**. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
18
+
3
19
  ## [2.43.2] — 2026-09-28 — three Class 3 corrections, and a failing scenario hook no longer hides its test
4
20
 
5
21
  - **The 2.43.2 cycle opens.** #1724 changes the packed `@openwop/spec-artifacts` tree (`spec/v2/core/interrupt.md`, `spec/v1/gaps.json`) after `v2.43.1` was tagged. No scenario changes in this entry: `approval.rejected` goes from SHOULD to MAY (Class 3), and no leg asserted the SHOULD.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.43.2 @openwop/spec-artifacts@2.43.2
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.44.1 @openwop/spec-artifacts@2.44.1
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.43.2",
4
- "stampSha256": "79b88391faebb7060dbaab54084f7e00e9ffcbaad1534dada438fcaa500483f9"
3
+ "version": "2.44.1",
4
+ "stampSha256": "c9a9550d05910b204589704d1e2c83fac85656062d8374a94e5afedf00b011e3"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.43.2",
3
+ "version": "2.44.1",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.43.2"
59
+ "@openwop/spec-artifacts": "2.44.1"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -34360,7 +34360,7 @@
34360
34360
  {
34361
34361
  "id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
34362
34362
  "file": "v2-webhook-durable-delivery.test.ts",
34363
- "line": 258,
34363
+ "line": 263,
34364
34364
  "title": "a failed attempt is retried and the event is delivered at least once",
34365
34365
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery",
34366
34366
  "citations": [
@@ -34397,7 +34397,7 @@
34397
34397
  {
34398
34398
  "id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
34399
34399
  "file": "v2-webhook-durable-delivery.test.ts",
34400
- "line": 378,
34400
+ "line": 383,
34401
34401
  "title": "an exhausted delivery is dead-lettered, never dropped",
34402
34402
  "explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
34403
34403
  "citations": [
@@ -34409,6 +34409,11 @@
34409
34409
  "section": "runs.md §Create",
34410
34410
  "requirement": "POST /runs MUST answer 201 for the noop fixture"
34411
34411
  },
34412
+ {
34413
+ "section": "webhooks.md §Delivery (RFC 0225 maxElapsedMs)",
34414
+ "requirement": null,
34415
+ "interpolated": true
34416
+ },
34412
34417
  {
34413
34418
  "section": "runs.md §Create",
34414
34419
  "requirement": "POST /runs MUST answer 201 for the noop fixture"
@@ -34431,7 +34436,7 @@
34431
34436
  {
34432
34437
  "id": "openwop.it.v2-webhook-durable-delivery.the-dead-letter-read-is-served-and-its-records-carry-no-payload",
34433
34438
  "file": "v2-webhook-durable-delivery.test.ts",
34434
- "line": 480,
34439
+ "line": 491,
34435
34440
  "title": "the dead-letter read is served and its records carry no payload",
34436
34441
  "explicitId": "openwop.requirement.0188.dead-letter-read",
34437
34442
  "citations": [
@@ -34444,7 +34449,7 @@
34444
34449
  {
34445
34450
  "id": "openwop.it.v2-webhook-durable-delivery.after-unregister-the-dead-letter-read-answers-404-not-found-as-for-a-subscriptio",
34446
34451
  "file": "v2-webhook-durable-delivery.test.ts",
34447
- "line": 501,
34452
+ "line": 512,
34448
34453
  "title": "after unregister, the dead-letter read answers 404 not_found, as for a subscription that never existed",
34449
34454
  "explicitId": "openwop.requirement.0217.dead-letter-read-after-unregister",
34450
34455
  "citations": [
@@ -34471,7 +34476,7 @@
34471
34476
  {
34472
34477
  "id": "openwop.it.v2-webhook-durable-delivery.a-dead-letter-record-carries-no-delivered-payload",
34473
34478
  "file": "v2-webhook-durable-delivery.test.ts",
34474
- "line": 534,
34479
+ "line": 545,
34475
34480
  "title": "a dead-letter record carries no delivered payload",
34476
34481
  "explicitId": "openwop.requirement.0188.dead-letter-content-free",
34477
34482
  "citations": [
@@ -34854,7 +34859,7 @@
34854
34859
  {
34855
34860
  "id": "openwop.it.v2-webhook-unregister-stops-delivery.no-attempt-for-an-unregistered-subscription-starts-after-the-204-while-its-contr",
34856
34861
  "file": "v2-webhook-unregister-stops-delivery.test.ts",
34857
- "line": 85,
34862
+ "line": 86,
34858
34863
  "title": "no attempt for an unregistered subscription starts after the 204, while its control keeps being retried",
34859
34864
  "explicitId": "openwop.requirement.0215.unregister-stops-delivery",
34860
34865
  "citations": [
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.43.2",
5
- "corpusTag": "v2.43.2",
4
+ "version": "2.44.1",
5
+ "corpusTag": "v2.44.1",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "987123ecfbfbc906c5655538b9380021ac0bcbfd93a467a67818e855d624ed0e",
13
- "api/v2/asyncapi.yaml": "76474704b7a8e829588abec30d31f09e11557ee5daacad301c3be7e77affe7ce",
14
- "api/v2/openapi.yaml": "f4af9e7fddad438d3a70753af29e5738ea48d15d1abd214a3fbeec97260cea9d",
12
+ "api/seams-v2.yaml": "6f9780f7751804b6dd9f57cb5474728326de805fcca667979f6b4f39c9c97336",
13
+ "api/v2/asyncapi.yaml": "ec6520792f7c4661403053003de7669430178597b5e0250ba5264b65dc82dbef",
14
+ "api/v2/openapi.yaml": "46314e02c9aeb4cb7a4ca0cfd5997dc24bd11c3971341e505cd0b7bef3150cd8",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -116,7 +116,7 @@
116
116
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
117
117
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
118
118
  "schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
119
- "schemas/v2/capabilities.schema.json": "ae6f60641d6ed680351b2710df945257c401f1ed83534f7e44fa2e651dc401ab",
119
+ "schemas/v2/capabilities.schema.json": "a76db7fbb18a98821c0ee4dfe51f615e59bb26c3d458bafd373fd3b6f7af4afe",
120
120
  "schemas/v2/certification-bundle.schema.json": "82ce31a1ed6c7e21b0ba88ff62a7b8b768fc3b176b1d06fce023203830b94725",
121
121
  "schemas/v2/channel-presence-payload.schema.json": "4eeb224910e6f7eee3ab5e7c3899559c25c83a26d0022e0ad195c4672611977a",
122
122
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -142,7 +142,7 @@
142
142
  "schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
143
143
  "schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
144
144
  "schemas/v2/envelopes/ui.a2ui-surface.schema.json": "e92a2548ba65454c9e0a34f72ea6768d18f59e61fa5da7668d0b2d5baddaddc8",
145
- "schemas/v2/error-envelope.schema.json": "d1a6862930e7c3fbc8ec334ef17953ac7b716525c6c82769edf89d0809d0c94b",
145
+ "schemas/v2/error-envelope.schema.json": "34f467d5775618f740059dab0aa49bf26247101feb0cdae3403560b6baf5eda9",
146
146
  "schemas/v2/eval-summary.schema.json": "2f384daf05b8f3b4f604688cae504d6d9971ba41b8c771e55fe9a17e09e86b7a",
147
147
  "schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
148
148
  "schemas/v2/form-content-pack-manifest.schema.json": "a1942a124e250a6e9fbdb6b811c5a125e78fbcbcaa46dfc71d0636f24710a339",
@@ -204,58 +204,59 @@
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
205
  "spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "0be2c8da837ddb170866303ab4640c8e8a8b2554d8c2bb9ac3d1bd9780bede5a",
207
+ "spec/v1/core-standard-manifest.json": "b6efb5d51e1432088bfeff049f310ae0095f3852a59e877e9d628a30782f5580",
208
208
  "spec/v1/deprecations.json": "46df807fb4adf0aae24671c20f69975cf3496e3bc3a521cb7e6ea556bdce563c",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
211
211
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
212
212
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
213
- "spec/v1/gaps.json": "7eefa3aa02e978a41e1e5948818961718d23cb0cbe29a4a491524360feb9fd6f",
213
+ "spec/v1/gaps.json": "07b0f5a206a015336307dd343b75adec647f06a9bae59ee08dbbfac851c503d5",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
215
  "spec/v1/migrations.json": "2efcd9064b2bc65a84a3b3d281aa86a91efd002690c1f4f14e8335c4a18194d1",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
217
217
  "spec/v1/operation-path-manifest.json": "fada0342815fb129c1ae61f1829bd6e34417717408ce86fcc54280f373c19e74",
218
218
  "spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
219
- "spec/v2/README.md": "4fa067d377580495e60baf08a4eafc3214b8ca48d8fb270bf91fc5066fad7186",
219
+ "spec/v2/README.md": "377639de1dad1ed71aba6dae8d34fdb6ee954fb23106e89454fe8d29cdde6349",
220
220
  "spec/v2/core/capabilities.md": "ad728e8a761153b55d036ece55043ff62d0982121e2e629e5e7cdf560fca3c03",
221
221
  "spec/v2/core/conformance.md": "0cfceb926eed87055550a5c23e640210c795fc8cf9dd1b2544db0e2b3842c564",
222
222
  "spec/v2/core/connection-packs.md": "1ee129d792c537bfe82393ccb92555d8e6ddc342b1aa781a13f101b27ec38171",
223
223
  "spec/v2/core/conversation.md": "354fa57c73d11062e523a44aa75a6353c5c81aefaf306d30c42416a228c82c86",
224
- "spec/v2/core/errors.md": "bcb62944d8a482b1b6d5ac08a692ad7e4aaadf6165746c7ba7fa39157be2a300",
224
+ "spec/v2/core/errors.md": "6d5a67389de84556f81e2558a30d83959c9283c8c5481ce7c759021e486d84fd",
225
225
  "spec/v2/core/events.md": "604abb6f9e2259a04103e1cb76d728712c3701c0d84dd915d4dc2949c912ee5e",
226
226
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
227
227
  "spec/v2/core/headers.md": "5cb1624fa63acc81992b6a6d842ec392cf7271aaf2f65d1f83c8c4fa286e6730",
228
- "spec/v2/core/host-services.md": "15923cceb95cba148a32e0944f6a2d1d25e9cc36b0c169b67ee42d483bcac800",
229
- "spec/v2/core/i18n.md": "846c6d2878fc8454c2ae0a1576f6b6e6c8958d47187a6c96cc5826ca3c3e2e91",
228
+ "spec/v2/core/host-services.md": "8b920bf2e9335ab04452068429b0c37d6eb8872b380a4d53368f2600d4afa241",
229
+ "spec/v2/core/i18n.md": "2d5a67bd3b9b956d1144acab2fbc3646cdb50b3621f97d772087eaa953798286",
230
230
  "spec/v2/core/idempotency.md": "2084d756789c1faf5121593fb39e67ca36a4c227178e175004742e403178ea49",
231
231
  "spec/v2/core/identity.md": "2dceb1c2697015e4a4fa8a9b28f4fbd25fa44a76ca5a919c752e2a8163c819db",
232
232
  "spec/v2/core/interop.md": "e72e25dff864c83b2980d7366b34054cffa072af0225821199f260f6228d260e",
233
- "spec/v2/core/interrupt.md": "2c8a4026930ec40ba47d7415dd02b6eda997a03d7a084e5084fec6f1261aba1c",
233
+ "spec/v2/core/interrupt.md": "4c825634acac72900ebd3c5ac5941f55648fa33d2df92913ef16371dcb700a34",
234
234
  "spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
235
235
  "spec/v2/core/oauth.md": "c96483a44b62978c596945fbb7d7c863cf6056cd980fdb4cf4e97555e3e10b72",
236
- "spec/v2/core/overview.md": "4087f171b00a3907551f508fec32fb9b0b3ccec8c0ac1b0c7c6d32cd2c0e3ff8",
237
- "spec/v2/core/packs.md": "b472a583211d466d6ab8e00c414c225b246e678043fe6283a710c899fadde913",
236
+ "spec/v2/core/overview.md": "c8c3bedb8e02dd8ce244b8a7a75d23af1c30dc92a343af912489424a89674d70",
237
+ "spec/v2/core/packs.md": "5929e708bd4e79f6e1beea570af583c80f3b63bb21dec78a5df03c235e73364a",
238
238
  "spec/v2/core/persistence.md": "711874f74cb08b03cd118d683056db10a063517762fcea73ec70feb5d983e6a4",
239
239
  "spec/v2/core/portability.md": "87c4327f674f714fda6217fd79b99d2ba219ad47dc02f9751bb56c200fb2c17a",
240
240
  "spec/v2/core/replay.md": "4d379494057746c6883f6047972e93e621863b998ed291b9a691d3aa57c1ff43",
241
241
  "spec/v2/core/runs.md": "55e21292ae4b41d0d104199a6c2634ecb3f787c4a671f9ebc37084be38ceef03",
242
242
  "spec/v2/core/security-defaults.md": "153884da7a2d54c30c81b5e649cfdef31e151b3a2a19dee1bc5a16c35d60a3aa",
243
+ "spec/v2/core/storage.md": "598ad4f3775c297c698a20daf72a7d15a6836c1231ddbca5961865c704f765b5",
243
244
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
244
245
  "spec/v2/core/versioning.md": "e3b3ab6d76e51035039a32173e9b412628a52c8fe7b16687ff58a2d0d983faa0",
245
- "spec/v2/core/webhooks.md": "6fc2fb7d9a993a682765ebc68e5c5bb215046ee599234ca7a660465279444eff",
246
+ "spec/v2/core/webhooks.md": "d2d33c6be777f217ade3e15dd369849e5c6ce67e1a4417216f0aa6ec5bdecc56",
246
247
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
247
248
  "spec/v2/corrections.json": "59f647f4d22477f6c2b06dcf913b682dde5d51dd5e607a5d0cf648ab60864d6e",
248
249
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
249
- "spec/v2/declaration.json": "184b83919e56d2f12bda73c7772905810a441cbab53fc42ad0b3135937fac3e5",
250
+ "spec/v2/declaration.json": "56c85c6d49f93d08d3bd48ccf1c049191cfbc4d4c22a1cd5202c1b6252759aca",
250
251
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
251
- "spec/v2/errors.json": "dc1a846abe3e888acb0ce4a33cf9d1fb98e41fcc8b49f42b3682fd65deca2eb2",
252
+ "spec/v2/errors.json": "fa650cbbdf25a140fdaf82ec5269f95b4585ac4bdde1fa6b0b3ffe48a3f7943c",
252
253
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
253
254
  "spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
254
255
  "spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
255
256
  "spec/v2/ext/README.md": "e2731ebb5156ece1840ed3b96e7a347f6345e73071ff4e54df0f21c1585c5027",
256
257
  "spec/v2/ext/a2uiSurface/README.md": "5518c7f355262eb7cc584422f4ff8e548f066b211878f969274a3a578127622d",
257
258
  "spec/v2/ext/brand/README.md": "82989c018d7965bab029bcce7a67b1d76ffc433d08971a4cac91befcccc09438",
258
- "spec/v2/ext/canvas/README.md": "0e91b68566d2be90319feedfaebe2cd2db0bd8f72fb59c958fa723c05e38023f",
259
+ "spec/v2/ext/canvas/README.md": "00ca0856e3a0146f1eeaec87209d450e2900ed2748dc7520cc60250a0d3ffbeb",
259
260
  "spec/v2/ext/chat/README.md": "f59b6706162abbfff96f8bccd4c0e1aeed721047cf62e9af58c875727a38d716",
260
261
  "spec/v2/ext/coordination/README.md": "c7fe0884f52405fdd02c5f9ec309ac6b585685ee06ec5f338fff7acf03c07148",
261
262
  "spec/v2/ext/dataIntegration/README.md": "286dec8aa29e4d3225cbd8b3a361f5a438c3288e759966b9415ecbb99c576e70",
@@ -268,7 +269,7 @@
268
269
  "spec/v2/ext/portability/README.md": "c59423ee446638b748a911b9e97e803c4786157be6120f656f0a060586bc7b82",
269
270
  "spec/v2/ext/provider-idempotency/README.md": "9d1bccef0ec16a19a3370ae79c2f5a1726ce0d1d25a51352907f288a2ccb15d7",
270
271
  "spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
271
- "spec/v2/ext/restTransport/README.md": "a8115faefeab01b7c85d516de60ebfc99b8e89552d16ba654456224518447b28",
272
+ "spec/v2/ext/restTransport/README.md": "c875fd7ae4818a5d5e93ab9b21e569de8700bb07c4dafacd60a46a46e4022ddb",
272
273
  "spec/v2/ext/sandbox-runtime-notes/README.md": "0d1290fb1e20b2e35c0b15051541ab6546e4e2a6f693b4f912cb3f7d6a65e92c",
273
274
  "spec/v2/ext/webResearch/README.md": "b675e6f6646128d781b661f754c2393eed86e5935d16c3a22df582a5ce025b9b",
274
275
  "spec/v2/facets/a2a.schema.json": "b68bf5e29e455e46c2ad7cfcccf5963730ec2310ff9db81884550d8df30fd6ef",
@@ -286,7 +287,7 @@
286
287
  "spec/v2/facets/sandbox.schema.json": "e75f54486d387b2d089791aa93b5093cd1450f1920753a0f97fc23acdfe978ed",
287
288
  "spec/v2/facets/schemaVersions.schema.json": "890d3416c4b66511d51f329abc68e6f467f4338f16807e489fe682999b2b56b9",
288
289
  "spec/v2/facets/supportedEnvelopes.schema.json": "644f22cc2236e30a611b5b7f1ba0458ff1925d68b94c514d9672583224e126bd",
289
- "spec/v2/facets/webhooks.schema.json": "2fac635ee5fbc10a14a1a46cf512a11bac660bfbabe0aea79f6c7b657ecb9e4e",
290
+ "spec/v2/facets/webhooks.schema.json": "54b576f1d150e28e1642693d846ce471c118e9c521bdf3924185305fbd1d0de4",
290
291
  "spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
291
292
  "spec/v2/harness-trust-anchors.json": "6007b92eae16a070fbc3777424efb0abfe865fda89d096963109b96282b91205",
292
293
  "spec/v2/harness-trust-anchors.schema.json": "372b5d92337c893d3449eff3f6bfd009ac09fdac285296d8bdbe0a08fff9e34c",
@@ -298,9 +299,9 @@
298
299
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
299
300
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
300
301
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
301
- "spec/v2/release.json": "ec99d76b53fba89d3d8788206c649a782774b55acf5284b75cab38b79ecf1d61",
302
+ "spec/v2/release.json": "af448fd044ab5b990a21945905f109dbbb9b0df9ab7f1105e79aa7d1e0f35a33",
302
303
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
303
- "spec/v2/surface-baseline.json": "3d3d0fc7c0b48e7d7339e5d8ada0640487908f37220b2de65ccc9bff093f876a"
304
+ "spec/v2/surface-baseline.json": "2f79e3283946dd8a401db039109d403b9c386e6b91a4780cbcb1440e8d3a6ffe"
304
305
  },
305
- "corpusCommit": "6612868966f2ecb9672d0a64382cb7067491c004"
306
+ "corpusCommit": "bf414c875dba212893f0230d44f2b4a2e226311d"
306
307
  }
@@ -22,12 +22,35 @@
22
22
  * default, or not a number, is ignored, so no operator can shrink the window to
23
23
  * hide a slow retry. A raised window is still bounded, so a host that never
24
24
  * retries still fails; it only fails later.
25
+ *
26
+ * RFC 0225 (suite 2.44.0): a host MAY advertise `retryPolicy.maxElapsedMs`,
27
+ * the longest from a delivery's first attempt to its dead-lettering. When it
28
+ * does, the wait is that bound plus a 30 s grace (the env var can still only
29
+ * raise it; clamped to the 1 h maximum), and a row convicts only once the wait
30
+ * has passed the advertised bound — the host's own promise, not a deadline the
31
+ * suite chose.
25
32
  */
26
33
  export const RETRY_WAIT_FLOOR_MS = 20_000;
27
34
  export const DEFAULT_RETRY_WAIT_CAP_MS = 90_000;
28
35
  /** A typo guard, not a policy: an hour per wait is longer than any retry schedule worth certifying in one sitting. */
29
36
  export const MAX_RETRY_WAIT_CAP_MS = 3_600_000;
30
37
  export const RETRY_WAIT_ENV = 'OPENWOP_WEBHOOK_RETRY_WAIT_MS';
38
+ /** RFC 0225: slack past an advertised `maxElapsedMs` for delivery latency and the sink write. */
39
+ export const ADVERTISED_BOUND_GRACE_MS = 30_000;
40
+
41
+ export interface AdvertisedRetryPolicy { readonly backoff?: string; readonly maxAttempts?: number; readonly maxElapsedMs?: unknown }
42
+
43
+ /** The advertised `retryPolicy.maxElapsedMs` (RFC 0225), or null when absent or not a positive integer. */
44
+ export function advertisedMaxElapsedMs(policy: AdvertisedRetryPolicy | null): number | null {
45
+ const v = policy?.maxElapsedMs;
46
+ return typeof v === 'number' && Number.isInteger(v) && v >= 1 ? v : null;
47
+ }
48
+
49
+ /** Whether a wait of `waitedMs` reached past the host's advertised bound plus the grace — the only point a row may convict. */
50
+ export function pastAdvertisedBound(policy: AdvertisedRetryPolicy | null, waitedMs: number): boolean {
51
+ const bound = advertisedMaxElapsedMs(policy);
52
+ return bound !== null && waitedMs >= bound + ADVERTISED_BOUND_GRACE_MS;
53
+ }
31
54
 
32
55
  export function retryWaitCapMs(env: Record<string, string | undefined> = process.env): number {
33
56
  const raw = Number(env[RETRY_WAIT_ENV]);
@@ -39,17 +62,39 @@ export function retryWaitCapMs(env: Record<string, string | undefined> = process
39
62
  * The floor stays 20 s so a host that advertises nothing is measured exactly as
40
63
  * before; an advertised `exponential` / `fixed` backoff widens it to the cap.
41
64
  */
42
- export function retryWaitFor(policy: { backoff?: string } | null, capMs: number): number {
65
+ export function retryWaitFor(policy: AdvertisedRetryPolicy | null, capMs: number): number {
43
66
  if (policy === null) return RETRY_WAIT_FLOOR_MS;
67
+ const bound = advertisedMaxElapsedMs(policy);
68
+ if (bound !== null) return Math.min(MAX_RETRY_WAIT_MS, Math.max(bound + ADVERTISED_BOUND_GRACE_MS, capMs));
44
69
  const backoff = String(policy.backoff ?? '');
45
- return backoff === 'exponential' || backoff === 'fixed' ? capMs : RETRY_WAIT_FLOOR_MS;
70
+ return backoff === 'exponential' || backoff === 'fixed' ? Math.min(MAX_RETRY_WAIT_MS, capMs) : RETRY_WAIT_FLOOR_MS;
71
+ }
72
+
73
+ /**
74
+ * The longest `retryWaitFor` can return, whatever a host advertises or an
75
+ * operator sets (suite 2.44.1).
76
+ *
77
+ * vitest fixes a test's timeout when the test is REGISTERED, before any
78
+ * discovery document is read, so a timeout cannot follow the host's advertised
79
+ * `maxElapsedMs`. 2.44.0 derived them from the cap alone (90 s by default):
80
+ * a host advertising 600000 had its dead-letter leg killed at 210 s ("Test
81
+ * timed out in 210000ms"), before its own bound plus the grace elapsed. That is
82
+ * the 2.0.2 defect again: a wait longer than the timeout that governs it. The
83
+ * timeout is only a backstop — every wait is bounded by its own window — so it
84
+ * is derived from the largest window instead.
85
+ */
86
+ export const MAX_RETRY_WAIT_MS = MAX_RETRY_WAIT_CAP_MS;
87
+
88
+ /** The timeout for a test that runs `waits` sequential retry waits, plus `slackMs` for the HTTP round trips around them. */
89
+ export function retryTestTimeoutMs(waits: number, slackMs: number): number {
90
+ return waits * MAX_RETRY_WAIT_MS + slackMs;
46
91
  }
47
92
 
48
93
  /** What a row says when its window closed before the host's schedule did. Computed, so the numbers a host reads are the ones the run used. */
49
94
  export function windowClosedNote(seen: number, maxAttempts: number, waitedMs: number, capMs: number): string {
50
95
  const raised = capMs > DEFAULT_RETRY_WAIT_CAP_MS;
51
96
  return `${seen} of the advertised ${maxAttempts} attempts arrived inside the ${waitedMs}ms this scenario waits, and the delivery is not in the sink yet. `
52
- + 'webhooks.retryPolicy carries only { maxAttempts, backoff } — no interval — so the suite cannot tell a slow conformant schedule from a host that stopped retrying, and it does not convict on a deadline it chose. '
97
+ + 'webhooks.retryPolicy advertises no maxElapsedMs (RFC 0225), so the suite cannot tell a slow conformant schedule from a host that stopped retrying, and it does not convict on a deadline it chose. A host can advertise retryPolicy.maxElapsedMs to have its own bound used. '
53
98
  + (raised
54
99
  ? `${RETRY_WAIT_ENV} is already raised to ${capMs}ms; set it above the SUM of this host's backoff intervals (at most ${MAX_RETRY_WAIT_CAP_MS}ms).`
55
100
  : `Set ${RETRY_WAIT_ENV} above the SUM of this host's backoff intervals (default ${DEFAULT_RETRY_WAIT_CAP_MS}ms; it can be raised, never lowered) and re-run.`);
@@ -35,7 +35,7 @@ import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedR
35
35
  import { readErrorCode } from '../lib/error-envelope.js';
36
36
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
37
37
  import { req } from '../lib/requirement-ids.js';
38
- import { retryWaitCapMs, retryWaitFor, windowClosedNote } from '../lib/webhook-retry-window.js';
38
+ import { pastAdvertisedBound, retryTestTimeoutMs, retryWaitCapMs, retryWaitFor, windowClosedNote } from '../lib/webhook-retry-window.js';
39
39
 
40
40
  export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
41
41
 
@@ -143,10 +143,10 @@ async function waitFor(pred: () => boolean, timeoutMs: number): Promise<boolean>
143
143
  * `webhooks.retryPolicy` first, `triggerBridge.retryPolicy` second for the
144
144
  * v1 overlap the schema explicitly preserves.
145
145
  */
146
- function advertisedRetryPolicy(doc: Record<string, unknown>): { maxAttempts?: number; backoff?: string } | null {
147
- const read = (holder: unknown): { maxAttempts?: number; backoff?: string } | null => {
146
+ function advertisedRetryPolicy(doc: Record<string, unknown>): { maxAttempts?: number; backoff?: string; maxElapsedMs?: unknown } | null {
147
+ const read = (holder: unknown): { maxAttempts?: number; backoff?: string; maxElapsedMs?: unknown } | null => {
148
148
  const rp = holder && typeof holder === 'object' ? (holder as { retryPolicy?: unknown }).retryPolicy : undefined;
149
- return rp && typeof rp === 'object' ? (rp as { maxAttempts?: number; backoff?: string }) : null;
149
+ return rp && typeof rp === 'object' ? (rp as { maxAttempts?: number; backoff?: string; maxElapsedMs?: unknown }) : null;
150
150
  };
151
151
  return read(doc['webhooks']) ?? read(doc['triggerBridge']);
152
152
  }
@@ -198,6 +198,11 @@ function retryWaitMs(doc: Record<string, unknown>): number {
198
198
  * to the wait carries its own timeout, the way the advert is sourced from the
199
199
  * constant the delivery loop reads. `WAIT_SLACK_MS` covers `waitTerminal`,
200
200
  * registration and the HTTP round trips around the waits.
201
+ *
202
+ * 2.44.1: and the cap was the wrong wait to derive from once RFC 0225 let a
203
+ * host advertise `maxElapsedMs` — the wait follows the advert, the timeout is
204
+ * fixed at registration. So the budget is derived from the largest wait
205
+ * `retryWaitFor` can return (`retryTestTimeoutMs`).
201
206
  */
202
207
  /**
203
208
  * How many attempts the receiver refuses before answering 204 (suite 2.0.3).
@@ -230,9 +235,9 @@ function failFirstFor(policy: { maxAttempts?: number } | null): number {
230
235
 
231
236
  const WAIT_SLACK_MS = 30_000;
232
237
  /** One `retryWaitMs` wait (the retry leg). */
233
- const RETRY_TEST_TIMEOUT_MS = RETRY_WAIT_CAP_MS + WAIT_SLACK_MS;
238
+ const RETRY_TEST_TIMEOUT_MS = retryTestTimeoutMs(1, WAIT_SLACK_MS);
234
239
  /** Two sequential `retryWaitMs` waits (the dead-letter leg: observe a retry, then exhaust). */
235
- const DEAD_LETTER_TEST_TIMEOUT_MS = RETRY_WAIT_CAP_MS * 2 + WAIT_SLACK_MS;
240
+ const DEAD_LETTER_TEST_TIMEOUT_MS = retryTestTimeoutMs(2, WAIT_SLACK_MS);
236
241
 
237
242
  /** Register the suite receiver; null (with a note) when the host's SSRF guard refuses a loopback URL. */
238
243
  async function register(rx: ScopedReceiver): Promise<{ webhookId: string } | null> {
@@ -314,8 +319,8 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
314
319
  // exactly RETRY_WAIT_CAP_MS — a host loses by the width of one delivery.
315
320
  // The obvious fix is to derive the wait from the intervals, and it cannot
316
321
  // be built: `spec/v2/facets/webhooks.schema.json` `retryPolicy` is
317
- // `additionalProperties: false` over exactly { maxAttempts, backoff }.
318
- // THE BASE INTERVAL IS NOT ON THE WIRE, so the suite cannot compute the
322
+ // `additionalProperties: false` over { maxAttempts, backoff } (RFC 0225 adds
323
+ // an OPTIONAL total `maxElapsedMs`, not the intervals). THE BASE INTERVAL IS NOT ON THE WIRE, so the suite cannot compute the
319
324
  // time to the Nth attempt, and any cap I pick is 2.0.1's 20-second
320
325
  // deadline again with a bigger literal.
321
326
  //
@@ -447,6 +452,12 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
447
452
  // sink still FAILS below; so does a delivery that was never retried.
448
453
  const seen = ours().length;
449
454
  if (inSink === false && policy?.maxAttempts !== undefined && attempts.length > 1 && seen < policy.maxAttempts) {
455
+ // RFC 0225: a host that advertised maxElapsedMs promised the sink by then.
456
+ // Past that bound (plus the grace) the empty sink is a conviction, not a
457
+ // window the suite closed early.
458
+ if (pastAdvertisedBound(policy, retryWaitMs(doc))) {
459
+ expect(inSink, req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'webhooks.md §Delivery (RFC 0225 maxElapsedMs)', `a host advertising retryPolicy.maxElapsedMs (${String(policy.maxElapsedMs)} ms) MUST dead-letter an exhausted delivery within it — ${seen} of ${policy.maxAttempts} attempts arrived and the sink is empty after ${retryWaitMs(doc)} ms`)).toBe(true);
460
+ }
450
461
  return blockedDespiteAssertions(windowClosedNote(seen, policy.maxAttempts, retryWaitMs(doc), RETRY_WAIT_CAP_MS));
451
462
  }
452
463
  expect(create.status, req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
@@ -33,7 +33,7 @@ import { req } from '../lib/requirement-ids.js';
33
33
  import { readErrorCode } from '../lib/error-envelope.js';
34
34
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
35
35
  import { hitHeader, mintWhsec, startModalReceiver, STANDARD_WEBHOOKS_ID, type ModalHit } from '../lib/webhook-receiver.js';
36
- import { retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
36
+ import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
37
37
  import {
38
38
  STANDARD_WEBHOOKS_ALG,
39
39
  deliveriesFor,
@@ -89,7 +89,7 @@ describe('RFC 0201 §C.10 — webhook-id is stable across retries, distinct acro
89
89
  return m;
90
90
  };
91
91
  const expectedKeys = subs.length * EVENTS.length;
92
- const window = retryWaitFor((g.facet['retryPolicy'] as { backoff?: string } | undefined) ?? null, CAP_MS);
92
+ const window = retryWaitFor((g.facet['retryPolicy'] as { backoff?: string; maxElapsedMs?: unknown } | undefined) ?? null, CAP_MS);
93
93
  await waitFor(() => {
94
94
  const m = byKey();
95
95
  return m.size >= expectedKeys && [...m.values()].every((a) => a.length >= 2);
@@ -128,5 +128,5 @@ describe('RFC 0201 §C.10 — webhook-id is stable across retries, distinct acro
128
128
  for (const { id } of perKey) {
129
129
  expect(subs.includes(id), req(ID, 'RFC 0201 §C.11', 'webhook-id MUST NOT be the subscription id')).toBe(false);
130
130
  }
131
- }, CAP_MS + 45_000);
131
+ }, retryTestTimeoutMs(1, 45_000));
132
132
  });
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * Absence is not evidence on its own (architect review, 2026-09-25). "No retry
10
10
  * arrived after the 204" proves nothing about a host whose next retry falls
11
- * outside the window, and `webhooks.retryPolicy` carries no interval, so the
11
+ * outside the window, and `webhooks.retryPolicy` carries no interval (RFC 0225's
12
+ * optional `maxElapsedMs` bounds the total, not the next retry), so the
12
13
  * suite cannot compute when that is. The leg therefore runs a CONTROL:
13
14
  *
14
15
  * - two subscriptions on one receiver, both filtering `run.completed`, both
@@ -44,7 +45,7 @@ import { absenceIsUnmeasured, noDeliveryCause, startScopedReceiver, type ScopedR
44
45
  import { readErrorCode } from '../lib/error-envelope.js';
45
46
  import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
46
47
  import { req } from '../lib/requirement-ids.js';
47
- import { retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
48
+ import { retryTestTimeoutMs, retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
48
49
 
49
50
  export const REQUIRES_HOST_CALLBACK = 'the host POSTs and retries webhook deliveries to the suite-owned scoped receiver behind OPENWOP_WEBHOOK_RECEIVER_URL';
50
51
 
@@ -64,10 +65,10 @@ afterEach(async () => {
64
65
  if (receiver) { const rx = receiver; receiver = null; await rx.close(); }
65
66
  });
66
67
 
67
- function advertisedRetryPolicy(doc: Record<string, unknown>): { maxAttempts?: number; backoff?: string } | null {
68
- const read = (holder: unknown): { maxAttempts?: number; backoff?: string } | null => {
68
+ function advertisedRetryPolicy(doc: Record<string, unknown>): { maxAttempts?: number; backoff?: string; maxElapsedMs?: unknown } | null {
69
+ const read = (holder: unknown): { maxAttempts?: number; backoff?: string; maxElapsedMs?: unknown } | null => {
69
70
  const rp = holder && typeof holder === 'object' ? (holder as { retryPolicy?: unknown }).retryPolicy : undefined;
70
- return rp && typeof rp === 'object' ? (rp as { maxAttempts?: number; backoff?: string }) : null;
71
+ return rp && typeof rp === 'object' ? (rp as { maxAttempts?: number; backoff?: string; maxElapsedMs?: unknown }) : null;
71
72
  };
72
73
  return read(doc['webhooks']) ?? read(doc['triggerBridge']);
73
74
  }
@@ -179,5 +180,5 @@ describe('RFC 0215 §B — unregistering stops the attempts (gated on webhooks)'
179
180
  if (controlLate.length === 0) {
180
181
  return blockedDespiteAssertions(`the control subscription was not retried after the 204 + ${GRACE_MS}ms within ${windowMs}ms either, so the target's silence shows nothing — the schedule finished inside the grace or runs past the window (raise OPENWOP_WEBHOOK_RETRY_WAIT_MS above the host's backoff sum) — ${detail}`);
181
182
  }
182
- }, CAP_MS * 2 + 90_000);
183
+ }, retryTestTimeoutMs(2, 90_000));
183
184
  });