@openwop/openwop-conformance 2.43.2 → 2.44.0

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,12 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.44.0] — 2026-09-28 — three run-failure codes registered, a webhook retry bound hosts can advertise, and RFC 0219 Accepted
4
+
5
+ - **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).
6
+ - **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.
7
+ - **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.
8
+ - **Suite `2.44.0`**. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
9
+
3
10
  ## [2.43.2] — 2026-09-28 — three Class 3 corrections, and a failing scenario hook no longer hides its test
4
11
 
5
12
  - **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.0 @openwop/spec-artifacts@2.44.0
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.0",
4
+ "stampSha256": "37326a214e400e23aba6d56c45d06915c32f0187bd4fe637888e0e14221c9767"
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.0",
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.0"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -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": 486,
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": 507,
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": 540,
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.0",
5
+ "corpusTag": "v2.44.0",
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": "7efbc96aa7d5fb2d501f90585cc7fbb10da6a0ff41289b52cf6ddadf04dc852a",
13
+ "api/v2/asyncapi.yaml": "ac39be9e90e1daf84cb12cc268be3152c6b50f6852129ac42820db43bb26f41d",
14
+ "api/v2/openapi.yaml": "a8b2fdc24b017cf146985dd2360da4e794a87deb83b98fc51c54a1363fc5e391",
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,13 +204,13 @@
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": "5ea51250e81a3bf70637811cdb3bacfa7d35609dc49ce1a4889f0cff9fb35c85",
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",
@@ -221,7 +221,7 @@
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",
@@ -230,7 +230,7 @@
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
236
  "spec/v2/core/overview.md": "4087f171b00a3907551f508fec32fb9b0b3ccec8c0ac1b0c7c6d32cd2c0e3ff8",
@@ -242,20 +242,20 @@
242
242
  "spec/v2/core/security-defaults.md": "153884da7a2d54c30c81b5e649cfdef31e151b3a2a19dee1bc5a16c35d60a3aa",
243
243
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
244
244
  "spec/v2/core/versioning.md": "e3b3ab6d76e51035039a32173e9b412628a52c8fe7b16687ff58a2d0d983faa0",
245
- "spec/v2/core/webhooks.md": "6fc2fb7d9a993a682765ebc68e5c5bb215046ee599234ca7a660465279444eff",
245
+ "spec/v2/core/webhooks.md": "2fcb502be558014fd20b342687e0f078a5388fc430b2f99d883c0e73ddf7789f",
246
246
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
247
247
  "spec/v2/corrections.json": "59f647f4d22477f6c2b06dcf913b682dde5d51dd5e607a5d0cf648ab60864d6e",
248
248
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
249
- "spec/v2/declaration.json": "184b83919e56d2f12bda73c7772905810a441cbab53fc42ad0b3135937fac3e5",
249
+ "spec/v2/declaration.json": "aba200967c28190c6889c27dcfb01fd02919d0c023b88693e9ffc57e9488021a",
250
250
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
251
- "spec/v2/errors.json": "dc1a846abe3e888acb0ce4a33cf9d1fb98e41fcc8b49f42b3682fd65deca2eb2",
251
+ "spec/v2/errors.json": "fa650cbbdf25a140fdaf82ec5269f95b4585ac4bdde1fa6b0b3ffe48a3f7943c",
252
252
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
253
253
  "spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
254
254
  "spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
255
255
  "spec/v2/ext/README.md": "e2731ebb5156ece1840ed3b96e7a347f6345e73071ff4e54df0f21c1585c5027",
256
256
  "spec/v2/ext/a2uiSurface/README.md": "5518c7f355262eb7cc584422f4ff8e548f066b211878f969274a3a578127622d",
257
257
  "spec/v2/ext/brand/README.md": "82989c018d7965bab029bcce7a67b1d76ffc433d08971a4cac91befcccc09438",
258
- "spec/v2/ext/canvas/README.md": "0e91b68566d2be90319feedfaebe2cd2db0bd8f72fb59c958fa723c05e38023f",
258
+ "spec/v2/ext/canvas/README.md": "00ca0856e3a0146f1eeaec87209d450e2900ed2748dc7520cc60250a0d3ffbeb",
259
259
  "spec/v2/ext/chat/README.md": "f59b6706162abbfff96f8bccd4c0e1aeed721047cf62e9af58c875727a38d716",
260
260
  "spec/v2/ext/coordination/README.md": "c7fe0884f52405fdd02c5f9ec309ac6b585685ee06ec5f338fff7acf03c07148",
261
261
  "spec/v2/ext/dataIntegration/README.md": "286dec8aa29e4d3225cbd8b3a361f5a438c3288e759966b9415ecbb99c576e70",
@@ -268,7 +268,7 @@
268
268
  "spec/v2/ext/portability/README.md": "c59423ee446638b748a911b9e97e803c4786157be6120f656f0a060586bc7b82",
269
269
  "spec/v2/ext/provider-idempotency/README.md": "9d1bccef0ec16a19a3370ae79c2f5a1726ce0d1d25a51352907f288a2ccb15d7",
270
270
  "spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
