Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 45 additions & 37 deletions VENDORED.md

Large diffs are not rendered by default.

195 changes: 183 additions & 12 deletions docs/revendor-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,9 @@ non-fatal to the section. Covered by
`vendor/intx/workflow/src/runtime/run.test.ts` — the intended home
`runlocal/run-local.ts:8` already names for this layer's coverage.

**This delta targets the current pin** (`59f5e7b9`). Upstream's `486a6b1b`
changed `run.ts` after this pin; the delta above re-diffs against the
re-pinned tree once PR #59 lands.
**Re-applied at `4ed8baf4`.** Upstream's `486a6b1b` reshaped `run.ts` across
the re-pin; the delta re-diffed onto the new tree without conflict and its
four tests stay green.

**Retire when** upstream ships a non-fatal body-failure edge on `onTrigger`
(or the whole source-format workflow line reaches a published `@intx/*` npm
Expand Down Expand Up @@ -357,15 +357,15 @@ see below), but the two adapters that wiring needs now exist.

- `vendor/intx/workflow-host/src/adapters/{action-invoker,effect-ledger,run-blobs}.ts`
(+ their tests), copied from gtm-workbench's `packages/workflow-host/src/
adapters/`. Important provenance note carried over from the port scope
adapters/`. Important provenance note carried over from the port scope
report: **these three files are gtm-OWNED, not upstream Interchange.**
Upstream `faremeter/interchange`'s own `packages/workflow-host/src/
adapters/` (at the pinned `59f5e7b9`) has none of them — only blob-
adapters/` (at the pinned `59f5e7b9`) has none of them — only blob-
substrate/repo-store/spawn-child/step-invoker. So there is no upstream
publish this sub-delta tracks toward; it is copy-with-attribution from a
sibling internal fork, not a normal "vendored until the next npm publish"
entry. The ordinary `VENDORED.md` kill date on the `vendor/intx/
workflow-host` row does not really apply to this slice — treat it as
workflow-host` row does not really apply to this slice — treat it as
retired only once workbench builds its own equivalent inline (dropping the
gtm-origin copy) or gtm's fork itself gets folded into a real publish,
whichever happens first.
Expand All @@ -388,12 +388,8 @@ see below), but the two adapters that wiring needs now exist.

**What did NOT land (deliberately, time-boxed):** the run-child wiring
(`resolveActionHandler` bindings field, `effects`/`invokeAction` construction
in `buildRuntimeEnv`, `loopFns`) is a separate final commit on this branch,
explicitly marked in its message as needing re-application after the
in-flight `vendor/intx` re-pin lands (a sibling change was re-pinning
concurrently with this port) — `buildRuntimeEnv` is exactly the kind of
generated/copied vendor surface a re-pin silently regenerates, so the wiring
diff would otherwise be dropped on the floor. The app-seam wiring in
in `buildRuntimeEnv`, `loopFns`) landed as a separate commit and has since
been re-applied onto the `4ed8baf4` tree unchanged. The app-seam wiring in
`apps/sidecar/src/workflow-substrate-factory/index.ts` (building the registry
and passing it as `resolveActionHandler`) is likewise deferred to that same
follow-up, since it depends on the run-child binding field existing first.
Expand All @@ -402,3 +398,178 @@ follow-up, since it depends on the run-child binding field existing first.
src/adapters/blob-substrate.ts` already has inline (private `writeBlob`/
`readBlob` helpers) rather than reconciling the two into one shared helper —
left as a known follow-up per the port scope report, not a blocker.

## CL-6324 re-pin: `59f5e7b9` → `4ed8baf4` (the workflow.json retirement)

The vendored trees are re-copied at upstream `main` tip `4ed8baf4`
(2026-08-19, 45 commits on). `VENDORED.md` is the pin of record. This
section is the map of what the bump costs on the workbench side, because
the app-side conversion does **not** land with it.

### What landed cleanly

- All 21 `vendor/intx/*` rows re-copied. Only nine trees actually changed
(`hub-sessions`, `workflow-host`, `workflow`, `workflow-deploy`, `types`,
`hub-api`, `db`, `agent`, `hub-agent`); the other twelve are byte-identical
at both commits and were re-pinned so the ledger records one commit rather
than a mix (this also collapses `inference-catalog`'s separate `5d2aa94a`
pin).
- Every workbench-local delta re-applied unchanged — upstream subsumed none
of them, and none of the five files they touch was modified upstream in the
45 commits: the `inference.usage` forward, `ownsWorkflowRunRepo`,
`hasConversationText`, and the `needs-you` approval-route carve-out. Their
tests pass.
- `packages/folded-runs`' `wrapHarnessAsSingleStepWorkflow` call moved onto
`buildSingleStepAgentDefinition`, which survives the deletion.

### What the bump breaks, and why it is one migration

