You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Cross-deployment start() stamps the starter's spec version, not the executing deployment's #4251
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-log noop, slot identity, and now the hook_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, ... }) in start.ts; the response carries specVersion and workflowCoreVersion, parsed in runtime/helpers.ts). Use the probed specVersion (capped at what this SDK and the World can mint) instead of world.specVersion when deploymentId !== 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 treating run_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().
Problem
A run's
specVersionis chosen instart()(packages/core/src/runtime/start.ts:const specVersion = opts.specVersion ?? world.specVersion) and written onrun_createdand the queue message. That is the spec version of the SDK callingstart().When
start()is called without adeploymentId, 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: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.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.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 onspecVersion >= 8(#4193). An intermediate revision replaced that gate with a per-runexecutionContext.hookForceClaimReaderVersionattestation stamped from the probe (same pattern ashookResumeInputVersion); it was reverted to keep a single versioning mechanism and instead fix the stamping here for all gates at once. The reverted commit (0feb2fdon thehook-force-claimbranch) shows how the probe value can be threaded intostart().🤖 Generated with Claude Code