Tracks the remaining work from #877, whose layer 1 shipped in #880.
Why this exists separately
#877 scoped four steps and was auto-closed by #880's merge, which delivered only step 2. Rather than reopen it with one step struck through, this epic carries the unshipped remainder. #877 stays closed and accurate: the shared archive engine exists.
What already landed (#880):
packages/server/src/lib/resourceVersions.ts — the shared archive: makeVersionStore (write side) + makeVersionArchive (list/get/restore), with per-resource adapters for the config projection, applyConfig, mapVersion, and loadResource.
- Agents and guardrails migrated onto it, behaviour-preserving for agents; guardrails levelled up to the same surface (
id, label, created_by, list + restore, no-op dedup).
- Layer 2's pure helpers extracted to
releaseAssignment.ts (bucketForKey, assignReleaseVersion, parseActiveRelease).
What landed for step 1 (#892, closing #872):
OrchestrationVersion as the third adapter on the shared engine — orchestrationVersionSnapshot.ts (projection + write side) and orchestrationVersions.ts (archive adapters, list/get/restore).
orchestrationRunGraph.ts — a single resolution seam. All four entry points (driveQueuedRun, wakeRun, resumeOrchestrationRunExecution, redriveRun) previously did orch.nodes as OrchestrationNode[] independently, so fixing three of them would have looked correct in review and left the bug in the path a run actually parks in.
OrchestrationRun.orchestrationVersion records the pin, stamped at start-orchestration-run and exposed as orchestration_version.
- Three IAM actions + three REST routes; MCP derives its tools from the spec automatically.
Agents, guardrails and now orchestrations are the three worked examples for everything below. Orchestrations is the closest model for #882: it is the first adapter where the consumption moment is not per-request.
Steps
What #892 makes cheaper for #882
The two are the same bug class, and #892 settled the design questions #882 would otherwise have to re-litigate:
| Decision |
Answer, and why it should carry over |
| Store the pin as a version number or an FK to the version table? |
Number. It is what the wire exposes and what an audit reader cites, it matches Generation.agentVersion, and the archive row is reachable with no join. |
Is name part of the versioned config? |
No, metadata. Bumping a version on a rename makes two version numbers denote the same state machine — the one thing the number must not do. |
| Guard edits while work is in flight? |
No. Pinning is the complete fix; a guard would remove the capability the design keeps, where the live columns become a draft for work started from now on. |
| Runs/tasks that predate the pin? |
Null pin → live definition, logged. Refusing to drive them would strand everything in flight across the deploy. |
| One resolution seam, or resolve at each read site? |
One seam. #882 has three read sites; the same "fix two of three and it still looks right" hazard applies. |
The one place #882 differs: a workflow's consumption moment is task creation, not per-request, and #877's own table notes a workflow release would mean "the version new tasks are created into" with no mid-life reassignment.
A sequencing decision worth recording
#877 listed as its step 1 a stopgap for #872: a run-level JSONB snapshot on OrchestrationRun, justified as "small, ships fast, no new tables". That justification no longer held — it existed because the shared engine didn't. Building the stopgap would have been write-then-replace work, since #877's own step 3 already says the snapshot "becomes a version reference".
Recommendation was to skip the stopgap and take #872 straight onto OrchestrationVersion. That is what #892 did, and the same reasoning applies to #882: there is no stopgap worth building for it either.
Operational follow-ups
None of these is repo work — they are per-deployment calls and release bookkeeping, listed here so they are not lost.
Related: #877, #880, #892
Tracks the remaining work from #877, whose layer 1 shipped in #880.
Why this exists separately
#877 scoped four steps and was auto-closed by #880's merge, which delivered only step 2. Rather than reopen it with one step struck through, this epic carries the unshipped remainder. #877 stays closed and accurate: the shared archive engine exists.
What already landed (#880):
packages/server/src/lib/resourceVersions.ts— the shared archive:makeVersionStore(write side) +makeVersionArchive(list/get/restore), with per-resource adapters for the config projection,applyConfig,mapVersion, andloadResource.id,label,created_by, list + restore, no-op dedup).releaseAssignment.ts(bucketForKey,assignReleaseVersion,parseActiveRelease).What landed for step 1 (#892, closing #872):
OrchestrationVersionas the third adapter on the shared engine —orchestrationVersionSnapshot.ts(projection + write side) andorchestrationVersions.ts(archive adapters, list/get/restore).orchestrationRunGraph.ts— a single resolution seam. All four entry points (driveQueuedRun,wakeRun,resumeOrchestrationRunExecution,redriveRun) previously didorch.nodes as OrchestrationNode[]independently, so fixing three of them would have looked correct in review and left the bug in the path a run actually parks in.OrchestrationRun.orchestrationVersionrecords the pin, stamped atstart-orchestration-runand exposed asorchestration_version.Agents, guardrails and now orchestrations are the three worked examples for everything below. Orchestrations is the closest model for #882: it is the first adapter where the consumption moment is not per-request.
Steps
WorkflowVersionand pin at task creation; resolvetasksTransition.ts,tasksApprovalGate.tsandtasks.tsthrough it.What #892 makes cheaper for #882
The two are the same bug class, and #892 settled the design questions #882 would otherwise have to re-litigate:
Generation.agentVersion, and the archive row is reachable with no join.namepart of the versioned config?The one place #882 differs: a workflow's consumption moment is task creation, not per-request, and #877's own table notes a workflow release would mean "the version new tasks are created into" with no mid-life reassignment.
A sequencing decision worth recording
#877 listed as its step 1 a stopgap for #872: a run-level JSONB snapshot on
OrchestrationRun, justified as "small, ships fast, no new tables". That justification no longer held — it existed because the shared engine didn't. Building the stopgap would have been write-then-replace work, since #877's own step 3 already says the snapshot "becomes a version reference".Recommendation was to skip the stopgap and take #872 straight onto
OrchestrationVersion. That is what #892 did, and the same reasoning applies to #882: there is no stopgap worth building for it either.Operational follow-ups
None of these is repo work — they are per-deployment calls and release bookkeeping, listed here so they are not lost.
guardrail_versions.document→configand rewrapped it as{ document }. The repo has no migration mechanism — schema is managed bysync --alter— so existing archived guardrail documents are not carried across. New versions archive correctly from the first write. Where that history matters:BREAKING CHANGE:footer (theGuardrailVersionresponse moveddocument→config.document). Packages are at0.19.2, and tools differ on whether a breaking change at0.xbumps to1.0.0or0.20.0. Confirm withpnpm lerna version --dry-runrather than assuming —.claude/rules/release.mddoes not cover the0.xcase. fix(orchestrations): pin a run to the graph it started on #892 is additive (fix:, no breaking footer), so it does not change this question — but it is unreleased alongside feat(server): extract a shared resource-versioning engine #880, so both land in the same release.orchestration_versionsis a new table andorchestrations.version/orchestration_runs.orchestration_versionare new columns, all created bysync --alter. Existing orchestrations getversion = 1with no archived row and existing runs get a null pin — together exactly the documented degradation path, so in-flight runs continue on the live graph and the first graph edit after deploy archives normally. No index was renamed or removed. Recorded here so nobody goes looking for a migration.Related: #877, #880, #892