Upstream retired the on-disk `workflow.json`. A deployed workflow's
definition is no longer serialized into the deploy tree and re-read by the
sidecar; it is evaluated from the deployment's own **source closure** and
re-verified in-child against the approved wire hash. Source-ref is now the
only deploy lineage, and the live-authored and instance chains are deleted:
`createWorkflowDeployOrchestrator`, `SessionService.deploySingleStepAtHead`,
`SessionService.deployInstanceAtHead`, `wrapHarnessAsSingleStepWorkflow`,
`createWorkflowSpawnChild`, `createWorkflowSpawnSuspendableChild`,
`loadVerifiedWorkflowDefinition`, and the `definition` field on the deploy
frame. `SpawnTimeEnv` drops `referencedDefinitionHashes` and gains
`closurePackageDir`; `RunWorkflowChildBindings` drops
`workflowDefinitionRepoId`.

Workbench has no code-sourced deploy front. Every run it launches — chat,
tasks, routines, agent lifecycle — goes through `packages/folded-runs`'
`deployAtHead`, which synthesizes a single-step definition in memory from a
system prompt plus tool-package pins and hands it to `deploySingleStepAtHead`.
The new front (`deployWorkflowFromSource` / `installAndApproveWorkflowSource`
/ `deployPreparedCodeSourcedWorkflow`) takes a registry `name@range` pin or
an asset tarball and resolves a dependency closure from it. Converting means
giving a folded run a real source package, not renaming a call.

That is why the remaining breakage cannot be split by tree:
`hub-sessions` (deploy front), `workflow-deploy` (orchestrator), `types`
(deploy frame), `workflow-host` (child definition load), `db` (frozen
approval bundle, migrations 0082/0083) and `hub-api` (run trigger) all move
together, and `apps/sidecar` reads the frame both sides write. Leaving any
one on the old pin leaves the frame contract split down the middle.

Open conversion sites, all blocked on that one decision:

| Site | What it needs |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `packages/folded-runs/src/launch.ts` (`deployAtHead`), `wake.ts` | A code-sourced deploy for the folded single-step run — the root blocker. |
| `apps/sidecar/src/workflow-host-wiring/index.ts`, `asset-materialization.ts` | Stop writing `workflow.json` and stop reading `projection.definition`; stage the closure instead. |
| `apps/sidecar/src/workflow-substrate-factory/index.ts`, `child-runtime.ts`, `config.ts` | Drop `WORKFLOW_DEFINITION_REPO_ID`/`_REF`; in-memory child spawn; `closurePackageDir` plumbing. |
| `apps/sidecar/src/workflow-deployment-record.ts` | Drop `referencedDefinitionHashes`; carry the grant-walk snapshot. |

Upstream's own diff over the same span is the reference implementation:
`apps/sidecar/src/workflow-substrate-factory.ts` and
`workflow-host-wiring.ts` at `4ed8baf4` show every one of these conversions
against the same contracts, and `apps/sidecar`'s `VENDORED.md` row stays at
`59f5e7b9` until workbench's fork is reconciled with them.

### Why the conversion is one chain, not four independent sites

Re-basing the re-pin onto post-merge `main` (133 commits, including the
`#58` workflow-host adapters and the `#68` `onTrigger` edge, both re-applied
onto the re-vendored trees and green) leaves the four sites above still red,
and they cannot be taken in any other order than this one:

