Skip to content

Durable workflow deploy source for every placement (CL-6581 phase 1) - #289

Merged
TheGreatAxios merged 3 commits into
mainfrom
cl-6581-durable-source
Aug 22, 2026
Merged

Durable workflow deploy source for every placement (CL-6581 phase 1)#289
TheGreatAxios merged 3 commits into
mainfrom
cl-6581-durable-source

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 of CL-6581 (sub-issue: CL-6583). A native workflow's deploy source (an npm name@range pin or a hub-asset commit) is a real external pin that cannot be regenerated, only remembered. Exclusive placement already remembers it in workflow_run_launch_spec (written by vendored workflow-allocation-service.ts, untouched here). Shared placement — the dev default, what workbench seed creates — remembered it nowhere in Postgres, only in the sidecar's local deployment.json. That file being the sole record is why boot replays every record the sidecar happens to have on disk.

This adds @corbits/workflow-deploy-source: a package-owned workflow_deploy_source table that stores the deploy call's WorkflowDefinitionSource verbatim — the same discriminated union POST /workflows/deployments already accepts (kind: "asset" today; kind: "registry" is defined by the union but the route currently 400s it as unsupported) — plus entry/pin/definitionAssetId/deploymentDomain/sourceRef, keyed on anchor run id. Nothing about the record is hub-git-specific: it is {source, entry, pin?} where source names its own origin, so a future source arm needs no schema change, just a new case inside the same jsonb value.

withDeploySourceRecording wraps the session service's deployWorkflowFromSource and deployAdoptedWorkflowFromSource — the shared-capacity deploy entry points POST /workflows/deployments and the routine launcher's adopted-anchor deploy both drive — so every shared deploy records its source after the deploy succeeds, without editing vendored session-service.ts. It's wired at the composition root in apps/hub/src/index.ts, the only place createSessionService is instantiated in the app, so no caller can bypass it.

This is not rebuilding what RepoStore + populateAsset already do

Raised in review and worth stating precisely, since the instinct is understandable: two other places already hold related data, and neither holds what this table holds.

  • RepoStore holds the definition's bytes — the git tree at a commit. It answers "what does the source code look like."
  • workflow_definition_version holds the body projectionwireProjection, grantSnapshot, approvedWireHash — the result of evaluating those bytes. It answers "what did the frozen definition resolve to."
  • Neither holds the deploy call's parameters: entry (which module the sidecar evaluates), pin (the name@range selecting a package member), sourceRef (which ref inside the asset carries the pinned commit), deploymentDomain, and the WorkflowDefinitionSource union itself (which arm — asset vs. registry — and its assetId/format). These are the inputs that select and apply the bytes and body above; they exist today only as the in-memory arguments of one HTTP request, gone the moment that request completes for shared placement. workflow_deploy_source is that missing record — a third, disjoint concern from bytes and body, not a duplicate of either.

scripts/checks/no-product-tenancy.ts's new allowlist entry states this same distinction inline, so the next reader hits it at the point of the violation, not just in this PR description.

Why this shape, not a bigger one

  • vendor/intx/ is never touched. The natural capture point (workflows.ts's route handler) is vendored, so the seam is a decorator around the already-workbench-composed sessionService, mirroring how createLaunchCaches already wraps repoStore/assetService at the same composition root.
  • Registry-sourced deploys: the route itself rejects source.kind === "registry" today (unsupported_source, 400) — there is currently nothing to durably record for that arm. The schema already accommodates it losslessly (the whole union is stored, not just the asset arm), so no follow-up migration is needed when the route grows registry support.
  • No second path: there is exactly one production createSessionService(...) call site, and it's the wrapped one — every shared-placement deploy (route, routine launcher, workbench seed, agent-authored deploy_workflow) funnels through it. Exclusive placement's existing durability is untouched, not duplicated.
  • deployment.json and the boot-restore scan are deliberately untouched — deleting them is phases 3/4 of CL-6581 and depends on this landing first.

What this proves

test/store.test.ts's "a redeploy's full source-call intent survives a fresh connection, with no sidecar record involved" writes a deploy-source record on one Postgres connection, opens an entirely fresh connection and store instance (standing in for a hub restart), and reconstructs the exact DeployWorkflowFromSourceParams shape a redeploy needs from the row alone — proving the record, not any sidecar file, is sufficient to redrive the deploy call. This is the deliberate, non-package-test check the task asked for, scoped to the layer this PR owns (hub-side durability); it does not exercise a live sidecar process, since the dev stack on :3000 was off-limits to restart for this lane.

Test plan

  • bun test in packages/workflow-deploy-source (10 tests, DB-gated tests run against a scratch database derived from local Postgres)
  • bun run typecheck in packages/workflow-deploy-source and apps/hub
  • check:no-product-tenancy, check:licenses pass
  • bunx eslint clean on all changed files
  • bunx prettier --check clean on all changed files
  • Live hub+sidecar restart proof (not run — see note above)

Shared-placement native workflow deploys have no durable hub-side record
of their WorkflowDefinitionSource today -- only the sidecar's local
deployment.json carries it. These tests define the contract for a new
@corbits/workflow-deploy-source package: recording and reading back a
deploy source, surviving a fresh Postgres connection with no sidecar
input (the CL-6581 phase 1 acceptance check), and wrapping a
SessionService so every shared deploy records its source without
touching vendored session-service.ts.
…ment

A native workflow's deploy source (an npm name@range pin or a hub-asset
commit) is a real external pin that cannot be regenerated -- only
remembered. Exclusive placement already remembers it in
workflow_run_launch_spec; shared placement, the dev default, remembered
it nowhere in Postgres, only in the sidecar's local deployment.json,
which is why boot replays every record the sidecar happens to have on
disk.

Add @corbits/workflow-deploy-source: a package-owned
workflow_deploy_source table storing the deploy call's
WorkflowDefinitionSource verbatim (the same discriminated union
POST /workflows/deployments already accepts -- an asset commit today,
a registry name@range or any future source arm with no schema change),
plus entry/pin/definitionAssetId/deploymentDomain/sourceRef, keyed on
anchor run id. withDeploySourceRecording wraps the session service's
deployWorkflowFromSource and deployAdoptedWorkflowFromSource -- the
shared-capacity deploy entry points POST /workflows/deployments and the
routine launcher's adopted-anchor deploy both drive -- so every shared
deploy records its source after the deploy succeeds, without editing
vendored session-service.ts. Wired at the composition root in
apps/hub/src/index.ts, the only place createSessionService is
instantiated, so no caller can bypass it.

Migrations wired into scripts/db-setup.ts alongside every other
installed package.
RepoStore holds the source bytes; workflow_definition_version holds the
body projection (wireProjection, grantSnapshot, approvedWireHash).
Neither stores the deploy PARAMETERS -- entry, pin, sourceRef,
deploymentDomain, and the WorkflowDefinitionSource union itself -- that
select and apply those bytes. workflow_deploy_source is that missing
record, not a duplicate of either.
@TheGreatAxios
TheGreatAxios force-pushed the cl-6581-durable-source branch from 235e80f to b04373d Compare August 22, 2026 02:31
@TheGreatAxios
TheGreatAxios merged commit abd51b1 into main Aug 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant