@openwop/openwop-conformance 2.44.3 → 2.44.5

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,19 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.44.5] — 2026-09-29 — a replay short-circuit raises no new interrupt request, and the hold node's outputs are pinned
4
+
5
+ - **The 2.44.5 cycle opens.** Class 3 and editorial follow-ups to 2.44.4 change the packed `@openwop/spec-artifacts` tree (`replay.md`, `fixtures.md`). No scenario is added or removed.
6
+ - **`core.conformance.hold`'s outputs are pinned, and `0223.replay-derives-rejection` cites its MUST.** The hold node's outputs are exactly its resolved inputs, `delayMs` included, under the same keys (`fixtures.md`), so hosts converge. The leg's requirement text now cites `replay.md` §Determinism caveats rule 2, which says a replay short-circuit raises no new `interrupt.requested` (clarified, Class 3).
7
+
8
+ ## [2.44.4] — 2026-09-29 — six more core families get v2 normative homes, replay derives a rejection and counts terminals, and the replay fixtures wait on a pure hold node
9
+
10
+ - **`0223.replay-derives-rejection`: replay derives a rejection, never re-decides it** (#1756). Two new legs in `v2-approval-reject-disposition` (target major 2), gated on `replay` advertising mode `replay` and on each fixture. The leg forks `mode: "replay"` at the gate's recorded `interrupt.resolved`, so the prefix holds the request and suspension and the fork must supply the resolution. It asserts four things. (i) No new `interrupt.requested` / `approval.requested` for the gate at or after `fromSeq`; this is the discriminator. (ii) The fork's `interrupt.resolved` carries the source's `decision`, `action` and `reason`. (iii) The fork fails the same way: `approval_rejected`, `failedNodeId: gate`, a non-retryable `node.failed`. (iv) Leg A only: a resolve answers `409 interrupt_already_resolved`. Leg B uses `conformance-approval-timeout-approve` and compares the fork's `resolvedBy` with the source's (a host subject or absent, RFC 0183), rather than requiring it absent. `interruptId` equality is not asserted. Forking at the gate's `node.started` was rejected: a host may re-emit the recorded request as history, which cannot be told from a new one. Measured on the v2 reference host (openwop-examples `b6302cd`): both legs `executed-pass`. With its replay derivation disabled, both fail.
11
+ - **The loop fixtures are bounded by their scenarios, not a host cap** (#1748). `conformance-approval-reject-loopback` and `conformance-replay-ordinal-loop` no longer claim to be bounded by `settings.maxLoopbackIterations`, which no spec binds. `v2-approval-reject-disposition`'s loopback leg and `v2-replay-suppression-ordinal` create the run with `configurable: { version: 1, run: { recursionLimit: 20 } }` as a runaway guard. A cycle-running host no longer needs a definition-level loopback cap to advertise either fixture.
12
+ - **`core.conformance.hold` replaces `core.delay` in the replay fixtures** (#1769). A new conformance-reserved PURE node (`fixtures.md`): it completes after `inputs.delayMs` with its inputs as outputs, is never classified side-effecting, and re-executes live in a replay. `conformance-replay-ordinal-loop` and the v1 `conformance-replay-side-effect` now wait on it. With `core.delay`, which the registry's flow pack declares `role: "side-effect"`, a host honouring `replay.md` rule 4 failed the wait with `replay_source_missing` before the node under test. `v2-replay-suppression-ordinal`'s docstring follows rule 2's re-correction: the ordinal counts recorded terminals, not `node.started`.
13
+ - **No leg change for #1770** (`interrupt.md` §Events: the kind-specific interrupt types are now a MAY). The suite never asserted their emission. `spec-artifacts` carries the corrected `interrupt.md`.
14
+ - **`webhook-signed-delivery`'s tunnel control registered the tunnel, not loopback, and failed conformant hosts.** The control leg ("with a tunnel wired, the host still refuses the loopback receiver") posted `receiver.url`, commented as "deliberately the raw loopback URL, NOT the tunnel". Since 2.37.0, `startScopedReceiver` returns the PUBLIC FRONT as `url` whenever `OPENWOP_WEBHOOK_RECEIVER_URL` is set: the tunnel, plus the exercise's nonce. So on every tunnelled cut the control registered a legitimate public https destination, a host with a working SSRF guard accepted it, and the row recorded `executed-fail` with "the guard is absent (a finding)". Measured on openwop-app production (2026-09-28, suite 2.43.1): the control failed while the same host answers `https://127.0.0.1:…` and `https://localhost:…` with `400 webhook_url_rejected` ("denied (loopback / link-local / private-IP)"). The control now registers `receiver.localUrl` rewritten to `https:`, so a refusal can only come from the loopback deny list and not from a scheme check. Untunnelled runs are unaffected, because the control soft-skips without a tunnel.
15
+ - **The 2.44.4 cycle opens.** Six more core families get a v2 normative home (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree: `memory`, `prompts` and `aiProviders` in `host-services.md`; `limits` in `runs.md`; `artifactTypes` in a new `artifact-type-packs.md`, with field-level rules in `schemas/v2/artifact-type-pack-manifest.schema.json` descriptions. v1-dependent core families: 8 → 3 (`agents`, `budget`, `production`, each blocked on error codes v2 does not register). No scenario changes.
16
+
3
17
  ## [2.44.3] — 2026-09-28 — fifteen more core families get v2 normative homes, and webhook retry rows record the wait path they measured
4
18
 
5
19
  - **Webhook retry rows say which wait path ran and what they measured** (RFC 0225 witness). A passing dead-letter row carried no detail, so a bundle could not show whether its wait came from the host's advertised `retryPolicy.maxElapsedMs`, an operator-raised cap or the 20 s floor, or when the dead-letter arrived. MyndHyve's 2.44.1 cut was the case: it advertises 600000, and it could not be counted as RFC 0225's witness. Rows driven by `retryWaitFor` now carry an informational detail, on passes too. `0173.webhook-durable-delivery.dead-letter` and `0188.dead-letter-content-free` record, for example, `observed: advertised-bound (maxElapsedMs 600000): waited ≤630000ms; 5 attempt(s), last attempt after 225031ms, sink after 231412ms`. The last attempt is a lower bound on the dead-letter time, and the sink read an upper bound. The retry leg, `v2-webhook-unregister-stops-delivery` and `v2-webhook-message-id-stable` record the path and window. The detail is prefixed `observed: `, never `partial-witness: `, so `check-accepted-predicate` counts the row as a full witness. The bundle already carries `detail` on every row inside the witness digest, so the note is signed. New `lib/row-observation.ts`, attached in `setup.ts`; `retryWaitSelection` / `waitObservation` in `lib/webhook-retry-window.ts`; server-free tests in `row-observation.test.ts` and `webhook-retry-window.test.ts`, each sabotage-checked.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.44.3",
4
- "stampSha256": "bc74ca389abf79ec8f23a07b8bb2742a6a5bab984fd5a6a06c56299dcf8b7076"
3
+ "version": "2.44.5",
4
+ "stampSha256": "140a372c32536f20db353da2daa11b6179322e13398756934c23eebe3227c1ea"
5
5
  }
@@ -2,7 +2,7 @@
2
2
  "id": "conformance-approval-reject-loopback",
3
3
  "name": "Conformance: Approval (reject loops back)",
4
4
  "version": "1.0",
5
- "description": "openwop#1697. A rejected approval gate routed over an any_failed edge to revise, which loops back to the gate (bounded by settings.maxLoopbackIterations 2). Rejected once, the gate is visited again: the host MUST raise a new interrupt.requested with a different key and wait for a new decision, never replaying the first rejection. Conformance scenario: v2-approval-reject-disposition.test.ts.",
5
+ "description": "openwop#1697. A rejected approval gate routed over an any_failed edge to revise, which loops back to the gate (the loop is bounded by the scenario, a single reject, not by the host; the scenario passes configurable.run.recursionLimit 20 as a runaway guard; settings.maxLoopbackIterations is authoring metadata, openwop#1748). Rejected once, the gate is visited again: the host MUST raise a new interrupt.requested with a different key and wait for a new decision, never replaying the first rejection. Conformance scenario: v2-approval-reject-disposition.test.ts.",
6
6
  "nodes": [
7
7
  { "id": "start", "typeId": "core.noop", "name": "Start", "position": { "x": 0, "y": 0 }, "config": {}, "inputs": {} },
8
8
  {
@@ -2,13 +2,13 @@
2
2
  "id": "conformance-replay-ordinal-loop",
3
3
  "name": "Conformance: Replay outcome keyed on the execution ordinal",
4
4
  "version": "1.0",
5
- "description": "openwop#1718. start → effect (a side-effecting node) → wait (core.delay) → back to effect, bounded by settings.maxLoopbackIterations 2, so effect executes twice. The suite cancels the source run inside wait, after effect's first execution completed and before its second, then forks mode:\"replay\" at effect's first node.started. In the fork, effect's first execution resolves from the recorded outcome for n = 1, and its second execution has no recorded outcome for n = 2, so it MUST fail closed with replay_source_missing (replay.md §Suppression rule 2). A host keying on (nodeId, attempt) or selecting the latest outcome resolves the second execution from the first and completes it. core.conformance.side-effect is the reserved side-effecting typeId (see conformance-replay-side-effect). Advertised only by a host that runs cycles and suppresses side effects in replay.",
5
+ "description": "openwop#1718. start → effect (a side-effecting node) → wait (core.conformance.hold, a reserved PURE node) → back to effect, bounded by the scenario (cancel plus fail-closed), not by the host; the scenario passes configurable.run.recursionLimit 20 as a runaway guard, and settings.maxLoopbackIterations is authoring metadata (openwop#1748), so effect executes twice. The suite cancels the source run inside wait, after effect's first execution completed and before its second, then forks mode:\"replay\" at effect's first node.started. In the fork, effect's first execution resolves from the recorded outcome for n = 1, and its second execution has no recorded outcome for n = 2, so it MUST fail closed with replay_source_missing (replay.md §Suppression rule 2). A host keying on (nodeId, attempt) or selecting the latest outcome resolves the second execution from the first and completes it. core.conformance.side-effect is the reserved side-effecting typeId (see conformance-replay-side-effect). Advertised only by a host that runs cycles and suppresses side effects in replay.",
6
6
  "nodes": [
7
7
  { "id": "start", "typeId": "core.noop", "name": "Start", "position": { "x": 0, "y": 0 }, "config": {}, "inputs": {} },
8
8
  { "id": "effect", "typeId": "core.conformance.side-effect", "name": "Side Effect", "position": { "x": 200, "y": 0 }, "config": {}, "inputs": {} },
9
9
  {
10
10
  "id": "wait",
11
- "typeId": "core.delay",
11
+ "typeId": "core.conformance.hold",
12
12
  "name": "Wait",
13
13
  "position": { "x": 400, "y": 0 },
14
14
  "config": {},
@@ -2,11 +2,11 @@
2
2
  "id": "conformance-replay-side-effect",
3
3
  "name": "Conformance: Replay Side-Effect Suppression",
4
4
  "version": "1.0",
5
- "description": "RFC 0140 fixture for `replay-side-effect-suppression.test.ts`. A `core.delay` gate followed by a node the host classifies as SIDE-EFFECTING. The test starts a run, cancels it mid-delay so the effect node never reaches a terminal outcome, then forks `mode:\"replay\"` — a host advertising `replay.sideEffectSuppression: \"recorded-outcome\"` MUST fail that node closed with `replay_source_missing` (replay.md §\"Side-effect suppression in replay\" requirement 3) rather than executing it. `core.conformance.side-effect` is a conformance-RESERVED typeId (same pattern as `core.conformance.mock-agent`): a host advertising the capability MUST map it to a node its replay classifier treats as side-effecting. The node's actual effect is irrelevant to the assertion — what matters is that the host would refuse to perform it during a replay. Hosts that do not advertise the capability need not supply this fixture; the scenario skips.",
5
+ "description": "RFC 0140 fixture for `replay-side-effect-suppression.test.ts`. A `core.conformance.hold` wait (a conformance-reserved PURE node that re-executes live in a replay; it replaced `core.delay` on 2026-09-29, openwop#1769, because the registry declares `core.delay` side-effecting) followed by a node the host classifies as SIDE-EFFECTING. The test starts a run, cancels it mid-delay so the effect node never reaches a terminal outcome, then forks `mode:\"replay\"` — a host advertising `replay.sideEffectSuppression: \"recorded-outcome\"` MUST fail that node closed with `replay_source_missing` (replay.md §\"Side-effect suppression in replay\" requirement 3) rather than executing it. `core.conformance.side-effect` is a conformance-RESERVED typeId (same pattern as `core.conformance.mock-agent`): a host advertising the capability MUST map it to a node its replay classifier treats as side-effecting. The node's actual effect is irrelevant to the assertion — what matters is that the host would refuse to perform it during a replay. Hosts that do not advertise the capability need not supply this fixture; the scenario skips.",
6
6
  "nodes": [
7
7
  {
8
8
  "id": "wait",
9
- "typeId": "core.delay",
9
+ "typeId": "core.conformance.hold",
10
10
  "name": "Wait",
11
11
  "position": {
12
12
  "x": 0,
package/fixtures.md CHANGED
@@ -255,7 +255,7 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
255
255
 
256
256
  - **Purpose**: witness that `key` is per visit (`spec/v2/core/interrupt.md` §Re-entry and resume values, openwop#1697).
257
257
  - **Inputs**: none.
258
- - **Graph**: `start` (`core.noop`) → `gate` (`any_success`); `gate` → `revise` (`core.noop`, `any_failed`); `revise` → `gate` (`any_success`). `settings.maxLoopbackIterations: 2`.
258
+ - **Graph**: `start` (`core.noop`) → `gate` (`any_success`); `gate` → `revise` (`core.noop`, `any_failed`); `revise` → `gate` (`any_success`). The loop is bounded by the scenario (a single reject; cancel plus fail-closed), not by the host. The scenario creates the run with `configurable: { version: 1, run: { recursionLimit: 20 } }` as a runaway guard (`runs.md` §run section). `settings.maxLoopbackIterations: 2` stays in the JSON as authoring metadata; the scenario does not depend on it (openwop#1748).
259
259
  - **Behavior**:
260
260
  1. Run reaches `gate` and MUST be `waiting-approval`.
261
261
  2. Client POSTs `{action: 'reject'}`. `gate` fails with `approval_rejected`, and `revise` runs.
@@ -306,8 +306,8 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
306
306
 
307
307
  ### `conformance-replay-ordinal-loop`
308
308
 
309
- - **Purpose**: witness that a replay fork keys a side-effecting node's recorded outcome on `(sourceRunId, nodeId, n)`, where `n` counts the node's `node.started` events through this execution (`spec/v2/core/replay.md` §Suppression rule 2, openwop#1718).
310
- - **Shape**: `start` (`core.noop`) → `effect` (`core.conformance.side-effect`, `any_success`) → `wait` (`core.delay`) → `effect` (`any_success`); `settings.maxLoopbackIterations: 2`, so `effect` executes twice.
309
+ - **Purpose**: witness that a replay fork keys a side-effecting node's recorded outcome on `(sourceRunId, nodeId, n)`, where `n` is one more than the node's recorded terminals (`node.completed`, `node.failed`) before this execution (`spec/v2/core/replay.md` §Suppression rule 2, openwop#1718, re-corrected by openwop#1769).
310
+ - **Shape**: `start` (`core.noop`) → `effect` (`core.conformance.side-effect`, `any_success`) → `wait` (`core.conformance.hold`) → `effect` (`any_success`), so `effect` executes twice. The loop is bounded by the scenario (a single reject; cancel plus fail-closed), not by the host. The scenario creates the run with `configurable: { version: 1, run: { recursionLimit: 20 } }` as a runaway guard (`runs.md` §run section). `settings.maxLoopbackIterations: 2` stays in the JSON as authoring metadata; the scenario does not depend on it (openwop#1748).
311
311
  - **Inputs**: `delayMs` (integer, default 3000), long enough to cancel inside `wait`.
312
312
  - **Expected behavior**:
313
313
  1. The source run completes `effect`'s first execution. The client cancels it inside `wait`, before the second execution.
@@ -318,7 +318,7 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
318
318
  ### `conformance-replay-side-effect`
319
319
 
320
320
  - **Purpose**: verify RFC 0140 — a `mode:"replay"` fork MUST NOT re-perform a side-effecting node's effect.
321
- - **Shape**: `core.delay` (`wait`) → `core.conformance.side-effect` (`effect`).
321
+ - **Shape**: `core.conformance.hold` (`wait`) → `core.conformance.side-effect` (`effect`). *(Corrected 2026-09-29, openwop#1769: `wait` was `core.delay`, which the registry's flow pack declares `role: "side-effect"`; a host honouring that floor resolved `wait` from the cancelled source and failed it with `replay_source_missing` before `effect` was reached.)*
322
322
  - **`core.conformance.side-effect` is a conformance-RESERVED typeId** (same pattern as `core.conformance.mock-agent`): a host advertising `replay.sideEffectSuppression: "recorded-outcome"` MUST map it to a node its replay classifier treats as side-effecting. The node's actual effect is irrelevant to the assertion — what matters is that the host would refuse to perform it during a replay. Hosts that do not advertise the capability need not supply this fixture; the scenario skips.
323
323
  - **Inputs**:
324
324
  - `delayMs` (integer, required, 1 ≤ value ≤ 60000) — long enough for the test to cancel before `effect` runs.
@@ -590,6 +590,7 @@ The fixtures reference these typeIds:
590
590
  | `core.delay` | delay, cancellable | Sleep `config.delayMs` ms |
591
591
  | `core.fail` | failure | Throw with `code: "example.conformance_failure"` (a vendor code under the registered `example` org; errors.md §The registry, openwop#1698), message: "Intentional conformance failure" |
592
592
  | `core.approvalGate` | approval | Call `ctx.interrupt({kind: 'approval', ...})` |
593
+ | `core.conformance.hold` | replay-side-effect, replay-ordinal-loop | Conformance-RESERVED (openwop#1769). Completes after `inputs.delayMs`; its outputs are exactly its resolved inputs, `delayMs` included, under the same keys. It performs nothing observable outside the run's own event log. A host advertising a fixture that uses it MUST NOT classify it side-effecting (`replay.md` §Suppression rule 1) and MUST re-execute it live in a replay. |
593
594
  | `core.clarificationGate` | clarification | Call `ctx.interrupt({kind: 'clarification', ...})` |
594
595
  | `conformance.requiresMissing` | capability-missing | Declares `requires: ['conformance.never-provided']`; engine MUST refuse dispatch. Opt-in fixture registration is recommended so production deployments don't expose the fixture surface. |
595
596
  | `conformance.artifact.emit` | artifact-emit | Opt-in (RFC 0205). Produce one artifact of `config.artifactType` with payload `config.data`; emit `artifact.created` naming it; readable through `getArtifact`. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.44.3",
3
+ "version": "2.44.5",
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.44.3"
59
+ "@openwop/spec-artifacts": "2.44.5"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
5
  "files": 648,
6
- "tests": 2490,
7
- "withStableId": 2490,
6
+ "tests": 2492,
7
+ "withStableId": 2492,
8
8
  "interpolatedTitles": 0,
9
9
  "explicitIds": 2372
10
10
  },
@@ -22926,7 +22926,7 @@
22926
22926
  {
22927
22927
  "id": "openwop.it.replay-side-effect-suppression.sideeffectsuppression-when-present-is-one-of-the-two-defined-values",
22928
22928
  "file": "replay-side-effect-suppression.test.ts",
22929
- "line": 101,
22929
+ "line": 103,
22930
22930
  "title": "sideEffectSuppression, when present, is one of the two defined values",
22931
22931
  "explicitId": "openwop.it.replay-side-effect-suppression.sideeffectsuppression-when-present-is-one-of-the-two-defined-values",
22932
22932
  "citations": [
@@ -22939,7 +22939,7 @@
22939
22939
  {
22940
22940
  "id": "openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne",
22941
22941
  "file": "replay-side-effect-suppression.test.ts",
22942
- "line": 115,
22942
+ "line": 117,
22943
22943
  "title": "fails a side-effecting node CLOSED with replay_source_missing when the source never recorded it",
22944
22944
  "explicitId": "openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne",
22945
22945
  "citations": [
@@ -22974,7 +22974,7 @@
22974
22974
  {
22975
22975
  "id": "openwop.it.replay-side-effect-suppression.the-manifest-still-offers-the-role-value-the-requirement-binds",
22976
22976
  "file": "replay-side-effect-suppression.test.ts",
22977
- "line": 240,
22977
+ "line": 242,
22978
22978
  "title": "the manifest still offers the role value the requirement binds",
22979
22979
  "explicitId": "openwop.it.replay-side-effect-suppression.the-manifest-still-offers-the-role-value-the-requirement-binds",
22980
22980
  "citations": [
@@ -22988,7 +22988,7 @@
22988
22988
  {
22989
22989
  "id": "openwop.it.replay-side-effect-suppression.replay-md-binds-that-declaration-naming-the-schema-it-comes-from",
22990
22990
  "file": "replay-side-effect-suppression.test.ts",
22991
- "line": 251,
22991
+ "line": 253,
22992
22992
  "title": "replay.md binds that declaration, naming the schema it comes from",
22993
22993
  "explicitId": "openwop.it.replay-side-effect-suppression.replay-md-binds-that-declaration-naming-the-schema-it-comes-from",
22994
22994
  "citations": [
@@ -23002,7 +23002,7 @@
23002
23002
  {
23003
23003
  "id": "openwop.it.replay-side-effect-suppression.a-throw-is-named-a-backstop-not-a-discharge",
23004
23004
  "file": "replay-side-effect-suppression.test.ts",
23005
- "line": 261,
23005
+ "line": 263,
23006
23006
  "title": "a throw is named a backstop, not a discharge",
23007
23007
  "explicitId": "openwop.it.replay-side-effect-suppression.a-throw-is-named-a-backstop-not-a-discharge",
23008
23008
  "citations": [
@@ -23016,7 +23016,7 @@
23016
23016
  {
23017
23017
  "id": "openwop.it.replay-side-effect-suppression.the-floor-direction-is-stated-not-left-to-inference",
23018
23018
  "file": "replay-side-effect-suppression.test.ts",
23019
- "line": 285,
23019
+ "line": 287,
23020
23020
  "title": "the floor direction is stated, not left to inference",
23021
23021
  "explicitId": "openwop.it.replay-side-effect-suppression.the-floor-direction-is-stated-not-left-to-inference",
23022
23022
  "citations": [
@@ -27856,7 +27856,7 @@
27856
27856
  {
27857
27857
  "id": "openwop.it.v2-approval-reject-disposition.a-rejected-gate-with-no-routing-edge-fails-the-run-with-approval-rejected",
27858
27858
  "file": "v2-approval-reject-disposition.test.ts",
27859
- "line": 126,
27859
+ "line": 141,
27860
27860
  "title": "a rejected gate with no routing edge fails the run with approval_rejected",
27861
27861
  "explicitId": null,
27862
27862
  "citations": [
@@ -27879,7 +27879,7 @@
27879
27879
  {
27880
27880
  "id": "openwop.it.v2-approval-reject-disposition.interrupt-resolved-records-action-reject-and-decision-rejected",
27881
27881
  "file": "v2-approval-reject-disposition.test.ts",
27882
- "line": 137,
27882
+ "line": 152,
27883
27883
  "title": "interrupt.resolved records action reject and decision rejected",
27884
27884
  "explicitId": null,
27885
27885
  "citations": [
@@ -27898,7 +27898,7 @@
27898
27898
  {
27899
27899
  "id": "openwop.it.v2-approval-reject-disposition.the-gate-node-fails-with-approval-rejected-not-retryable-and-run-failed-names-it",
27900
27900
  "file": "v2-approval-reject-disposition.test.ts",
27901
- "line": 150,
27901
+ "line": 165,
27902
27902
  "title": "the gate node fails with approval_rejected, not retryable, and run.failed names it",
27903
27903
  "explicitId": null,
27904
27904
  "citations": [
@@ -27919,7 +27919,7 @@
27919
27919
  {
27920
27920
  "id": "openwop.it.v2-approval-reject-disposition.a-majority-reject-decides-the-quorum-gate-on-the-deciding-vote-and-fails-the-run",
27921
27921
  "file": "v2-approval-reject-disposition.test.ts",
27922
- "line": 163,
27922
+ "line": 178,
27923
27923
  "title": "a majority reject decides the quorum gate on the deciding vote and fails the run with approval_rejected",
27924
27924
  "explicitId": null,
27925
27925
  "citations": [
@@ -27945,7 +27945,7 @@
27945
27945
  {
27946
27946
  "id": "openwop.it.v2-approval-reject-disposition.a-rejected-gate-routed-over-an-any-failed-edge-fails-as-a-node-its-all-success-s",
27947
27947
  "file": "v2-approval-reject-disposition.test.ts",
27948
- "line": 186,
27948
+ "line": 201,
27949
27949
  "title": "a rejected gate routed over an any_failed edge fails as a node, its all_success sibling never runs, and the run completes",
27950
27950
  "explicitId": null,
27951
27951
  "citations": [
@@ -27971,7 +27971,7 @@
27971
27971
  {
27972
27972
  "id": "openwop.it.v2-approval-reject-disposition.an-unresolved-gate-past-its-timeoutms-with-no-ontimeout-is-resolved-rejected-by",
27973
27973
  "file": "v2-approval-reject-disposition.test.ts",
27974
- "line": 205,
27974
+ "line": 220,
27975
27975
  "title": "an unresolved gate past its timeoutMs with no onTimeout is resolved rejected by the host and fails the run naming the gate",
27976
27976
  "explicitId": null,
27977
27977
  "citations": [
@@ -27992,7 +27992,7 @@
27992
27992
  {
27993
27993
  "id": "openwop.it.v2-approval-reject-disposition.a-gate-whose-ontimeout-is-approve-still-resolves-rejected-on-timeout-and-fails-t",
27994
27994
  "file": "v2-approval-reject-disposition.test.ts",
27995
- "line": 224,
27995
+ "line": 239,
27996
27996
  "title": "a gate whose onTimeout is approve still resolves rejected on timeout and fails the run: a timeout never grants",
27997
27997
  "explicitId": null,
27998
27998
  "citations": [
@@ -28014,7 +28014,7 @@
28014
28014
  {
28015
28015
  "id": "openwop.it.v2-approval-reject-disposition.a-rejected-gate-looped-back-to-itself-is-asked-again-under-a-new-key-and-the-fir",
28016
28016
  "file": "v2-approval-reject-disposition.test.ts",
28017
- "line": 242,
28017
+ "line": 257,
28018
28018
  "title": "a rejected gate looped back to itself is asked again under a new key, and the first rejection is not replayed",
28019
28019
  "explicitId": null,
28020
28020
  "citations": [
@@ -28037,6 +28037,33 @@
28037
28037
  }
28038
28038
  ]
28039
28039
  },
28040
+ {
28041
+ "id": "openwop.it.v2-approval-reject-disposition.a-replay-fork-of-a-decided-reject-derives-the-rejection-from-the-log-and-never-r",
28042
+ "file": "v2-approval-reject-disposition.test.ts",
28043
+ "line": 333,
28044
+ "title": "a replay fork of a decided reject derives the rejection from the log and never re-decides it (leg A)",
28045
+ "explicitId": null,
28046
+ "citations": [
28047
+ {
28048
+ "section": "spec/v2/core/errors.md §One code per state",
28049
+ "requirement": "a resolve against the replayed gate MUST answer 409 interrupt_already_resolved — the fork holds no open decision"
28050
+ }
28051
+ ]
28052
+ },
28053
+ {
28054
+ "id": "openwop.it.v2-approval-reject-disposition.a-replay-fork-of-a-timed-out-gate-derives-the-timeout-rejection-from-the-log-and",
28055
+ "file": "v2-approval-reject-disposition.test.ts",
28056
+ "line": 348,
28057
+ "title": "a replay fork of a timed-out gate derives the timeout rejection from the log and never re-arms it (leg B)",
28058
+ "explicitId": null,
28059
+ "citations": [
28060
+ {
28061
+ "section": null,
28062
+ "requirement": "leg B: the derived resolution is the recorded timeout — action timeout, reason timeout, and the source's resolvedBy (a host subject or absent), never a caller",
28063
+ "interpolated": true
28064
+ }
28065
+ ]
28066
+ },
28040
28067
  {
28041
28068
  "id": "openwop.it.v2-approver-enforced.a-resolver-outside-approverslist-is-refused-at-resolve-time",
28042
28069
  "file": "v2-approver-enforced.test.ts",
@@ -33078,7 +33105,7 @@
33078
33105
  {
33079
33106
  "id": "openwop.it.v2-replay-suppression-ordinal.a-replay-fork-resolves-each-execution-of-a-looped-side-effecting-node-by-its-own",
33080
33107
  "file": "v2-replay-suppression-ordinal.test.ts",
33081
- "line": 73,
33108
+ "line": 74,
33082
33109
  "title": "a replay fork resolves each execution of a looped side-effecting node by its own ordinal, failing closed where none was recorded",
33083
33110
  "explicitId": "openwop.requirement.replay.suppression-execution-ordinal",
33084
33111
  "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.44.3",
5
- "corpusTag": "v2.44.3",
4
+ "version": "2.44.5",
5
+ "corpusTag": "v2.44.5",
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": "3319d19f95c90c82ad611d3698daa77062760aaed9c703000f0eaa93cdcb3ed3",
13
- "api/v2/asyncapi.yaml": "fc977f137a3a40ac20c17911f9987e55f1b93ab5e73d039e94ab2749537ad4b3",
14
- "api/v2/openapi.yaml": "353fe2c7b556dfa12f45f85783e0b1409ae3f6e59755719f08ba525b830189ac",
12
+ "api/seams-v2.yaml": "27a2f887597684e7a1ceeb275970e7c7079650881f238be26bdd8d940dfe5a5e",
13
+ "api/v2/asyncapi.yaml": "9ed411f174d9235d47fae7edee86edb2b4ea51cefffd17179e727b00d2f5fe79",
14
+ "api/v2/openapi.yaml": "b479b1cb2319a60b5d48c471b793646cce843564a447a136eebf12636ec1a75d",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -112,7 +112,7 @@
112
112
  "schemas/v2/ai-envelope.schema.json": "0dc7dfbf032a37239ffd81fc2d67f999a40782ff5074866a56e414164cdee809",
113
113
  "schemas/v2/annotation-create.schema.json": "ec32c22a243b343eeb6956a23be2d76ba5b50c8018ec3ec9d6efa5a1d9b0c792",
114
114
  "schemas/v2/annotation.schema.json": "eb4813cdaa8e325412cfcaeeee5c8a28f3751c946dec64e546c7ba0ad3aad8d4",
115
- "schemas/v2/artifact-type-pack-manifest.schema.json": "c8820351a2ab93855bd654ad3d8a9788f97975d09b3a7a0418e374ed6114595d",
115
+ "schemas/v2/artifact-type-pack-manifest.schema.json": "acbbf14d32da4e0cb81254bbd6cc48fc10083890572044ddef71c59a0f65c185",
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",
@@ -193,53 +193,54 @@
193
193
  "schemas/v2/webhook-delivery.schema.json": "4e8ab3b722ad1731adf0548c1217f0de62da40de14e8abf77533aee4ace4c079",
194
194
  "schemas/v2/webhook-verification.schema.json": "76e4e0d81143ffd2649bc7adba2aa34f2525e2c4e9f0300a425d908b3163196b",
195
195
  "schemas/v2/workflow-chain-pack-manifest.schema.json": "0816c6c59a91452d6fa1931394b766780eaf90949c9f7cd0ced522dadd32ff81",
196
- "schemas/v2/workflow-definition.schema.json": "b9043e3413fc01d939e7b2892cc0fe28bdfd77d5bacc561b6ca4ae20197b985b",
196
+ "schemas/v2/workflow-definition.schema.json": "3f031c0cb1644ac21f203945a0860e04dcea4bd1ab722f280bdcdc788edd0a22",
197
197
  "schemas/v2/workload-identity.schema.json": "1f589debfac6d70e0908da511550ec69a27b4bcc8314551cd843b6d923605efc",
198
198
  "schemas/v2/workspace-file-create.schema.json": "b6b5260857318c3b67de0231ed15e3a746f525880a763e7b36a7854178dd10d0",
199
199
  "schemas/v2/workspace-file.schema.json": "d2ed39e8c229dde33b56905c8df6f8b2ff056c97e1c5f615e3124baa54a6254f",
200
200
  "schemas/workflow-chain-pack-manifest.schema.json": "c027571913c44d8d9678750c72701a710a02fa0df55e86849da03e13e95c333a",
201
- "schemas/workflow-definition.schema.json": "28b050ffe22121796c9d4a5a8c7e57c901bb283bd08d3fd3ff2ea25a898108c1",
201
+ "schemas/workflow-definition.schema.json": "fbab3787707fc0e00ecf2adea8de8aba582e6057af9cde893b1b0eb969adea83",
202
202
  "schemas/workload-identity.schema.json": "64ed93e12cc9f0657e56ff9b8f045e5d68d397df5276d103c07f42c7f34f0579",
203
203
  "schemas/workspace-file-create.schema.json": "70a23b92c2958c0165d7d29aea9020a045dc7816af82c80c5f50d0aac19c4856",
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": "8789a8c30013d9137db12e2cac6bd38b461a0f16147617366263b26bd265a1d8",
207
+ "spec/v1/core-standard-manifest.json": "2a7f88488571ffee4073ce1b7fc77abbfd539b9c493d60a7cd4887317ede9b17",
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": "3eb4316a7895eeefd9bb918e496b145d8fc0e39d5cbc5dcc72e2fbefda6803ed",
213
+ "spec/v1/gaps.json": "11c35393ad666899fe8a668c45fa69f79d20361ab57e84949dce3bc2f528d5c3",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
- "spec/v1/migrations.json": "2efcd9064b2bc65a84a3b3d281aa86a91efd002690c1f4f14e8335c4a18194d1",
215
+ "spec/v1/migrations.json": "ca57b7ce335a681aaa78573ce229e9c3d7d75859f03e087ae93bbfc889359127",
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": "ae7e9dceb3a6a1df75259de43c08244840c8ca8380a3eb4c727ae07dc4bdc4b1",
219
+ "spec/v2/README.md": "ddea90d7cf14aefffe1a43eeeecd4222dadad1d1e5ac64dceb073990eb93d9ef",
220
+ "spec/v2/core/artifact-type-packs.md": "e143bfd6f98d4867855bb57090a6b854e98d4b0a6202b9b3734cb45366f99854",
220
221
  "spec/v2/core/capabilities.md": "ad728e8a761153b55d036ece55043ff62d0982121e2e629e5e7cdf560fca3c03",
221
222
  "spec/v2/core/conformance.md": "0cfceb926eed87055550a5c23e640210c795fc8cf9dd1b2544db0e2b3842c564",
222
223
  "spec/v2/core/connection-packs.md": "1ee129d792c537bfe82393ccb92555d8e6ddc342b1aa781a13f101b27ec38171",
223
224
  "spec/v2/core/conversation.md": "354fa57c73d11062e523a44aa75a6353c5c81aefaf306d30c42416a228c82c86",
224
225
  "spec/v2/core/errors.md": "6d5a67389de84556f81e2558a30d83959c9283c8c5481ce7c759021e486d84fd",
225
226
  "spec/v2/core/events.md": "8a2767691f604b2adf76399edeeaccfc151c697fc9717dc17cf7dd57372a8c22",
226
- "spec/v2/core/execution.md": "ec1afd30a2b7dab4a722f4502764cb752685e441ecd6ca61e96ed775163be139",
227
+ "spec/v2/core/execution.md": "299c4d548e16e5a0809957a47cbd34b56a38de82b8c9b3a5d036b2c7abca09c6",
227
228
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
228
229
  "spec/v2/core/headers.md": "5cb1624fa63acc81992b6a6d842ec392cf7271aaf2f65d1f83c8c4fa286e6730",
229
- "spec/v2/core/host-services.md": "244e222ec6f7d4774751bd3cc124bbc9f548fdd84fabca65c19159ff00d49e84",
230
+ "spec/v2/core/host-services.md": "aa8b269e6348bdcb797da1a23c82f9df95c2133a7b947253799f6eaf88ccd3f1",
230
231
  "spec/v2/core/i18n.md": "2d5a67bd3b9b956d1144acab2fbc3646cdb50b3621f97d772087eaa953798286",
231
232
  "spec/v2/core/idempotency.md": "2084d756789c1faf5121593fb39e67ca36a4c227178e175004742e403178ea49",
232
233
  "spec/v2/core/identity.md": "582eddbf2e1bad41911e886bd08524683837045404f7b8f6bceccf61792b2765",
233
234
  "spec/v2/core/interop.md": "e72e25dff864c83b2980d7366b34054cffa072af0225821199f260f6228d260e",
234
- "spec/v2/core/interrupt.md": "4c825634acac72900ebd3c5ac5941f55648fa33d2df92913ef16371dcb700a34",
235
+ "spec/v2/core/interrupt.md": "c1e13179e792bf8099b2d1c7891542646bb1d4cdee1757590f190833cae6047c",
235
236
  "spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
236
237
  "spec/v2/core/oauth.md": "c96483a44b62978c596945fbb7d7c863cf6056cd980fdb4cf4e97555e3e10b72",
237
- "spec/v2/core/overview.md": "76fdb48ea68de416d6a63ae3b3ec750cafda01d234ea22a1096426fded7b54f3",
238
- "spec/v2/core/packs.md": "5929e708bd4e79f6e1beea570af583c80f3b63bb21dec78a5df03c235e73364a",
238
+ "spec/v2/core/overview.md": "d7000fbcd2f59280870fc592f5d5ea28a5195b9eb209029f09c5de59e0f2bf9c",
239
+ "spec/v2/core/packs.md": "935fc2f633c68fa98ad6c9cdb083dd556e955b1a15eff32f4773d13329a57238",
239
240
  "spec/v2/core/persistence.md": "711874f74cb08b03cd118d683056db10a063517762fcea73ec70feb5d983e6a4",
240
241
  "spec/v2/core/portability.md": "87c4327f674f714fda6217fd79b99d2ba219ad47dc02f9751bb56c200fb2c17a",
241
- "spec/v2/core/replay.md": "4d379494057746c6883f6047972e93e621863b998ed291b9a691d3aa57c1ff43",
242
- "spec/v2/core/runs.md": "d0e84ccf7cd7dfa657a8d9a90548dbe1d1f7033be30b405d56c41e36007af977",
242
+ "spec/v2/core/replay.md": "e7f935c215cae84d58da9e6babcb37ce0afe6217c453d1f50199d5ebd288a5cf",
243
+ "spec/v2/core/runs.md": "8a1d71fb9305fd0df31150bfbbd892f96b8aab553400a36196212f7d0a24906d",
243
244
  "spec/v2/core/security-defaults.md": "153884da7a2d54c30c81b5e649cfdef31e151b3a2a19dee1bc5a16c35d60a3aa",
244
245
  "spec/v2/core/storage.md": "598ad4f3775c297c698a20daf72a7d15a6836c1231ddbca5961865c704f765b5",
245
246
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
@@ -248,7 +249,7 @@
248
249
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
249
250
  "spec/v2/corrections.json": "59f647f4d22477f6c2b06dcf913b682dde5d51dd5e607a5d0cf648ab60864d6e",
250
251
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
251
- "spec/v2/declaration.json": "25ceb30f1b055afa529c2d822e768498e8c3e95bdc652b5edb2404bf75ac96ee",
252
+ "spec/v2/declaration.json": "edace8c4ec7e32aba44828f459e2ae8933dc88ec8129f16549bca6279e3ca24c",
252
253
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
253
254
  "spec/v2/errors.json": "fa650cbbdf25a140fdaf82ec5269f95b4585ac4bdde1fa6b0b3ffe48a3f7943c",
254
255
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
@@ -266,11 +267,11 @@
266
267
  "spec/v2/ext/kanban/README.md": "bfc386dc0f874a4afa1e3ec6a2f99084b547dcce9bd363da3b8ebd8786b9c184",
267
268
  "spec/v2/ext/knowledge/README.md": "cbe1f945a757791dedac0fd542a68c86c5b457778b831d58d9ae44a2baa270f2",
268
269
  "spec/v2/ext/launchStudio/README.md": "a941ef1d2401438a412eaea48d2d6ca807a032b44ea08719fea33120daf088a0",
269
- "spec/v2/ext/messaging/README.md": "949f350e35294f52ad490e42699f8c560cd154549516fbaa1805f06312b7f342",
270
+ "spec/v2/ext/messaging/README.md": "ee2d19fb6f6c2de11d889e966a41e6d1ff34272771689a81d7de039105a5be03",
270
271
  "spec/v2/ext/portability/README.md": "c59423ee446638b748a911b9e97e803c4786157be6120f656f0a060586bc7b82",
271
272
  "spec/v2/ext/provider-idempotency/README.md": "9d1bccef0ec16a19a3370ae79c2f5a1726ce0d1d25a51352907f288a2ccb15d7",
272
273
  "spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
273
- "spec/v2/ext/restTransport/README.md": "c875fd7ae4818a5d5e93ab9b21e569de8700bb07c4dafacd60a46a46e4022ddb",
274
+ "spec/v2/ext/restTransport/README.md": "bd7207683b2fbcead78bdb7d474fa723e08adad500185ea10804fd4e99541c0c",
274
275
  "spec/v2/ext/sandbox-runtime-notes/README.md": "0d1290fb1e20b2e35c0b15051541ab6546e4e2a6f693b4f912cb3f7d6a65e92c",
275
276
  "spec/v2/ext/webResearch/README.md": "b675e6f6646128d781b661f754c2393eed86e5935d16c3a22df582a5ce025b9b",
276
277
  "spec/v2/facets/a2a.schema.json": "b68bf5e29e455e46c2ad7cfcccf5963730ec2310ff9db81884550d8df30fd6ef",
@@ -300,9 +301,9 @@
300
301
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
301
302
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
302
303
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
303
- "spec/v2/release.json": "f84bac4d5066e20022a2f9011a4f7441e74b261f3e41da59990cf52214896478",
304
+ "spec/v2/release.json": "ec7e5ac7845e1c7ff67f0f522f5897db65bf840c7787768ac0324095baad2e2f",
304
305
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
305
- "spec/v2/surface-baseline.json": "8acb9fab0aca69abfe40dc56fcf48729aaf0d56a41467962e5c7247f3548eea3"
306
+ "spec/v2/surface-baseline.json": "ca7310e7b970f4cd5629e20718d77ee64427a04c7aba2e963bbfd43500ba1521"
306
307
  },
307
- "corpusCommit": "e8eda2ffa2042f1ab81232cfe4fe5657c9576032"
308
+ "corpusCommit": "e84e633368000323a54ddd6913cadcabafb268c6"
308
309
  }
@@ -34,7 +34,9 @@
34
34
  *
35
35
  * ## Construction
36
36
  *
37
- * `conformance-replay-side-effect` is `core.delay` → a side-effecting node. We
37
+ * `conformance-replay-side-effect` is `core.conformance.hold` (a reserved PURE
38
+ * wait; it was `core.delay`, which the registry declares side-effecting,
39
+ * openwop#1769) → a side-effecting node. We
38
40
  * start it with a long delay, cancel mid-flight so the effect node never
39
41
  * records a terminal outcome, then fork `mode:"replay"`. Requirement 3 of
40
42
  * `replay.md` §"Side-effect suppression in replay" then applies to that node.
@@ -145,7 +147,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('replay-side-effect-suppression: a replay does
145
147
  // side-effecting node downstream never reaches a terminal outcome.
146
148
  //
147
149
  // Kept SHORT on purpose: a `replay` fork re-executes from seq 0, so it
148
- // re-runs this delay too (a delay is not side-effecting). A 30s delay
150
+ // re-runs this hold too (`core.conformance.hold` is pure). A 30s delay
149
151
  // here would make the fork outlive any sane poll budget — measured, not
150
152
  // theorised: the first version of this scenario used 30s and timed out
151
153
  // at step 3 with the fork still `running`.
@@ -182,7 +184,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('replay-side-effect-suppression: a replay does
182
184
  }
183
185
  expect(fork.status, req('openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne', 'replay.md §"Side-effect suppression in replay" requirement 1', 'fork should be accepted')).toBe(201);
184
186
  const forkRunId = (fork.json as { runId: string }).runId;
185
- await pollUntilTerminal(forkRunId, { timeoutMs: 45_000 }); // re-runs the 5s delay before reaching `effect`
187
+ await pollUntilTerminal(forkRunId, { timeoutMs: 45_000 }); // re-runs the pure hold before reaching `effect`
186
188
 
187
189
  // 3. THE assertion. A non-suppressing host executes the node; a suppressing
188
190
  // host cannot, and has no recorded outcome to serve, so it must fail
@@ -33,6 +33,21 @@
33
33
  * rejection is not replayed). `inapplicable` on a host that does not
34
34
  * advertise the fixture, i.e. one that does not run cycles.
35
35
  *
36
+ * 9. `openwop.requirement.0223.replay-derives-rejection` (openwop#1756, suite
37
+ * 2.44.4; gated on `replay` advertising mode `replay` and on each leg's
38
+ * fixture): a `replay` fork taken at the gate's recorded `interrupt.resolved` (the
39
+ * prefix holds its request and suspension) derives
40
+ * the rejection from the recorded `interrupt.resolved` and never
41
+ * re-decides it. The fork raises no new `interrupt.requested` for the
42
+ * gate (the discriminator: a re-deciding host re-raises and waits), records
43
+ * an `interrupt.resolved` whose `decision` / `action` / `reason` equal the
44
+ * source's, fails the same way (`approval_rejected`, `failedNodeId`, a
45
+ * non-retryable `node.failed`), and answers a resolve with `409
46
+ * interrupt_already_resolved`. Leg A replays a decided reject
47
+ * (`conformance-approval`); leg B replays a timeout
48
+ * (`conformance-approval-timeout-approve`), whose `resolvedBy` must match the source's (a host subject or absent).
49
+ * `interruptId` equality is not asserted: a host may re-mint ids.
50
+ *
36
51
  * The legs are separate rows so a host that misses one field fails that row
37
52
  * only. Legs 5 and 6 each need their own fixture, because `conformance-approval`
38
53
  * has no edges and no timer and widening a fixture every host serves would force
@@ -98,8 +113,8 @@ const errorCode = (o: unknown): string | undefined => (o as { code?: unknown } |
98
113
  const resolve = (runId: string, resumeValue: unknown): Promise<OpenWOPResponse | null> => http(() => driver.post(`/runs/${enc(runId)}/interrupts/${enc(NODE_ID)}`, { resumeValue }));
99
114
 
100
115
  /** Create a run of `fixture` and wait for it to suspend on its gate. */
101
- async function suspended(fixture: string): Promise<{ runId: string } | { reason: string }> {
102
- const res = await http(() => driver.post('/runs', { workflowId: fixture }));
116
+ async function suspended(fixture: string, extra: Record<string, unknown> = {}): Promise<{ runId: string } | { reason: string }> {
117
+ const res = await http(() => driver.post('/runs', { workflowId: fixture, ...extra }));
103
118
  if (res === null) return { reason: 'POST /runs unreachable (fetch failed)' };
104
119
  const runId = (res.json as { runId?: unknown } | null)?.runId;
105
120
  if (res.status !== 201 || typeof runId !== 'string') return { reason: `POST /runs (${fixture}) answered ${res.status} ${readErrorCode(res.json) ?? ''}`.trim() };
@@ -242,7 +257,8 @@ describe('RFC 0223 — v2-approval-reject-disposition (gated on interrupt + conf
242
257
  it('a rejected gate looped back to itself is asked again under a new key, and the first rejection is not replayed', async () => {
243
258
  if (!(await gateFamily('interrupt'))) return softSkip('inapplicable', 'interrupt family not advertised (gate recorded under openwop.family.interrupt)');
244
259
  if (!isFixtureAdvertised(LOOPBACK)) return softSkip('inapplicable', `fixture ${LOOPBACK} is not advertised — the host does not run cycles`);
245
- const s = await suspended(LOOPBACK);
260
+ // The loop is bounded by the scenario (one reject), not by the host; recursionLimit is a runaway guard (openwop#1748).
261
+ const s = await suspended(LOOPBACK, { configurable: { version: 1, run: { recursionLimit: 20 } } });
246
262
  if ('reason' in s) return softSkip('blocked', s.reason);
247
263
  const res = await resolve(s.runId, { action: 'reject' });
248
264
  if (res === null || res.status < 200 || res.status >= 300) return softSkip('blocked', `the reject resolve answered ${res?.status ?? 'nothing'} ${readErrorCode(res?.json) ?? ''} — interrupt resolution owns that contract`);
@@ -259,4 +275,91 @@ describe('RFC 0223 — v2-approval-reject-disposition (gated on interrupt + conf
259
275
  expect(evs.filter((e) => e.type === 'interrupt.resolved').length, req(id, 'spec/v2/core/interrupt.md §Re-entry and resume values', 'an earlier visit\'s resumeValue MUST NOT be returned: only the one caller resolve is recorded')).toBe(1);
260
276
  await http(() => driver.post(`/runs/${enc(s.runId)}/cancel`, {}));
261
277
  }, 45_000);
278
+
279
+ // ── openwop#1756: replay derives the rejection, never re-decides it ──────────
280
+ const REPLAY_ID = R('replay-derives-rejection');
281
+ const REPLAY_DOC = `${DOC}, last bullet; spec/v2/core/replay.md §Determinism caveats rule 2 (short-circuit, raising no new interrupt.requested)`;
282
+ interface SeqEv extends Ev { readonly sequence?: number }
283
+ async function debugLog(runId: string): Promise<SeqEv[]> {
284
+ const res = await http(() => driver.get(`/runs/${enc(runId)}/events/poll?timeout=1&streamMode=debug`));
285
+ const evs = ((res?.json as { events?: SeqEv[] } | null)?.events ?? []).filter((e) => typeof e.sequence === 'number');
286
+ return evs.sort((a, b) => (a.sequence as number) - (b.sequence as number));
287
+ }
288
+ const onGate = (e: SeqEv): boolean => (e.payload?.['nodeId'] ?? e.nodeId) === NODE_ID;
289
+ async function replayGate(): Promise<string | null> {
290
+ const fam = await gateFamily('replay');
291
+ if (!fam) return 'replay family not advertised (gate recorded under openwop.family.replay)';
292
+ const modes = Array.isArray(fam['modes']) ? (fam['modes'] as unknown[]) : [];
293
+ return modes.includes('replay') ? null : `replay.modes ${JSON.stringify(modes)} does not include replay`;
294
+ }
295
+ /**
296
+ * Run → the source's own disposition → a `replay` fork at the gate's recorded
297
+ * `interrupt.resolved`: the prefix holds the gate's request and suspension, and the
298
+ * fork must supply the resolution. A deriving host takes the recorded one (replay.md
299
+ * §Determinism 2); a re-deciding host raises a NEW `interrupt.requested` and waits.
300
+ * Forking at the gate's `node.started` instead would let a host re-emit the recorded
301
+ * request as history, which cannot be told apart from a new one.
302
+ */
303
+ async function replayedRejection(fixture: string, decide: ((runId: string) => Promise<string | null>) | null, settleMs: number): Promise<{ source: SeqEv[]; fromSeq: number; forkId: string; forkStatus: string | null; fork: SeqEv[] } | { reason: string }> {
304
+ const s = await suspended(fixture);
305
+ if ('reason' in s) return s;
306
+ if (decide !== null) { const why = await decide(s.runId); if (why !== null) return { reason: why }; }
307
+ const terminal = await waitStatus(s.runId, TERMINAL, settleMs);
308
+ if (terminal?.['status'] !== 'failed') return { reason: `the source run did not end failed (status ${String(terminal?.['status'])}) — the reject rows own that contract` };
309
+ const source = await debugLog(s.runId);
310
+ const at = source.find((e) => e.type === 'interrupt.resolved' && onGate(e));
311
+ if (at?.sequence === undefined) return { reason: 'the source log has no interrupt.resolved for the gate to fork at' };
312
+ const fork = await http(() => driver.post(`/runs/${enc(s.runId)}:fork`, { mode: 'replay', fromSeq: at.sequence }));
313
+ const forkId = (fork?.json as { runId?: unknown } | null)?.runId;
314
+ if (fork === null || fork.status !== 201 || typeof forkId !== 'string') return { reason: `POST /runs/{runId}:fork answered ${fork?.status ?? 'nothing'} ${readErrorCode(fork?.json) ?? ''} — forkRun owns that contract`.trim() };
315
+ const settled = await waitStatus(forkId, new Set([...TERMINAL, 'waiting-approval']), scaledTimeoutMs(20_000));
316
+ return { source, fromSeq: at.sequence, forkId, forkStatus: settled === null ? null : String(settled['status']), fork: await debugLog(forkId) };
317
+ }
318
+ function assertDerived(r: { source: SeqEv[]; fromSeq: number; forkId: string; forkStatus: string | null; fork: SeqEv[] }, leg: string): void {
319
+ const reRaised = r.fork.filter((e) => (e.sequence as number) >= r.fromSeq && (e.type === 'interrupt.requested' || e.type === 'approval.requested') && onGate(e));
320
+ expect(reRaised.length, req(REPLAY_ID, REPLAY_DOC, `${leg}: the replay fork MUST NOT raise a new interrupt for the gate — the rejection is derived from the recorded interrupt.resolved, never re-decided (status ${String(r.forkStatus)})`)).toBe(0);
321
+ const pick = (e: SeqEv | undefined): Record<string, unknown> => ({ decision: e?.payload?.['decision'], action: e?.payload?.['action'], reason: e?.payload?.['reason'] });
322
+ const srcResolved = r.source.find((e) => e.type === 'interrupt.resolved' && onGate(e));
323
+ const forkResolved = r.fork.find((e) => e.type === 'interrupt.resolved' && onGate(e));
324
+ expect(pick(forkResolved), req(REPLAY_ID, REPLAY_DOC, `${leg}: the fork's interrupt.resolved for the gate MUST carry the source's decision, action and reason`)).toEqual(pick(srcResolved));
325
+ const runFailed = r.fork.find((e) => e.type === 'run.failed');
326
+ const nodeFailed = r.fork.find((e) => e.type === 'node.failed' && onGate(e));
327
+ expect(
328
+ { status: r.forkStatus, code: errorCode(runFailed?.payload?.['error']), failedNodeId: runFailed?.payload?.['failedNodeId'], retryable: (nodeFailed?.payload?.['error'] as { retryable?: unknown } | undefined)?.retryable },
329
+ req(REPLAY_ID, REPLAY_DOC, `${leg}: the fork MUST fail as the source did — failed, run.failed approval_rejected naming the gate, and a non-retryable node.failed`),
330
+ ).toEqual({ status: 'failed', code: CODE, failedNodeId: NODE_ID, retryable: false });
331
+ }
332
+
333
+ it('a replay fork of a decided reject derives the rejection from the log and never re-decides it (leg A)', async () => {
334
+ if (!(await gateFamily('interrupt'))) return softSkip('inapplicable', 'interrupt family not advertised (gate recorded under openwop.family.interrupt)');
335
+ const noReplay = await replayGate();
336
+ if (noReplay !== null) return softSkip('inapplicable', noReplay);
337
+ if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', `fixture ${FIXTURE} is not advertised`);
338
+ const r = await replayedRejection(FIXTURE, async (runId) => {
339
+ const res = await resolve(runId, { action: 'reject' });
340
+ return res === null || res.status < 200 || res.status >= 300 ? `the reject resolve answered ${res?.status ?? 'nothing'} ${readErrorCode(res?.json) ?? ''}` : null;
341
+ }, 10_000);
342
+ if ('reason' in r) return softSkip('blocked', r.reason);
343
+ const again = await resolve(r.forkId, { action: 'accept' });
344
+ assertDerived(r, 'leg A (decided reject)');
345
+ expect({ status: again?.status, code: readErrorCode(again?.json) }, req(REPLAY_ID, 'spec/v2/core/errors.md §One code per state', 'a resolve against the replayed gate MUST answer 409 interrupt_already_resolved — the fork holds no open decision')).toEqual({ status: 409, code: 'interrupt_already_resolved' });
346
+ }, 90_000);
347
+
348
+ it('a replay fork of a timed-out gate derives the timeout rejection from the log and never re-arms it (leg B)', async () => {
349
+ if (!(await gateFamily('interrupt'))) return softSkip('inapplicable', 'interrupt family not advertised (gate recorded under openwop.family.interrupt)');
350
+ const noReplay = await replayGate();
351
+ if (noReplay !== null) return softSkip('inapplicable', noReplay);
352
+ if (!isFixtureAdvertised(TIMEOUT_APPROVE)) return softSkip('inapplicable', `fixture ${TIMEOUT_APPROVE} is not advertised`);
353
+ const r = await replayedRejection(TIMEOUT_APPROVE, null, scaledTimeoutMs(20_000));
354
+ if ('reason' in r) return softSkip('blocked', r.reason);
355
+ assertDerived(r, 'leg B (timeout)');
356
+ // resolvedBy on a timeout row is the host's subject or absent (RFC 0183 §Unresolved 1), so
357
+ // the leg asserts the fork carries the SOURCE's value, not that it is absent.
358
+ const forkResolved = r.fork.find((e) => e.type === 'interrupt.resolved' && onGate(e));
359
+ const srcResolved = r.source.find((e) => e.type === 'interrupt.resolved' && onGate(e));
360
+ expect(
361
+ { action: forkResolved?.payload?.['action'], reason: forkResolved?.payload?.['reason'], resolvedBy: JSON.stringify(forkResolved?.payload?.['resolvedBy'] ?? null) },
362
+ req(REPLAY_ID, REPLAY_DOC, 'leg B: the derived resolution is the recorded timeout — action timeout, reason timeout, and the source\'s resolvedBy (a host subject or absent), never a caller'),
363
+ ).toEqual({ action: 'timeout', reason: 'timeout', resolvedBy: JSON.stringify(srcResolved?.payload?.['resolvedBy'] ?? null) });
364
+ }, 120_000);
262
365
  });
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * `spec/v2/core/replay.md` §Suppression rule 2 — a replay fork resolves a
3
- * side-effecting node's outcome keyed on `(sourceRunId, nodeId, n)`, where `n`
4
- * counts the node's `node.started` events through this execution, including
5
- * retries, later visits and the fork's inherited prefix (openwop#1718, suite
6
- * 2.43.2, target major 2).
3
+ * side-effecting node's outcome keyed on `(sourceRunId, nodeId, n)`, where `n` is
4
+ * one more than the node's recorded terminals (`node.completed`, `node.failed`)
5
+ * before this execution, the fork's inherited prefix included (openwop#1718,
6
+ * re-corrected by openwop#1769 from a count of `node.started`, which a pause or a
7
+ * suspend re-emits; suite 2.43.2, target major 2).
7
8
  *
8
9
  * The rule said `(nodeId, attempt)`. In a run where a node executes more than
9
10
  * once (a loop back over an edge), that key names no single execution: a host
@@ -11,8 +12,8 @@
11
12
  * outcome, and one selecting the latest recorded outcome does the same when
12
13
  * only one exists.
13
14
  *
14
- * Construction (`conformance-replay-ordinal-loop`): start → effect → wait →
15
- * effect, so `effect` executes twice. The suite cancels the source inside
15
+ * Construction (`conformance-replay-ordinal-loop`): start → effect → wait
16
+ * (`core.conformance.hold`, a reserved PURE node that re-executes live) → effect, so `effect` executes twice. The suite cancels the source inside
16
17
  * `wait`, after `effect`'s first execution completed and before its second, and
17
18
  * forks `mode: "replay"` at `effect`'s first `node.started`. In the fork:
18
19
  * - `effect` execution 1 (n = 1) has a recorded outcome and resolves from it;
@@ -74,7 +75,7 @@ describe('v2 replay-suppression-ordinal (replay.md §Suppression rule 2, openwop
74
75
  if (!(await gateFamily('replay'))) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay)');
75
76
  if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', `fixture ${FIXTURE} is not advertised — the host does not run cycles with a side-effecting node`);
76
77
 
77
- const created = await http(() => driver.post('/runs', { workflowId: FIXTURE, inputs: { delayMs: 3000 } }));
78
+ const created = await http(() => driver.post('/runs', { workflowId: FIXTURE, inputs: { delayMs: 3000 }, configurable: { version: 1, run: { recursionLimit: 20 } } }));
78
79
  const runId = (created?.json as { runId?: unknown } | null)?.runId;
79
80
  if (created === null || created.status !== 201 || typeof runId !== 'string') return softSkip('blocked', `POST /runs (${FIXTURE}) answered ${created?.status ?? 'nothing'} ${readErrorCode(created?.json) ?? ''}`.trim());
80
81
 
@@ -453,8 +453,18 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
453
453
  activeServer = receiver;
454
454
 
455
455
  const ownedTenant = await discoverOwnedTenant(driver);
456
+ // The LOOPBACK address, over https so the refusal can only come from the
457
+ // loopback deny list and not from a scheme check. `receiver.url` is the
458
+ // wrong value here: since 2.37.0 `startScopedReceiver` returns the PUBLIC
459
+ // FRONT as `url` whenever OPENWOP_WEBHOOK_RECEIVER_URL is set (the tunnel,
460
+ // plus this exercise's nonce). So this control registered the tunnel, a
461
+ // host with a working guard correctly accepted it, and the control failed
462
+ // on a conformant host. Measured on openwop-app production 2026-09-28: the
463
+ // same host answers `https://127.0.0.1:…` and `https://localhost:…` with
464
+ // 400 webhook_url_rejected ("denied (loopback / link-local / private-IP)").
465
+ const loopback = receiver.localUrl.replace(/^http:/, 'https:');
456
466
  const reg = await driver.post('/v1/webhooks', {
457
- url: receiver.url, // deliberately the raw loopback URL, NOT the tunnel
467
+ url: loopback, // deliberately the raw loopback listener, NOT the tunnel
458
468
  events: ['run.completed'],
459
469
  ...(ownedTenant ? { tenantId: ownedTenant } : {}),
460
470
  });