1. **`packages/folded-runs`' `deployAtHead` must become a code-sourced
deploy.** `deployWorkflowFromSource` /
`deployPreparedCodeSourcedWorkflow` take a `WorkflowDefinitionSource`
(`vendor/intx/types/src/workflow-sources.ts`) — an npm `registry` pin, an
asset `tarball` selected by a `name@range` pin, or an asset `source` tree
at a `commitSha`. Workbench has no caller of any of them today: grepping
`apps/`, `packages/`, `workflows/`, and `scripts/` for
`deployWorkflowFromSource` returns nothing. So this is not a call-site
rename — it is a new deploy pipeline (mint the `workflow`-kind definition
asset, seed the source asset's tarball, configure
`toolPackageRegistries`, supply the attachment resolver and asset
service).
2. **A folded run's definition is per-run, a source package is not.** The
synthesized definition varies by `systemPrompt`, `trigger.to`
(the run's own mail address), resolved inference sources, and the
`credentialBindings` folded in for `@corbits/mcp-tools`. A seeded tarball
is static, so the varying half has to move out of the definition body and
into deploy-time config the entry module reads. That is a design change
to the folded-run contract, not a mechanical port.
3. **Only then can the sidecar convert.** `SpawnTimeEnv` drops
`referencedDefinitionHashes` and gains `closurePackageDir`
(`vendor/intx/workflow-host/src/child/env-bootstrap.ts:126`), and
`createInMemorySpawnChild` / `createInMemorySpawnSuspendableChild`
(`adapters/spawn-child.ts:115,220`) take a `bodies: ReadonlyMap<string,
WorkflowDefinition>` lifted from the re-evaluated closure — not a deploy
ref. There is no closure dir to stage and no bodies map to lift until a
deploy actually ships a source closure, so swapping the spawners ahead of
step 1 could only be done by re-deriving the bodies from the retired
in-memory definition: precisely the fallback shim this conversion is not
allowed to build.

The sidecar's three remaining red suites
(`test/workflow-substrate-factory-suspendable-child.test.ts` and siblings,
failing on the deleted `createWorkflowSpawnChild` export) are the visible
tail of that same chain.

### Section deploys on the new rails (CL-6329's prerequisite)

Phase 1.3's turn=run swap needs the converted front to deploy an `onTrigger`
SECTION definition carrying `referencedDefinitions`, not just the single-step
folded agent. Checked against the re-vendored trees, and the answer is that
the new front handles this **natively and unconditionally** — it is strictly
more natural here than on the retired path:

- `deployCodeSourcedWorkflow`
(`vendor/intx/hub-sessions/src/session-service.ts:745`) calls
`enumerateInertOnTriggerBodies(projection)` on **every** code-sourced
deploy. It resolves each body step's inference source through the same
`pickStepInferenceSource` + operator-approval gate the top-level steps use,
recomputes each body's wire hash from the frozen inert body, and ships the
set as `referencedDefinitions` on the deploy frame.
- There is no caller opt-in and no second entry point. A definition with no
inline section simply enumerates zero bodies and the field is omitted.

**So the launch API needs no single-step/section branch.** Which shape gets
deployed is entirely a property of what the source package's
`interchange.workflow` entry module evaluates to. One front, one set of
parameters (source, entry, pin, `definitionAssetId`, config); the section-ness
is downstream of it. A converted `deployAtHead` that exposes a
"deploy a section" flag would be modelling a distinction the platform does
not have.

Two corrections to the seams as cited from the old pin:

- `vendor/intx/workflow-deploy/src/orchestrator.ts:1020` no longer exists —
the re-pin cut that file from 1094 lines to 319 (`createWorkflowDeployOrchestrator`
is deleted). Body enumeration now lives in `hub-sessions`' code-sourced
deploy, and the pure structural half in
`workflow-deploy/src/inert-ontrigger-bodies.ts`.
- `vendor/intx/workflow/src/ontrigger-bodies.ts:43` (`onTriggerBodyRef`) is
intact and is now the single owner of the `<workflowId>__<stepId>` scheme,
shared by the hub's inert enumerator and the in-child rewrite.

#### `onBodyFailure` survives the new rails, but only because of where it is read

The CL-6326 delta adds `onBodyFailure` to the LIVE `onTrigger` primitive and
reads it live in `runtime/run.ts`. The inert projector does **not** carry it:
`projectOnTrigger` (`vendor/intx/workflow/src/live-inert-projector.ts:376`)
is an explicit field whitelist — `kind`, `id`, `on`, `body`, `drainBehavior`,
`after` — and `InertOnTrigger` has no such field.

On the code-sourced rails this is harmless, and in fact load-bearing in our
favour. The child EVALUATES the pinned closure to a live definition and runs
the runtime against that (`run-child.ts:645-649`); the inert projection is
only the approval/hash surface. So:

- `onBodyFailure` reaches the runtime, because the entry module authored it
and the child re-evaluates the entry module.
- `onBodyFailure` is invisible to `computeLiveDefinitionHash`, because the
hash is taken over the projection that drops it — so the field cannot
cause a re-verify divergence.

The consequence worth stating plainly: the delta is **only** sound on a path
where the executing definition is re-evaluated from source. Anything that
ships the projection as the executable definition would silently drop the
policy and take the default `"end"` — a section that dies on its first failed
turn, with no error. That is one more reason the conversion cannot be
half-taken.
24 changes: 16 additions & 8 deletions packages/folded-runs/src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { generateId } from "@intx/hub-common";
import { InferenceSource } from "@intx/types/runtime";
import type { WireGrantRule } from "@intx/types/grant-wire";
import {
wrapHarnessAsSingleStepWorkflow,
buildSingleStepAgentDefinition,
type FoldedBody,
} from "@intx/workflow-deploy";
import { defineWorkflow, step, type Selector } from "@intx/workflow";
Expand Down Expand Up @@ -279,15 +279,23 @@ export async function deployAtHead(
};
const deployContent = { systemPrompt: params.foldedBody.systemPrompt };
// A folded run is a conversation: its one step must service every
// inbound mail as another turn, never complete after the first. The
// platform's `deployInstanceAtHead` wraps the agent as a step with the
// default trigger budget of 1 (batch), which is exactly what made every
// chat go silent after its first real reply — so the folded launch
// builds the same single-step workflow itself, with the budget
// declared, and deploys it through the same head deploy.
// inbound mail as another turn, never complete after the first. A wrap
// with the platform's default trigger budget of 1 (batch) is exactly what
// made every chat go silent after its first real reply — so the folded
// launch builds the single-step agent itself, with the budget declared,
// and deploys it through the same head deploy. The launch pins its tools
// as packages rather than factories, so the step agent carries none.
const foldedSteps = {
[FOLDED_STEP_ID]: step({
agent: wrapHarnessAsSingleStepWorkflow({ config, deployContent }),
agent: buildSingleStepAgentDefinition({
id: config.agentId,
systemPrompt: deployContent.systemPrompt,
inferencePreferences: config.sources.map((source) => ({
provider: source.provider,
model: source.model,
})),
toolFactories: [],
}),
triggers: "unbounded",
...(params.stepInput !== undefined ? { input: params.stepInput } : {}),
}),
Expand Down
Loading
Loading