271
- "spec/v2/ext/restTransport/README.md": "a8115faefeab01b7c85d516de60ebfc99b8e89552d16ba654456224518447b28",
271
+ "spec/v2/ext/restTransport/README.md": "c875fd7ae4818a5d5e93ab9b21e569de8700bb07c4dafacd60a46a46e4022ddb",
272
272
  "spec/v2/ext/sandbox-runtime-notes/README.md": "0d1290fb1e20b2e35c0b15051541ab6546e4e2a6f693b4f912cb3f7d6a65e92c",
273
273
  "spec/v2/ext/webResearch/README.md": "b675e6f6646128d781b661f754c2393eed86e5935d16c3a22df582a5ce025b9b",
274
274
  "spec/v2/facets/a2a.schema.json": "b68bf5e29e455e46c2ad7cfcccf5963730ec2310ff9db81884550d8df30fd6ef",
@@ -286,7 +286,7 @@
286
286
  "spec/v2/facets/sandbox.schema.json": "e75f54486d387b2d089791aa93b5093cd1450f1920753a0f97fc23acdfe978ed",
287
287
  "spec/v2/facets/schemaVersions.schema.json": "890d3416c4b66511d51f329abc68e6f467f4338f16807e489fe682999b2b56b9",
288
288
  "spec/v2/facets/supportedEnvelopes.schema.json": "644f22cc2236e30a611b5b7f1ba0458ff1925d68b94c514d9672583224e126bd",
289
- "spec/v2/facets/webhooks.schema.json": "2fac635ee5fbc10a14a1a46cf512a11bac660bfbabe0aea79f6c7b657ecb9e4e",
289
+ "spec/v2/facets/webhooks.schema.json": "54b576f1d150e28e1642693d846ce471c118e9c521bdf3924185305fbd1d0de4",
290
290
  "spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
291
291
  "spec/v2/harness-trust-anchors.json": "6007b92eae16a070fbc3777424efb0abfe865fda89d096963109b96282b91205",
292
292
  "spec/v2/harness-trust-anchors.schema.json": "372b5d92337c893d3449eff3f6bfd009ac09fdac285296d8bdbe0a08fff9e34c",
@@ -298,9 +298,9 @@
298
298
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
299
299
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
300
300
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
301
- "spec/v2/release.json": "ec99d76b53fba89d3d8788206c649a782774b55acf5284b75cab38b79ecf1d61",
301
+ "spec/v2/release.json": "a93ba35569a8a8bbc68243fbaef446363e5d5d5d0b8c1235ec68d0eed5ab6363",
302
302
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
303
- "spec/v2/surface-baseline.json": "3d3d0fc7c0b48e7d7339e5d8ada0640487908f37220b2de65ccc9bff093f876a"
303
+ "spec/v2/surface-baseline.json": "0d60d3d34ddc8f1290a2fc3a6be2e5ef5cfa211cde0918d8702ec4dd299d5549"
304
304
  },
305
- "corpusCommit": "6612868966f2ecb9672d0a64382cb7067491c004"
305
+ "corpusCommit": "8bb20a89a81544aaf90b3a375fc6b1f4ffef3a33"
306
306
  }
@@ -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,8 +62,10 @@ 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_CAP_MS, Math.max(bound + ADVERTISED_BOUND_GRACE_MS, capMs));
44
69
  const backoff = String(policy.backoff ?? '');
45
70
  return backoff === 'exponential' || backoff === 'fixed' ? capMs : RETRY_WAIT_FLOOR_MS;
46
71
  }
@@ -49,7 +74,7 @@ export function retryWaitFor(policy: { backoff?: string } | null, capMs: number)
49
74
  export function windowClosedNote(seen: number, maxAttempts: number, waitedMs: number, capMs: number): string {
50
75
  const raised = capMs > DEFAULT_RETRY_WAIT_CAP_MS;
51
76
  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. '
77
+ + '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
78
  + (raised
54
79
  ? `${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
80
  : `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, 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
  }
@@ -314,8 +314,8 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
314
314
  // exactly RETRY_WAIT_CAP_MS — a host loses by the width of one delivery.
315
315
  // The obvious fix is to derive the wait from the intervals, and it cannot
316
316
  // 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
317
+ // `additionalProperties: false` over { maxAttempts, backoff } (RFC 0225 adds
318
+ // an OPTIONAL total `maxElapsedMs`, not the intervals). THE BASE INTERVAL IS NOT ON THE WIRE, so the suite cannot compute the
319
319
  // time to the Nth attempt, and any cap I pick is 2.0.1's 20-second
320
320
  // deadline again with a bigger literal.
321
321
  //
@@ -447,6 +447,12 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
447
447
  // sink still FAILS below; so does a delivery that was never retried.
448
448
  const seen = ours().length;
449
449
  if (inSink === false && policy?.maxAttempts !== undefined && attempts.length > 1 && seen < policy.maxAttempts) {
450
+ // RFC 0225: a host that advertised maxElapsedMs promised the sink by then.
451
+ // Past that bound (plus the grace) the empty sink is a conviction, not a
452
+ // window the suite closed early.
453
+ if (pastAdvertisedBound(policy, retryWaitMs(doc))) {
454
+ 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);
455
+ }
450
456
  return blockedDespiteAssertions(windowClosedNote(seen, policy.maxAttempts, retryWaitMs(doc), RETRY_WAIT_CAP_MS));
451
457
  }
452
458
  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);
@@ -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);
@@ -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
@@ -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
  }