Repository navigation
Description
Problem
A run's specVersion is chosen in start() (packages/core/src/runtime/start.ts: const specVersion = opts.specVersion ?? world.specVersion) and written on run_created and the queue message. That is the spec version of the SDK calling start().
When start() is called without a deploymentId, the run executes on the same deployment, so the caller's version is the executor's version and everything is consistent.
When start({ deploymentId }) targets another deployment, the run is stamped with the caller's spec version even though a different deployment — possibly on an older or newer @workflow/core — will read and write its event log. Every consumer of the spec version then reasons about the wrong party:
- Reader gates that decide "does this run's runtime understand row X?" from
run.specVersion(sealed-lognoop, slot identity, and now thehook_disposed{forceClaimedBy}victim gate from feat(hooks):createHook({ experimental_force: true })takes a held token over #4193 / vercel/workflow-server#980) can be wrong for cross-deployment runs: a new starter stamps ≥ N onto a run executed by an older deployment that does not read N. - Writer behaviour keyed on the stamped version (compression ≥ 5, CBOR queue transport, attributes) is chosen by the starter but has to be honoured by the executor.
Deployments are pinned on Vercel, so there is no rolling-version window within one deployment; the discrepancy exists specifically between two different deployments.
Proposed fix
The executing deployment should determine the run's spec version. Options, roughly in order of preference:
start()already probes the target deployment via the health check for a cross-deployment start (healthCheck(world, { deploymentId, ... })instart.ts; the response carriesspecVersionandworkflowCoreVersion, parsed inruntime/helpers.ts). Use the probedspecVersion(capped at what this SDK and the World can mint) instead ofworld.specVersionwhendeploymentId !== currentDeploymentId. Same-deployment starts are unchanged. If the probe fails or the target predates a versioned health response, fall back to the lowest version both sides are known to support rather than the caller's current version.- Alternatively let the target's
run_started(written by the executor) settle/override the version, with Worlds treatingrun_created's version as provisional until then. Larger change; touches every World.
Either way, add mixed-version tests: new starter → older executor and older starter → newer executor, for the sealed-log and hook-force-claim reader gates.
Context
Surfaced while gating createHook({ experimental_force }) victims on specVersion >= 8 (#4193). An intermediate revision replaced that gate with a per-run executionContext.hookForceClaimReaderVersion attestation stamped from the probe (same pattern as hookResumeInputVersion); it was reverted to keep a single versioning mechanism and instead fix the stamping here for all gates at once. The reverted commit (0feb2fd on the hook-force-claim branch) shows how the probe value can be threaded into start().
🤖 Generated with Claude Code
Activity