From 7110d93a804b82ef2c7ef82849658c444e43b535 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 28 Aug 2026 05:10:35 -0700 Subject: [PATCH 1/2] Re-vendor @intx/workflow and @intx/workflow-deploy at a8bc06ae with no delta Upstream b977ade6 ships the onTrigger body-failure policy workbench had vendored as onBodyFailure: "continue" (CL-6326/CL-6324) under the literal "tolerate", with the same semantics: a failed body re-arms the section, a cancelled body always ends it, and the field is omitted from the wire hash when default. The vendored @intx/workflow therefore carries no local change; the section author (@corbits/agent-runtime) and every test and comment now say "tolerate", and @intx/db migration 0088 rewrites the retired literal inside stored wire projections (no hub-side reader acts on the field; the sidecar re-evaluates the source closure). @intx/workflow-deploy carries no delta of its own either. It stays vendored, at the same commit, because the re-vendored hub-sessions imports inertLoopBody (upstream 1ea2f39b), which npm 0.3.0 predates; its row now says exactly that. Root overrides point every vendored @intx name at workspace:* so published packages resolve the vendored copies rather than a second npm copy. --- VENDORED.md | 55 ++- bun.lock | 32 +- docs/CHAT.md | 2 +- package.json | 12 +- packages/agent-runtime/src/definition.test.ts | 4 +- packages/agent-runtime/src/definition.ts | 8 +- packages/chat/src/standalone-launch.ts | 2 +- packages/chat/test/platform-adapter.test.ts | 4 +- packages/folded-runs/test/definition.test.ts | 2 +- scripts/checks/kill-dates.txt | 10 +- scripts/e2e/cl-6324-launch-proof.ts | 4 +- scripts/e2e/cl-6329-turn-swap-proof.ts | 2 +- vendor/intx/db/VENDORED-FROM | 2 +- ...finition_version_tolerate_body_failure.sql | 9 + vendor/intx/db/migrations/meta/_journal.json | 7 + vendor/intx/db/src/migrations-journal.test.ts | 12 +- vendor/intx/db/src/tolerate-migration.test.ts | 77 ++++ vendor/intx/hub-sessions/package.json | 2 +- vendor/intx/workflow-deploy/VENDORED-FROM | 4 +- vendor/intx/workflow-deploy/package.json | 4 +- vendor/intx/workflow-deploy/src/index.ts | 1 + .../src/inert-ontrigger-bodies.ts | 24 ++ .../intx/workflow-deploy/src/orchestrator.ts | 77 ++-- vendor/intx/workflow-deploy/tsconfig.json | 8 +- vendor/intx/workflow/README.md | 26 ++ vendor/intx/workflow/VENDORED-FROM | 4 +- vendor/intx/workflow/package.json | 9 +- .../workflow/src/definition/primitives.ts | 43 ++- .../intx/workflow/src/definition/workflow.ts | 3 +- vendor/intx/workflow/src/index.ts | 2 + .../workflow/src/live-inert-projector.test.ts | 51 --- vendor/intx/workflow/src/runlocal/index.ts | 2 + .../workflow/src/runlocal/loop-iteration.ts | 32 +- .../intx/workflow/src/runlocal/run-local.ts | 4 +- vendor/intx/workflow/src/runtime/run.test.ts | 334 ------------------ vendor/intx/workflow/src/runtime/run.ts | 74 ++-- vendor/intx/workflow/tsconfig.json | 10 +- 37 files changed, 381 insertions(+), 577 deletions(-) create mode 100644 vendor/intx/db/migrations/0088_workflow_definition_version_tolerate_body_failure.sql create mode 100644 vendor/intx/db/src/tolerate-migration.test.ts delete mode 100644 vendor/intx/workflow/src/live-inert-projector.test.ts delete mode 100644 vendor/intx/workflow/src/runtime/run.test.ts diff --git a/VENDORED.md b/VENDORED.md index 66c153d71..5203d117f 100644 --- a/VENDORED.md +++ b/VENDORED.md @@ -22,21 +22,21 @@ never a convenience. ## Ledger -| Vendored path | What was copied | Upstream repo @ commit | Why not a published package | Owner | Kill date | Kill-date test | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- | ----------------- | -| `apps/sidecar` | Derived from upstream's own `apps/sidecar`: of 38 tracked `src/` modules, 5 are byte-identical to upstream (`default-harness.ts`, `source-asset-delivery.ts`, `workflow-closure-apply.ts`, `workflow-probe-handler.ts`, `workflow-run-pack-restore.ts`), 10 are substantially rewritten under the same name (`atomic-write.ts`, `config.ts`, `conversation-state.ts`, `index.ts`, `run-grants.ts`, `signing-keypair.ts`, `step-agent-tools.ts`, `tool-materialization.ts`, `workflow-closure-materialization.ts`, `workflow-run-pack-client.ts`), and the remaining 23 are workbench-only, including the `workflow-host-wiring/` and `workflow-substrate-factory/` module splits of upstream's single-file `workflow-host-wiring.ts` and `workflow-substrate-factory.ts`. A living fork, not a frozen copy, so this row carries no tree hash. | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | An app is never npm-published, so no publish can cover the execution host; retired by consuming an upstream-published host, or by renewing this row deliberately | sawyer | 2026-09-19 | `check:killdates` | -| `vendor/intx/agent` | `@intx/agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the operator-configurable doom-loop threshold (`afd0c82b`, `c421c092`) the re-vendored `workflow-host` configures; no local delta; retired by the next `@intx/agent` publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/db` | `@intx/db` source (`src/`, `migrations/`, drizzle config, manifest, tsconfigs) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the `wire_projection` column/loader delta (CL-6324) or the `workflow_definition.origin` column separating a definition from the per-run record of one folded run's deploy (CL-6452), shipped as migrations `0086`/`0087` behind upstream's `0085_add_approval_run_idx`; retired when upstream absorbs the deltas | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the exported null-principal `resolveApproval` (CL-6345) or the bearer-authenticated workflow-deploy mirror (`middleware/workflow-run-deploy-auth.ts`, CL-workflow-deploy-bearer); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` | -| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the usage forward (CL-5879), pack-acceptance fixes, adopted deploy front, wire-projection writer, event-collector serialization, anchor ordering, or malformed tool-call-name sanitization (CL-6478) | sawyer | 2026-09-19 | `check:killdates` | -| `vendor/intx/inference` | `@intx/inference` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates doom-loop detection (`8da4c827`, `afd0c82b`, `c421c092`); one local delta: `providers/google-genai-files.ts` builds its upload body as `new Uint8Array(bytes)` because TS 6's lib.dom `BodyInit` rejects `Uint8Array` (upstream compiles ESNext-only under TS 5.9); retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/mail-memory` | `@intx/mail-memory` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the `@intx/mailbox` extraction (`af03bb90`), on-demand body reads (`54f7c239`) and `expunge` returning the swept uids (`bcabb1f8`) that the re-vendored `workflow-host` binds against; no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/mailbox` | `@intx/mailbox` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | Never published: a new package at the target pin (`af03bb90`) that `workflow-host`'s substrate mailbox store and supervisor-backed transport import; no local delta; retired by its first publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/mime` | `@intx/mime` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the non-RFC message-id guard `isMessageId` (`d97e1832`), the full `References` chain (`65c6fe70`) and the lossless `decodeMail` decoder (`3b6d06b2`) that `mailbox`/`mail-memory` at the same pin import; no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/types` | `@intx/types` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the type surface the re-vendored trees compile against: `expunge` returning `expungedUids` (`bcabb1f8`), plain-string `PackRejectReason` (`7b42f405`), the run authorization/approvals REST types (`71ad6c08`), the decoded-mail `Mail`/`MailPartReader` model (`3b6d06b2`) and the `interchange.actions`/`loops` package-json refs (`3bd5b837`, `1ea2f39b`); no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/workflow` | `@intx/workflow` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the `onBodyFailure` trigger policy and its projection (CL-6326, CL-6324); retired when upstream absorbs the delta | sawyer | 2026-09-19 | `check:killdates` | -| `vendor/intx/workflow-deploy` | `@intx/workflow-deploy` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | Carries no delta of its own, but must bind against the vendored `@intx/workflow` (whose `onBodyFailure` field flows through the projection it hashes); retired with the workflow delta | sawyer | 2026-09-19 | `check:killdates` | -| `vendor/intx/workflow-host` | `@intx/workflow-host` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the empty-mail drop (CL-6164), the action/loop runtime bind (CL-6325; its adapters live in `packages/workflow-host-actions` since CL-6435), or the body-spawn authorize/credential threading (CL-6448); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` | +| Vendored path | What was copied | Upstream repo @ commit | Why not a published package | Owner | Kill date | Kill-date test | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- | ----------------- | +| `apps/sidecar` | Derived from upstream's own `apps/sidecar`: of 38 tracked `src/` modules, 5 are byte-identical to upstream (`default-harness.ts`, `source-asset-delivery.ts`, `workflow-closure-apply.ts`, `workflow-probe-handler.ts`, `workflow-run-pack-restore.ts`), 10 are substantially rewritten under the same name (`atomic-write.ts`, `config.ts`, `conversation-state.ts`, `index.ts`, `run-grants.ts`, `signing-keypair.ts`, `step-agent-tools.ts`, `tool-materialization.ts`, `workflow-closure-materialization.ts`, `workflow-run-pack-client.ts`), and the remaining 23 are workbench-only, including the `workflow-host-wiring/` and `workflow-substrate-factory/` module splits of upstream's single-file `workflow-host-wiring.ts` and `workflow-substrate-factory.ts`. A living fork, not a frozen copy, so this row carries no tree hash. | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | An app is never npm-published, so no publish can cover the execution host; retired by consuming an upstream-published host, or by renewing this row deliberately | sawyer | 2026-09-19 | `check:killdates` | +| `vendor/intx/agent` | `@intx/agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the operator-configurable doom-loop threshold (`afd0c82b`, `c421c092`) the re-vendored `workflow-host` configures; no local delta; retired by the next `@intx/agent` publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/db` | `@intx/db` source (`src/`, `migrations/`, drizzle config, manifest, tsconfigs) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the `wire_projection` column/loader delta (CL-6324) or the `workflow_definition.origin` column separating a definition from the per-run record of one folded run's deploy (CL-6452), shipped as migrations `0086`/`0087` behind upstream's `0085_add_approval_run_idx`, plus `0088` rewriting the retired `onBodyFailure: "continue"` literal to upstream's `"tolerate"` in stored wire projections; retired when upstream absorbs the deltas | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the exported null-principal `resolveApproval` (CL-6345) or the bearer-authenticated workflow-deploy mirror (`middleware/workflow-run-deploy-auth.ts`, CL-workflow-deploy-bearer); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` | +| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the usage forward (CL-5879), pack-acceptance fixes, adopted deploy front, wire-projection writer, event-collector serialization, anchor ordering, or malformed tool-call-name sanitization (CL-6478) | sawyer | 2026-09-19 | `check:killdates` | +| `vendor/intx/inference` | `@intx/inference` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates doom-loop detection (`8da4c827`, `afd0c82b`, `c421c092`); one local delta: `providers/google-genai-files.ts` builds its upload body as `new Uint8Array(bytes)` because TS 6's lib.dom `BodyInit` rejects `Uint8Array` (upstream compiles ESNext-only under TS 5.9); retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/mail-memory` | `@intx/mail-memory` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the `@intx/mailbox` extraction (`af03bb90`), on-demand body reads (`54f7c239`) and `expunge` returning the swept uids (`bcabb1f8`) that the re-vendored `workflow-host` binds against; no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/mailbox` | `@intx/mailbox` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | Never published: a new package at the target pin (`af03bb90`) that `workflow-host`'s substrate mailbox store and supervisor-backed transport import; no local delta; retired by its first publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/mime` | `@intx/mime` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the non-RFC message-id guard `isMessageId` (`d97e1832`), the full `References` chain (`65c6fe70`) and the lossless `decodeMail` decoder (`3b6d06b2`) that `mailbox`/`mail-memory` at the same pin import; no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/types` | `@intx/types` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the type surface the re-vendored trees compile against: `expunge` returning `expungedUids` (`bcabb1f8`), plain-string `PackRejectReason` (`7b42f405`), the run authorization/approvals REST types (`71ad6c08`), the decoded-mail `Mail`/`MailPartReader` model (`3b6d06b2`) and the `interchange.actions`/`loops` package-json refs (`3bd5b837`, `1ea2f39b`); no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/workflow` | `@intx/workflow` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | No local delta: npm 0.3.0 predates the `onBodyFailure: "tolerate"` section policy (`b977ade6`) that `@corbits/agent-runtime` authors and the action/loop primitives (`3bd5b837`, `1ea2f39b`) the re-vendored `workflow-host` runs; retired by the next `@intx/workflow` publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/workflow-deploy` | `@intx/workflow-deploy` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | No local delta: npm 0.3.0 predates `inertLoopBody` and the loop-body source pin (`1ea2f39b`) that the re-vendored `hub-sessions` imports; retired by the next `@intx/workflow-deploy` publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/workflow-host` | `@intx/workflow-host` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the empty-mail drop (CL-6164), the action/loop runtime bind (CL-6325; its adapters live in `packages/workflow-host-actions` since CL-6435), or the body-spawn authorize/credential threading (CL-6448); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` | The re-pin to `a8bc06ae` (upstream `origin/main`, 2026-08-27, 72 commits past `v0.3.0`) is landing row by row; npm is still `0.3.0`, so every tree an @@ -66,9 +66,9 @@ folded model, so the fifteen previously vendored trees that carried no local delta — `agent`, `authz`, `crypto`, `harness`, `hub-agent`, `hub-common`, `inference`, `inference-catalog`, `log`, `mail-memory`, `mime`, `pack-transport`, `storage-isogit`, `tool-packaging`, `types` — are retired: -deleted and consumed as published `@intx/*@0.3.0` packages. The six rows -above survive only because each carries (or must bind against) a local delta -the publish lacks. The root `package.json` `overrides` pin every `@intx/*` +deleted and consumed as published `@intx/*@0.3.0` packages. The rows +above survive because each carries a local delta the publish lacks, or is +imported at a newer API by a tree that does. The root `package.json` `overrides` pin every `@intx/*` name to `0.3.0` so external dependencies' older exact pins collapse onto the same resolution workbench uses: the npm publish for retired names, the vendored workspace copy for surviving ones. @@ -117,7 +117,7 @@ sidecar withheld the ack, and the hub redelivered forever (`docs/revendor-inventory.md`). `vendor/intx/hub-sessions` (CL-6361) also widens that same anchor lookup to resolve a per-step pack source address (`deriveStepAddress`'s `-@`, -`vendor/intx/workflow-deploy/src/orchestrator.ts:837`) back to its base run's +`@intx/workflow-deploy`'s `orchestrator.ts`) back to its base run's anchor address via the new pure helper `anchorAddressForPackSource`. Upstream keys the lookup on an exact `workflow_run.address` match, which only the anchor row ever carries; a multi-step deployment's per-step agents push their @@ -160,16 +160,11 @@ seam: `RunSuspendableChild`'s input and building the body resolver, so a body agent's tool calls gate through the same per-step grant snapshot a top-level step's do instead of the host's throwing authorize stub. Upstream never runs tool-bearing body agents, so -the seam has no upstream analog yet. `vendor/intx/workflow` (CL-6326, CL-6324) gives -`onTrigger` an `onBodyFailure?: "end" | "continue"` policy: absent or `"end"` -preserves terminal-is-final, while `"continue"` lets a long-lived section -re-arm past a `failed` body occurrence instead of one bad turn permanently -ending the section. Cancellation is unaffected — it reflects a drain/operator -decision, not a turn-level error — and the failed occurrence stays on the -run's durable audit log either way, so the policy makes it non-fatal, never -silent. The live→inert projector carries the field too, so an authored policy -survives the child→hub projection the deploy gate hashes rather than being -dropped on the way. `vendor/intx/hub-sessions` (CL-6324) adds a third +the seam has no upstream analog yet. `vendor/intx/workflow` carries no local delta: upstream `b977ade6` ships the +`onTrigger` body-failure policy workbench had vendored as +`onBodyFailure: "continue"` (CL-6326, CL-6324) under the literal `"tolerate"`, +so the authoring site (`@corbits/agent-runtime`) says `"tolerate"` and `@intx/db` +migration `0088` rewrites the retired literal inside stored wire projections. `vendor/intx/hub-sessions` (CL-6324) adds a third code-sourced deploy front, `deployAdoptedCodeSourcedWorkflow`, which deploys onto shared capacity while adopting an anchor `workflow_run` row the caller already owns. Neither upstream front can: `deployWorkflowFromSource` inserts @@ -278,7 +273,7 @@ remaining shared modules stay substantially rewritten, as the row records. - [ ] Restore the `@intx/*` dependencies in `apps/*`, `packages/*`, and `workflows/*` to the published npm version that covers each surviving tree's delta, and drop the root `overrides` pins. -- [ ] Delete the six ledger rows above and the local-modifications note. +- [ ] Delete the `vendor/intx/*` ledger rows above and the local-modifications note. - [ ] Drop the `vendor/intx/*` rows from `scripts/checks/kill-dates.txt`. - [ ] `bun install` - [ ] `bun run check` diff --git a/bun.lock b/bun.lock index 6e5f5e1b9..c506ba175 100644 --- a/bun.lock +++ b/bun.lock @@ -1553,7 +1553,7 @@ "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", "@intx/log": "0.3.0", - "@intx/types": "0.3.0", + "@intx/types": "workspace:*", "arktype": "catalog:", "drizzle-orm": "catalog:", "postgres": "catalog:", @@ -1601,17 +1601,17 @@ "name": "@intx/hub-sessions", "version": "0.3.0", "dependencies": { - "@intx/agent": "0.3.0", + "@intx/agent": "workspace:*", "@intx/crypto": "0.3.0", "@intx/db": "workspace:*", "@intx/hub-common": "0.3.0", - "@intx/inference": "0.3.0", + "@intx/inference": "workspace:*", "@intx/log": "0.3.0", - "@intx/mime": "0.3.0", + "@intx/mime": "workspace:*", "@intx/pack-transport": "0.3.0", "@intx/storage-isogit": "0.3.0", "@intx/tool-packaging": "0.3.0", - "@intx/types": "0.3.0", + "@intx/types": "workspace:*", "@intx/workflow": "workspace:*", "@intx/workflow-deploy": "workspace:*", "arktype": "catalog:", @@ -1697,9 +1697,9 @@ "name": "@intx/workflow", "version": "0.3.0", "dependencies": { - "@intx/agent": "0.3.0", - "@intx/inference": "0.3.0", - "@intx/types": "0.3.0", + "@intx/agent": "workspace:*", + "@intx/inference": "workspace:*", + "@intx/types": "workspace:*", "arktype": "catalog:", }, "devDependencies": { @@ -1711,9 +1711,9 @@ "name": "@intx/workflow-deploy", "version": "0.3.0", "dependencies": { - "@intx/agent": "0.3.0", + "@intx/agent": "workspace:*", "@intx/tool-packaging": "0.3.0", - "@intx/types": "0.3.0", + "@intx/types": "workspace:*", "@intx/workflow": "workspace:*", }, "devDependencies": { @@ -1960,12 +1960,12 @@ "@intx/agent": "workspace:*", "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", - "@intx/db": "0.3.0", + "@intx/db": "workspace:*", "@intx/harness": "0.3.0", "@intx/hub-agent": "0.3.0", - "@intx/hub-api": "0.3.0", + "@intx/hub-api": "workspace:*", "@intx/hub-common": "0.3.0", - "@intx/hub-sessions": "0.3.0", + "@intx/hub-sessions": "workspace:*", "@intx/inference": "workspace:*", "@intx/inference-catalog": "0.3.0", "@intx/log": "0.3.0", @@ -1976,9 +1976,9 @@ "@intx/storage-isogit": "0.3.0", "@intx/tool-packaging": "0.3.0", "@intx/types": "workspace:*", - "@intx/workflow": "0.3.0", - "@intx/workflow-deploy": "0.3.0", - "@intx/workflow-host": "0.3.0", + "@intx/workflow": "workspace:*", + "@intx/workflow-deploy": "workspace:*", + "@intx/workflow-host": "workspace:*", "arktype": "^2.2.0", "better-auth": "1.6.29", "hono": "4.13.3", diff --git a/docs/CHAT.md b/docs/CHAT.md index 3d4169640..22327bfbc 100644 --- a/docs/CHAT.md +++ b/docs/CHAT.md @@ -112,7 +112,7 @@ it — one switch feeding both the wake and the relaunch path. The workbench itself has no host run and no occurrences to name; only invited agents deploy as sections. -`onBodyFailure: "continue"` — authored in the section shape itself — is +`onBodyFailure: "tolerate"` — authored in the section shape itself — is the failure edge: a turn that ends `failed` records the failed occurrence and leaves the section subscribed, so one bad turn kills neither the agent nor the room. The runtime names an occurrence's child run `turn__`, diff --git a/package.json b/package.json index 273cd082c..c54afb0ed 100644 --- a/package.json +++ b/package.json @@ -92,12 +92,12 @@ "@intx/agent": "workspace:*", "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", - "@intx/db": "0.3.0", + "@intx/db": "workspace:*", "@intx/harness": "0.3.0", "@intx/hub-agent": "0.3.0", - "@intx/hub-api": "0.3.0", + "@intx/hub-api": "workspace:*", "@intx/hub-common": "0.3.0", - "@intx/hub-sessions": "0.3.0", + "@intx/hub-sessions": "workspace:*", "@intx/inference": "workspace:*", "@intx/inference-catalog": "0.3.0", "@intx/log": "0.3.0", @@ -107,9 +107,9 @@ "@intx/storage-isogit": "0.3.0", "@intx/tool-packaging": "0.3.0", "@intx/types": "workspace:*", - "@intx/workflow": "0.3.0", - "@intx/workflow-deploy": "0.3.0", - "@intx/workflow-host": "0.3.0", + "@intx/workflow": "workspace:*", + "@intx/workflow-deploy": "workspace:*", + "@intx/workflow-host": "workspace:*", "better-auth": "1.6.29", "hono": "4.13.3", "@intx/mailbox": "workspace:*" diff --git a/packages/agent-runtime/src/definition.test.ts b/packages/agent-runtime/src/definition.test.ts index 27d2cb58e..7b8ab6668 100644 --- a/packages/agent-runtime/src/definition.test.ts +++ b/packages/agent-runtime/src/definition.test.ts @@ -151,7 +151,7 @@ describe("buildAgentRuntimeWorkflow — section mode", () => { const section = buildAgentRuntimeWorkflow(sectionConfig).steps[AGENT_RUNTIME_SECTION_ID]; - expect(section).toMatchObject({ onBodyFailure: "continue" }); + expect(section).toMatchObject({ onBodyFailure: "tolerate" }); }); test("the section's failure policy survives the live→inert projection", () => { @@ -162,7 +162,7 @@ describe("buildAgentRuntimeWorkflow — section mode", () => { expect(section).toMatchObject({ kind: "onTrigger", - onBodyFailure: "continue", + onBodyFailure: "tolerate", }); }); diff --git a/packages/agent-runtime/src/definition.ts b/packages/agent-runtime/src/definition.ts index d47c631de..ed8124ce3 100644 --- a/packages/agent-runtime/src/definition.ts +++ b/packages/agent-runtime/src/definition.ts @@ -15,11 +15,11 @@ // whose body is one agent step, so every message becomes an occurrence // with its own child run id and event log. // -// Section mode authors `onBodyFailure: "continue"`, the failure edge +// Section mode authors `onBodyFailure: "tolerate"`, the failure edge // that keeps a section subscribed after a failed turn: a conversation // whose agent threw on one message must still answer the next, and the -// primitive's default (`"end"`) retires the whole run instead. The -// vendored surface carries the field through the live→inert projection, +// primitive's default (`"end"`) retires the whole run instead. +// `@intx/workflow` carries the field through the live→inert projection, // so the policy reaches the hub's frozen projection rather than being // silently dropped before deploy. import { buildSingleStepAgentDefinition } from "@intx/workflow-deploy"; @@ -106,7 +106,7 @@ function buildSectionWorkflow( [AGENT_RUNTIME_SECTION_ID]: onTrigger({ on: { type: "mail" as const, to: config.triggerAddress }, body, - onBodyFailure: "continue", + onBodyFailure: "tolerate", }), }; return config.credentialBindings.length > 0 diff --git a/packages/chat/src/standalone-launch.ts b/packages/chat/src/standalone-launch.ts index 731a770ee..99e87ab71 100644 --- a/packages/chat/src/standalone-launch.ts +++ b/packages/chat/src/standalone-launch.ts @@ -26,7 +26,7 @@ import { CHAT_TURN_TIMEOUT_MS } from "./turn-claims"; * That child id is what a reply's `run_id` carries, which is the whole * reason a reply is traceable. * - * `onBodyFailure: "continue"` — authored in the section shape itself + * `onBodyFailure: "tolerate"` — authored in the section shape itself * (`@corbits/agent-runtime`) — is the failure edge: a turn that throws * records a failed occurrence and leaves the section subscribed, so one * bad turn kills neither the agent nor the conversation. diff --git a/packages/chat/test/platform-adapter.test.ts b/packages/chat/test/platform-adapter.test.ts index 8e1325790..1dddccd90 100644 --- a/packages/chat/test/platform-adapter.test.ts +++ b/packages/chat/test/platform-adapter.test.ts @@ -1097,12 +1097,12 @@ describe("createHubChatPlatform", () => { // CL-6329: a room agent deploys as an `onTrigger` section, so every // message it is asked to answer is an occurrence with its own child - // run — and `onBodyFailure: "continue"` keeps the section subscribed + // run — and `onBodyFailure: "tolerate"` keeps the section subscribed // after a turn that threw. const rendered = deployedDefinition(assetService.populatedTrees); expect(Object.keys(rendered.steps)).toEqual([AGENT_RUNTIME_SECTION_ID]); expect(rendered.steps[AGENT_RUNTIME_SECTION_ID]).toMatchObject({ - onBodyFailure: "continue", + onBodyFailure: "tolerate", }); }); diff --git a/packages/folded-runs/test/definition.test.ts b/packages/folded-runs/test/definition.test.ts index 938af4699..2850215cf 100644 --- a/packages/folded-runs/test/definition.test.ts +++ b/packages/folded-runs/test/definition.test.ts @@ -57,7 +57,7 @@ function sectionProjection(overrides: Partial> = {}) { kind: "onTrigger", id: "turn", on: { type: "mail", to: "agent@example.com" }, - onBodyFailure: "continue", + onBodyFailure: "tolerate", body: { inline: { id: "wfd_2_body", diff --git a/scripts/checks/kill-dates.txt b/scripts/checks/kill-dates.txt index c73a9e77f..c2c29bbed 100644 --- a/scripts/checks/kill-dates.txt +++ b/scripts/checks/kill-dates.txt @@ -15,16 +15,16 @@ # package's VENDORED-FROM delta line in the same change. apps/sidecar | sawyer | 2026-09-19 vendor/intx/agent | sawyer | 2026-10-26 | d0d56d9f452b78f4b541ad8f4e89f975e8069446bb98f2c8097b90de4b020243 -vendor/intx/db | sawyer | 2026-09-19 | 3ab08b9122f3ab84d60e1f2d5b7af88c3eb673f9120ce6dedf3d2600d4a6cad4 -vendor/intx/hub-api | sawyer | 2026-09-19 | f93a383cb5d6acdf50a461b43e4c8991dbdf7e13d34a4e5598e556eaee66308b -vendor/intx/hub-sessions | sawyer | 2026-09-19 | df5f1d275e197668851c53f58c51182d8bc455f3585e2ed65784d3687a856001 +vendor/intx/db | sawyer | 2026-09-19 | 0a4cdb9a8a6ff19d5d4713cbc4f5cc9257aad839b1fa393b2026e6d5afd828b9 +vendor/intx/hub-api | sawyer | 2026-09-19 | 4ee1c6b69304cdc4097a50b79b0ce02505dbdc918a36bd679b6f797e61eb5370 +vendor/intx/hub-sessions | sawyer | 2026-09-19 | e3344253ed6f2b0998599f9f8712d4e9b0757895b31a3e7df6a245c2bb76af3c vendor/intx/inference | sawyer | 2026-10-26 | 77fec29b078e8d03e686747c70e6b62ac1fd1434db0fb2c1e12e84b6dc71465f vendor/intx/mail-memory | sawyer | 2026-10-26 | 9f3601a7fb22e2d1c63daa976f3afccbd79af2187c155a0080c0d60c82450b92 vendor/intx/mailbox | sawyer | 2026-10-26 | d36d7ffcc32018571276e4922a8c2714b7ee0bb5deb80b01e73859245975d4c6 vendor/intx/mime | sawyer | 2026-10-26 | d02e5f8f1429eac7c27d3a37eec31111f8a1053c91fbfae77ac58a0d63c823ed vendor/intx/types | sawyer | 2026-10-26 | ec1de14b859007b4db137da1533d4ce79d11024ad69c8b36938017319d6d8e86 -vendor/intx/workflow | sawyer | 2026-09-19 | 34628e7bbd0587f131a07e3a206141983881106963a20ab607e68aeed1135593 -vendor/intx/workflow-deploy | sawyer | 2026-09-19 | 95711adf282180852b0daec1cac39d00a4dc24aff15f9a515e07eb3d2ca749f9 +vendor/intx/workflow | sawyer | 2026-09-19 | 4b51b9bd6a124cfaa0c916e2b26c04ac9170618bb092f0c8e1c312263fc84fdf +vendor/intx/workflow-deploy | sawyer | 2026-10-26 | 960a2ae408223649fe8be0e3b9d63f2b0cca25259bc0ae06761bca521bb738e5 vendor/intx/workflow-host | sawyer | 2026-09-19 | 48ae3e34c6f14b99a3a940ede98b119e52dfe7410a86f644d3b78cf6e7d44f4d packages/folded-runs | sawyer | 2026-11-01 diff --git a/scripts/e2e/cl-6324-launch-proof.ts b/scripts/e2e/cl-6324-launch-proof.ts index a79fdc127..c2c62c3f5 100644 --- a/scripts/e2e/cl-6324-launch-proof.ts +++ b/scripts/e2e/cl-6324-launch-proof.ts @@ -1128,7 +1128,7 @@ async function main(): Promise { ); expectStatus("send the mid-turn message", sent, 201); // The section deployment takes the same kill mid-occurrence, so the - // restart has to prove `onBodyFailure: "continue"` too: a section + // restart has to prove `onBodyFailure: "tolerate"` too: a section // that retired on the dead body would never answer again. const sectionSent = await api( hub.baseUrl, @@ -1221,7 +1221,7 @@ async function main(): Promise { // The section deployment rode the same kill. Boot restore replays its // pin from the sidecar data dir exactly as it does the folded run's, - // and `onBodyFailure: "continue"` is what keeps the section subscribed + // and `onBodyFailure: "tolerate"` is what keeps the section subscribed // when the killed occurrence died mid-body: a section that retired on // that failure would never produce a second occurrence at all. // diff --git a/scripts/e2e/cl-6329-turn-swap-proof.ts b/scripts/e2e/cl-6329-turn-swap-proof.ts index 3eafca898..d1a1527ba 100644 --- a/scripts/e2e/cl-6329-turn-swap-proof.ts +++ b/scripts/e2e/cl-6329-turn-swap-proof.ts @@ -9,7 +9,7 @@ // 2. Three rapid messages serialize into ordered turns rather than // racing: occurrences 0,1,2 in arrival order. // 3. A turn killed mid-occurrence leaves BOTH the room and the section -// alive: `onBodyFailure: "continue"` keeps the section subscribed, +// alive: `onBodyFailure: "tolerate"` keeps the section subscribed, // the failed turn is visible rather than silent, and the next // message is still answered. // diff --git a/vendor/intx/db/VENDORED-FROM b/vendor/intx/db/VENDORED-FROM index 5009a2c63..a2c0625ac 100644 --- a/vendor/intx/db/VENDORED-FROM +++ b/vendor/intx/db/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/db) Commit: a8bc06ae38661c5e0ed91ded8559bf09f502213d (origin/main, 2026-08-27) License: LGPL-2.1-only (see vendor/intx/LICENSE) -Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. CL-6324: workflow_definition_version gains a wire_projection jsonb column (migration 0086_workflow_definition_version_wire_projection.sql -- renumbered from 0085 when upstream a8bc06ae took that slot for 0085_add_approval_run_idx.sql, and from 0084 before that; the journal keeps our original `when` values, which still sort after upstream's 0085), schema/workflow-definitions.ts declares it, and workflow-definition-store.ts adds loadFrozenWireProjection (exported from src/index.ts) to read it back beside the approved wire hash. CL-6452: workflow_definition gains an origin column (migration 0087_workflow_definition_origin.sql, renumbered from 0086; declared in schema/workflow-definitions.ts) marking the per-run record a folded run's deploy mints for itself, so launch and refresh resolve the hub-authored definition instead of the newest same-named row. Workbench applies these files by sorted filename through scripts/db-setup.ts (replay-from-scratch with a filename ledger); the drizzle journal exists for drizzle-kit only. +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. CL-6324: workflow_definition_version gains a wire_projection jsonb column (migration 0086_workflow_definition_version_wire_projection.sql -- renumbered from 0085 when upstream a8bc06ae took that slot for 0085_add_approval_run_idx.sql, and from 0084 before that; the journal keeps our original `when` values, which still sort after upstream's 0085), schema/workflow-definitions.ts declares it, and workflow-definition-store.ts adds loadFrozenWireProjection (exported from src/index.ts) to read it back beside the approved wire hash. CL-6452: workflow_definition gains an origin column (migration 0087_workflow_definition_origin.sql, renumbered from 0086; declared in schema/workflow-definitions.ts) marking the per-run record a folded run's deploy mints for itself, so launch and refresh resolve the hub-authored definition instead of the newest same-named row. Migration 0088_workflow_definition_version_tolerate_body_failure.sql rewrites the retired `onBodyFailure: "continue"` literal (our pre-a8bc06ae vendored @intx/workflow delta) to upstream b977ade6's `"tolerate"` inside stored wire projections. Workbench applies these files by sorted filename through scripts/db-setup.ts (replay-from-scratch with a filename ledger); the drizzle journal exists for drizzle-kit only. diff --git a/vendor/intx/db/migrations/0088_workflow_definition_version_tolerate_body_failure.sql b/vendor/intx/db/migrations/0088_workflow_definition_version_tolerate_body_failure.sql new file mode 100644 index 000000000..f995f7f5c --- /dev/null +++ b/vendor/intx/db/migrations/0088_workflow_definition_version_tolerate_body_failure.sql @@ -0,0 +1,9 @@ +-- WORKBENCH DELTA (see VENDORED.md): upstream b977ade6 named the opt-in +-- onTrigger body-failure policy "tolerate"; workbench's earlier vendored +-- delta had called the same policy "continue". A frozen inert projection +-- is stored verbatim, so rewrite the retired literal in place. Postgres +-- renders jsonb text canonically (`"key": "value"`), so the textual +-- replace matches exactly this key/value pair at any nesting depth. +UPDATE "workflow_definition_version" +SET "wire_projection" = replace("wire_projection"::text, '"onBodyFailure": "continue"', '"onBodyFailure": "tolerate"')::jsonb +WHERE "wire_projection"::text LIKE '%"onBodyFailure": "continue"%'; diff --git a/vendor/intx/db/migrations/meta/_journal.json b/vendor/intx/db/migrations/meta/_journal.json index 9aa4ad833..d35207e9e 100644 --- a/vendor/intx/db/migrations/meta/_journal.json +++ b/vendor/intx/db/migrations/meta/_journal.json @@ -610,6 +610,13 @@ "when": 1787810000000, "tag": "0087_workflow_definition_origin", "breakpoints": true + }, + { + "idx": 88, + "version": "7", + "when": 1787900000000, + "tag": "0088_workflow_definition_version_tolerate_body_failure", + "breakpoints": true } ] } diff --git a/vendor/intx/db/src/migrations-journal.test.ts b/vendor/intx/db/src/migrations-journal.test.ts index af9e90f58..5337d0f26 100644 --- a/vendor/intx/db/src/migrations-journal.test.ts +++ b/vendor/intx/db/src/migrations-journal.test.ts @@ -1,26 +1,22 @@ // WORKBENCH DELTA (see VENDORED.md): our two migrations ride behind // upstream's in the drizzle-kit journal. Renumbering them at each re-pin // is a hand edit, so pin the invariants drizzle-kit relies on: one journal -// entry per SQL file, in filename order, with non-decreasing `when`. +// entry per SQL file, in filename order. (`idx`/`when` are not asserted: +// upstream's own journal has an idx gap and non-monotonic timestamps.) import { expect, test } from "bun:test"; import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; const MIGRATIONS_DIR = path.resolve(import.meta.dir, "..", "migrations"); -test("every migration file has a journal entry in filename order with non-decreasing `when`", () => { +test("every migration file has a journal entry in filename order", () => { const files = readdirSync(MIGRATIONS_DIR) .filter((file) => file.endsWith(".sql")) .sort() .map((file) => file.slice(0, -".sql".length)); const journal = JSON.parse( readFileSync(path.join(MIGRATIONS_DIR, "meta", "_journal.json"), "utf8"), - ) as { entries: { idx: number; when: number; tag: string }[] }; + ) as { entries: { tag: string }[] }; expect(journal.entries.map((entry) => entry.tag)).toEqual(files); - journal.entries.forEach((entry, i) => { - expect(entry.idx).toBe(i); - expect(entry.tag.startsWith(String(i).padStart(4, "0"))).toBe(true); - if (i > 0) expect(entry.when).toBeGreaterThanOrEqual(journal.entries[i - 1]!.when); - }); }); diff --git a/vendor/intx/db/src/tolerate-migration.test.ts b/vendor/intx/db/src/tolerate-migration.test.ts new file mode 100644 index 000000000..6a4173551 --- /dev/null +++ b/vendor/intx/db/src/tolerate-migration.test.ts @@ -0,0 +1,77 @@ +// WORKBENCH DELTA (see VENDORED.md): migration 0088 rewrites the retired +// `onBodyFailure: "continue"` literal to upstream's `"tolerate"` inside +// frozen wire projections. Exercised against a scratch table carrying the +// column the statement touches. DB-gated: skipped when DATABASE_URL is unset. +import { afterAll, describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { userInfo } from "node:os"; +import path from "node:path"; +import postgres from "postgres"; + +const databaseUrl = process.env["DATABASE_URL"] ?? ""; +const describeIfDb = databaseUrl === "" ? describe.skip : describe; + +const MIGRATION = path.resolve( + import.meta.dir, + "..", + "migrations", + "0088_workflow_definition_version_tolerate_body_failure.sql", +); + +describeIfDb("0088 rewrites onBodyFailure continue -> tolerate", () => { + const schema = `tolerate_test_${Date.now().toString(36)}`; + const parsed = new URL(databaseUrl); + const sql = postgres({ + host: parsed.hostname, + port: Number(parsed.port || 5432), + user: decodeURIComponent(parsed.username) || userInfo().username, + password: decodeURIComponent(parsed.password), + database: parsed.pathname.slice(1), + max: 1, + onnotice: () => undefined, + connection: { search_path: `"${schema}"` }, + }); + afterAll(async () => { + await sql.unsafe(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`); + await sql.end(); + }); + + test("rewrites the literal at any depth and leaves other rows untouched", async () => { + await sql.unsafe(`CREATE SCHEMA "${schema}"`); + await sql.unsafe( + `CREATE TABLE "workflow_definition_version" (id text PRIMARY KEY, wire_projection jsonb)`, + ); + const nested = { + steps: { + section: { + kind: "onTrigger", + onBodyFailure: "continue", + body: { + inline: { + steps: { inner: { kind: "onTrigger", onBodyFailure: "continue" } }, + }, + }, + }, + }, + }; + const untouched = { steps: { s: { kind: "onTrigger", onBodyFailure: "end" } } }; + await sql`INSERT INTO "workflow_definition_version" VALUES + ('a', ${sql.json(nested)}), ('b', ${sql.json(untouched)}), ('c', NULL)`; + + for (const statement of readFileSync(MIGRATION, "utf8") + .split("--> statement-breakpoint") + .map((s) => s.trim()) + .filter((s) => s.length > 0)) { + await sql.unsafe(statement); + } + + const rows = await sql<{ id: string; wire_projection: unknown }[]>` + SELECT id, wire_projection FROM "workflow_definition_version" ORDER BY id`; + expect(JSON.stringify(rows[0]!.wire_projection)).not.toContain("continue"); + expect(rows[0]!.wire_projection).toEqual( + JSON.parse(JSON.stringify(nested).replaceAll('"continue"', '"tolerate"')), + ); + expect(rows[1]!.wire_projection).toEqual(untouched); + expect(rows[2]!.wire_projection).toBeNull(); + }); +}); diff --git a/vendor/intx/hub-sessions/package.json b/vendor/intx/hub-sessions/package.json index 752a88d57..56ecd1baf 100644 --- a/vendor/intx/hub-sessions/package.json +++ b/vendor/intx/hub-sessions/package.json @@ -31,7 +31,7 @@ "@intx/tool-packaging": "0.3.0", "@intx/types": "0.3.0", "@intx/workflow": "workspace:*", - "@intx/workflow-deploy": "workspace:*", + "@intx/workflow-deploy": "0.3.0", "arktype": "catalog:", "drizzle-orm": "catalog:", "isomorphic-git": "catalog:", diff --git a/vendor/intx/workflow-deploy/VENDORED-FROM b/vendor/intx/workflow-deploy/VENDORED-FROM index ada3d2d8a..6fa01b996 100644 --- a/vendor/intx/workflow-deploy/VENDORED-FROM +++ b/vendor/intx/workflow-deploy/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/workflow-deploy) -Commit: b5580a02fb918eebccc33ded7727ffee781ffbd1 (tag v0.3.0) +Commit: a8bc06ae38661c5e0ed91ded8559bf09f502213d (origin/main, 2026-08-27) License: LGPL-2.1-only (see vendor/intx/LICENSE) -Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. No source delta. diff --git a/vendor/intx/workflow-deploy/package.json b/vendor/intx/workflow-deploy/package.json index 6c5bd0d4a..d16200a4f 100644 --- a/vendor/intx/workflow-deploy/package.json +++ b/vendor/intx/workflow-deploy/package.json @@ -18,9 +18,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@intx/agent": "0.3.0", + "@intx/agent": "workspace:*", "@intx/tool-packaging": "0.3.0", - "@intx/types": "0.3.0", + "@intx/types": "workspace:*", "@intx/workflow": "workspace:*" }, "devDependencies": { diff --git a/vendor/intx/workflow-deploy/src/index.ts b/vendor/intx/workflow-deploy/src/index.ts index dc53298b8..a366bba30 100644 --- a/vendor/intx/workflow-deploy/src/index.ts +++ b/vendor/intx/workflow-deploy/src/index.ts @@ -30,6 +30,7 @@ export { export { extractFoldedBody, type FoldedBody } from "./fold-synthesis"; export { enumerateInertOnTriggerBodies, + inertLoopBody, type EnumeratedInertOnTriggerBody, type InertBodyStepPreference, } from "./inert-ontrigger-bodies"; diff --git a/vendor/intx/workflow-deploy/src/inert-ontrigger-bodies.ts b/vendor/intx/workflow-deploy/src/inert-ontrigger-bodies.ts index 177acfb8c..070c44567 100644 --- a/vendor/intx/workflow-deploy/src/inert-ontrigger-bodies.ts +++ b/vendor/intx/workflow-deploy/src/inert-ontrigger-bodies.ts @@ -112,6 +112,30 @@ function firstPreference( * enumeration here and the top-level source pin in `orchestrator.ts` read a * step's preference through one validator. */ +const InertLoopStep = type({ kind: "'loop'", body: "unknown" }); + +/** + * If an inert projection step is a `loop`, return its body projection (a nested + * inert workflow definition); otherwise null. A loop body runs in-process as a + * child run sharing the parent's env, so its agent steps resolve their pinned + * inference source from the same flat top-level sources map -- the source pin + * recurses through this into loop bodies. Throws on a `loop` step whose body is + * not a valid projection (a malformed frozen projection). + */ +export function inertLoopBody( + stepValue: unknown, +): typeof WorkflowProjectionDefinition.infer | null { + const asLoop = InertLoopStep(stepValue); + if (asLoop instanceof type.errors) return null; + const body = WorkflowProjectionDefinition(asLoop.body); + if (body instanceof type.errors) { + throw new Error( + `inertLoopBody: loop step body is not a valid workflow projection: ${body.summary}`, + ); + } + return body; +} + export function readInertStepPreference( stepValue: unknown, context: string, diff --git a/vendor/intx/workflow-deploy/src/orchestrator.ts b/vendor/intx/workflow-deploy/src/orchestrator.ts index bef676d03..a110c3e68 100644 --- a/vendor/intx/workflow-deploy/src/orchestrator.ts +++ b/vendor/intx/workflow-deploy/src/orchestrator.ts @@ -28,7 +28,10 @@ import type { WorkflowProjectionDefinition } from "@intx/types/sidecar"; import { formatRunAddress } from "@intx/types"; import { type ApprovalSet } from "./capability-approval"; -import { readInertStepPreference } from "./inert-ontrigger-bodies"; +import { + inertLoopBody, + readInertStepPreference, +} from "./inert-ontrigger-bodies"; /** * Minimal structural `DeployContent` shape. Carried as a structural type so @@ -137,18 +140,27 @@ export function pickStepInferenceSource(args: { } /** - * Pin every TOP-LEVEL step of a frozen inert projection to a single approved - * inference source, producing the `sources` map the source-ref deploy frame - * carries. The hub holds no live definition, so each step's declared - * `(provider, model)` preference is read off the inert projection's - * `modelSources` and resolved through the `pickStepInferenceSource` resolver + - * operator-approval gate. A step whose preferred source the operator never - * approved (or that resolves to no approved source at all) throws, failing the - * whole deploy closed before any frame is sent. + * Pin every step of a frozen inert projection to a single approved inference + * source, producing the `sources` map the source-ref deploy frame carries. The + * hub holds no live definition, so each step's declared `(provider, model)` + * preference is read off the inert projection's `modelSources` and resolved + * through the `pickStepInferenceSource` resolver + operator-approval gate. A + * step whose preferred source the operator never approved (or that resolves to + * no approved source at all) throws, failing the whole deploy closed before any + * frame is sent. * - * Every step in `stepOrder` gets one entry (a non-agent step falls back to the - * approved default), so the sidecar child finds a pinned source for each - * staged step. + * The walk RECURSES into `loop` bodies: a loop body runs in-process as a child + * run sharing the parent's env, so its agent steps resolve their pinned source + * from this same flat map, keyed by the body step's plain id. Loop-body step + * ids share a namespace with the top-level steps here; a body step id that + * collides with another step must resolve to the same source, else the deploy + * fails closed rather than silently mis-pin. (onTrigger bodies are NOT walked + * here -- they are lifted to `referencedDefinitions` with their own per-body pin + * in the deploy composition. childWorkflow bodies are resolved at the child + * host, not pinned here.) + * + * Every step gets one entry (a non-agent step falls back to the approved + * default), so the sidecar child finds a pinned source for each staged step. */ export function buildInertProjectionStepSources(args: { projection: WorkflowProjectionDefinition; @@ -156,25 +168,44 @@ export function buildInertProjectionStepSources(args: { operatorApprovals: ApprovalSet; }): Record { const sources: Record = {}; - for (const stepId of args.projection.stepOrder) { - const preferred = readInertStepPreference( - args.projection.steps[stepId], - "buildInertProjectionStepSources: ", - stepId, - ); - sources[stepId] = [ - pickStepInferenceSource({ + const pin = (def: WorkflowProjectionDefinition): void => { + for (const stepId of def.stepOrder) { + const stepValue = def.steps[stepId]; + const preferred = readInertStepPreference( + stepValue, + "buildInertProjectionStepSources: ", + stepId, + ); + const resolved = pickStepInferenceSource({ preferred, stepId, workflowId: args.projection.id, config: args.config, operatorApprovals: args.operatorApprovals, - }), - ]; - } + }); + const existing = sources[stepId]?.[0]; + if (existing !== undefined) { + if (!sameInferenceSource(existing, resolved)) { + throw new WorkflowDefinitionInvalidError( + args.projection.id, + `step id ${stepId} resolves to two different inference sources across nested loop bodies; a loop-body step id that collides with another step must resolve to the same source`, + ); + } + } else { + sources[stepId] = [resolved]; + } + const loopBody = inertLoopBody(stepValue); + if (loopBody !== null) pin(loopBody); + } + }; + pin(args.projection); return sources; } +function sameInferenceSource(a: InferenceSource, b: InferenceSource): boolean { + return a.id === b.id && a.provider === b.provider && a.model === b.model; +} + /** * Pure function: derive a step's run address from * `(runId, stepId, domain)`. Exported so the supervisor can reconstruct diff --git a/vendor/intx/workflow-deploy/tsconfig.json b/vendor/intx/workflow-deploy/tsconfig.json index d984862c9..dbbb0384b 100644 --- a/vendor/intx/workflow-deploy/tsconfig.json +++ b/vendor/intx/workflow-deploy/tsconfig.json @@ -1,11 +1,11 @@ { "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.ts" + ], "compilerOptions": { "types": [ "bun" ] - }, - "include": [ - "src/**/*.ts" - ] + } } diff --git a/vendor/intx/workflow/README.md b/vendor/intx/workflow/README.md index 0ba6d0068..d1ebacf45 100644 --- a/vendor/intx/workflow/README.md +++ b/vendor/intx/workflow/README.md @@ -65,3 +65,29 @@ selector DSL is intentionally a pure, statically-inspectable path vocabulary (so the deploy-time capability walk can compute grants without executing user code), and parsing an opaque agent reply is host work that belongs at an `action`/LoopFn seam. + +## Crash and suspension behavior of a `loop` body + +Two facts govern what a `loop` body can and cannot survive. + +First, crashes. An `action` runs at most once. A mid-invocation crash fails the +run and the effect is never re-run -- and this is true everywhere, not just in a +loop: a top-level action or agent step that crashes mid-invocation also settles +`RunFailed` (it is not re-invoked on resume). So a crash inside a loop iteration +fails the run exactly as a crash in any other step does; loops are not special +here. + +Second, suspension -- and this IS the loop-specific limitation. The body-ban +forbids a loop body from containing an `awaitSignal`, `sleep`, `childWorkflow`, +or a nested `loop`; the first three are the suspending primitives that matter +here. A top-level step and an onTrigger section body CAN park on such a +primitive and resume across a restart; a loop body cannot. Loops run each +iteration in-process with no durable per-iteration park/resume, so an iteration +is a short, self-contained unit that runs start-to-finish or fails. + +Practical guidance: use a loop to repeat a cheap, self-contained unit until a +pure `while`/`carry` says stop. Do not model a long-lived, human-in-the-loop, or +otherwise suspending interaction as a loop body -- put that in an onTrigger +section (which may `awaitSignal`) or a top-level step. Keep a loop body's action +idempotent where practical, since a crash fails the run and recovery is a fresh +trigger, not a mid-loop resume. diff --git a/vendor/intx/workflow/VENDORED-FROM b/vendor/intx/workflow/VENDORED-FROM index bc1d52f04..1befdb63a 100644 --- a/vendor/intx/workflow/VENDORED-FROM +++ b/vendor/intx/workflow/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/workflow) -Commit: b5580a02fb918eebccc33ded7727ffee781ffbd1 (tag v0.3.0) +Commit: a8bc06ae38661c5e0ed91ded8559bf09f502213d (origin/main, 2026-08-27) License: LGPL-2.1-only (see vendor/intx/LICENSE) -Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. CL-6326: `onTrigger` gains an `onBodyFailure?: "end" | "continue"` policy field (definition/primitives.ts, re-exported from definition/index.ts as `BodyFailurePolicy`); `runtime/run.ts`'s steady-state drive loop and `planOnTriggerResume` read it live to let a `"continue"`-policy section re-arm past a `failed` body occurrence instead of ending the whole run (`cancelled` is unaffected, always terminal-is-final). CL-6324 extends it through the projection: `live-inert-projector.ts`'s `InertOnTrigger` and `projectOnTrigger` carry `onBodyFailure`, so an authored policy survives the live->inert projection the child->hub boundary hashes instead of being dropped before deploy. See VENDORED.md and docs/revendor-inventory.md. +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. No source delta: upstream b977ade6 supersedes the former onBodyFailure "continue" policy (CL-6326/CL-6324) as "tolerate". diff --git a/vendor/intx/workflow/package.json b/vendor/intx/workflow/package.json index ff6f73b7b..dd7e4b2da 100644 --- a/vendor/intx/workflow/package.json +++ b/vendor/intx/workflow/package.json @@ -27,13 +27,12 @@ } }, "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" + "typecheck": "tsc --noEmit" }, "dependencies": { - "@intx/agent": "0.3.0", - "@intx/inference": "0.3.0", - "@intx/types": "0.3.0", + "@intx/agent": "workspace:*", + "@intx/inference": "workspace:*", + "@intx/types": "workspace:*", "arktype": "catalog:" }, "devDependencies": { diff --git a/vendor/intx/workflow/src/definition/primitives.ts b/vendor/intx/workflow/src/definition/primitives.ts index ca87381e1..1b7335d4f 100644 --- a/vendor/intx/workflow/src/definition/primitives.ts +++ b/vendor/intx/workflow/src/definition/primitives.ts @@ -30,8 +30,6 @@ import type { Trigger } from "./triggers"; export type DrainBehavior = "cancel" | "wait"; -export type BodyFailurePolicy = "end" | "continue"; - export interface RetryPolicy { /** Maximum number of attempts including the first. */ maxAttempts: number; @@ -230,6 +228,16 @@ export interface LoopPrimitive extends PrimitiveBase { drainBehavior?: DrainBehavior; } +/** + * Section body-failure policy. Absent (or `"end"`) is terminal-is-final: a body + * run that ends non-`completed` ends the whole section (today's behavior). With + * `"tolerate"`, a body that ends `failed` re-arms the section for the next + * trigger instead of terminating; a cancelled body always terminates. The field + * is only present when an author opts in, so a default section's wire hash is + * unchanged. + */ +export type BodyFailurePolicy = "end" | "tolerate"; + /** * Long-lived, event-driven section. The workflow subscribes to `on` and * runs `body` -- a full sub-DAG -- once per occurrence of that trigger, @@ -241,35 +249,23 @@ export interface LoopPrimitive extends PrimitiveBase { * `trigger.payload`. * * The section never self-completes: the workflow stays running while - * subscribed and terminates only on a body run ending `cancelled`, a body - * run ending `failed` under the default `onBodyFailure: "end"` policy, or - * an explicit end-of-workflow -- `onBodyFailure: "continue"` keeps the - * section alive through a failed occurrence -- and a terminated run is - * final -- never relaunched. The first occurrence is the run's own firing - * trigger (its `RunStarted.trigger.payload`); each later occurrence - * arrives as an input signal carrying the next payload. `defineWorkflow` - * collects every `on` into the workflow's `triggers`, so `on` is the - * first-class binding between a trigger and the section it drives. + * subscribed and terminates only on a body error or an explicit + * end-of-workflow, and a terminated run is final -- never relaunched. The + * first occurrence is the run's own firing trigger (its + * `RunStarted.trigger.payload`); each later occurrence arrives as an input + * signal carrying the next payload. `defineWorkflow` collects every `on` + * into the workflow's `triggers`, so `on` is the first-class binding + * between a trigger and the section it drives. * * `drainBehavior` defaults to `"wait"`: a live interactive section is not * abandoned mid-conversation at redeploy unless the author opts into - * `"cancel"`. + * `"cancel"`. `onBodyFailure` defaults to `"end"`; see `BodyFailurePolicy`. */ export interface OnTriggerPrimitive extends PrimitiveBase { kind: "onTrigger"; on: Trigger; body: OnTriggerBody; drainBehavior?: DrainBehavior; - /** - * How a body run that ends `failed` affects the section. Absent (or - * `"end"`) preserves terminal-is-final: a failed body run ends the - * whole section run, exactly as before this field existed. `"continue"` - * records the failed occurrence and keeps the section subscribed -- - * the next occurrence spawns and runs normally. A body run that ends - * `cancelled` is unaffected by this field and always ends the section: - * cancellation reflects a drain/operator decision, not a turn-level - * error. - */ onBodyFailure?: BodyFailurePolicy; } @@ -603,6 +599,9 @@ export function onTrigger(opts: OnTriggerOpts): OnTriggerPrimitive { // Authored inline; the deploy step rewrites this to `{ ref }`. body: { inline: opts.body }, drainBehavior, + // Conditional passthrough (NOT resolved to a default like drainBehavior): + // an absent policy must leave the field off so a default section's inert + // projection -- and therefore its approval hash -- is unchanged. ...(opts.onBodyFailure !== undefined ? { onBodyFailure: opts.onBodyFailure } : {}), diff --git a/vendor/intx/workflow/src/definition/workflow.ts b/vendor/intx/workflow/src/definition/workflow.ts index c46197ce9..943f3006d 100644 --- a/vendor/intx/workflow/src/definition/workflow.ts +++ b/vendor/intx/workflow/src/definition/workflow.ts @@ -37,7 +37,6 @@ export interface WorkflowDefinition { * invoker's authority at trigger time. Each entry declares a resource, * action, and source (`creator` or `invoker`); the trigger route * materializes the satisfied ones as grants on the run principal. - * Mirrors an agent definition's `grantRequirements`. */ grantRequirements?: readonly GrantRequirement[]; /** @@ -46,7 +45,7 @@ export interface WorkflowDefinition { * concrete provider and authorizing the delegation against the * binding's authority. The launch reads these from the folded body and * materializes a consumer-scoped `credential:{id}` / `use` grant per - * binding. Mirrors an agent definition's `credentialBindings`. + * binding. */ credentialBindings?: readonly CredentialBinding[]; } diff --git a/vendor/intx/workflow/src/index.ts b/vendor/intx/workflow/src/index.ts index 0968d47e0..2ee70d791 100644 --- a/vendor/intx/workflow/src/index.ts +++ b/vendor/intx/workflow/src/index.ts @@ -72,6 +72,8 @@ export { export { runLocal, createLoopIteration, + createDefaultActionInvoker, + createInMemoryEffectLedger, createInMemoryRepoStore, createInMemoryScheduler, createInMemorySignalChannel, diff --git a/vendor/intx/workflow/src/live-inert-projector.test.ts b/vendor/intx/workflow/src/live-inert-projector.test.ts deleted file mode 100644 index 587ce4f1c..000000000 --- a/vendor/intx/workflow/src/live-inert-projector.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { defineWorkflow } from "./definition/workflow"; -import { action, onTrigger } from "./definition/primitives"; -import { projectLiveToInert } from "./live-inert-projector"; -import type { InertOnTrigger } from "./live-inert-projector"; - -function sectionWorkflow(onBodyFailure?: "end" | "continue") { - const body = defineWorkflow({ - id: "body", - triggers: [{ type: "manual" }], - steps: { reply: action({ handler: "reply" }) }, - }); - return defineWorkflow({ - id: "section-host", - steps: { - turn: onTrigger({ - on: { type: "mail", to: "section@example.test" }, - body, - ...(onBodyFailure !== undefined ? { onBodyFailure } : {}), - }), - }, - }); -} - -function projectedSection(onBodyFailure?: "end" | "continue"): InertOnTrigger { - const projected = projectLiveToInert(sectionWorkflow(onBodyFailure)); - const step = projected.steps["turn"]; - if (step === undefined || step.kind !== "onTrigger") { - throw new Error("expected a projected onTrigger section"); - } - return step; -} - -describe("live->inert projection of onTrigger.onBodyFailure", () => { - test("carries an explicit \"continue\" policy through the projection", () => { - expect(projectedSection("continue").onBodyFailure).toBe("continue"); - }); - - test("carries an explicit \"end\" policy through the projection", () => { - expect(projectedSection("end").onBodyFailure).toBe("end"); - }); - - test("omits the field entirely when the author set no policy", () => { - const section = projectedSection(); - expect(section.onBodyFailure).toBeUndefined(); - expect(Object.prototype.hasOwnProperty.call(section, "onBodyFailure")).toBe( - false, - ); - }); -}); diff --git a/vendor/intx/workflow/src/runlocal/index.ts b/vendor/intx/workflow/src/runlocal/index.ts index bcbec310d..8736ac374 100644 --- a/vendor/intx/workflow/src/runlocal/index.ts +++ b/vendor/intx/workflow/src/runlocal/index.ts @@ -1,5 +1,7 @@ export { runLocal, + createDefaultActionInvoker, + createInMemoryEffectLedger, type ActionHandler, type RunLocalOptions, } from "./run-local"; diff --git a/vendor/intx/workflow/src/runlocal/loop-iteration.ts b/vendor/intx/workflow/src/runlocal/loop-iteration.ts index 9ba9c6edd..ef52318fd 100644 --- a/vendor/intx/workflow/src/runlocal/loop-iteration.ts +++ b/vendor/intx/workflow/src/runlocal/loop-iteration.ts @@ -1,14 +1,28 @@ -// Loop-iteration host seam for runLocal. +// Loop-iteration host seam (shared by runLocal and the deployed child host). // // Runs one loop iteration's body as a child run against the SHARED store -// (the parent's repoStore + blobs + effects), with idempotency: a -// childRunId whose durable log is already terminal returns its recorded -// outputs without re-running. Because the body-ban forbids a loop body -// from suspending (awaitSignal/sleep/childWorkflow), a persisted child -// log is always terminal -- a mid-iteration crash drops the whole -// buffered segment, leaving an empty log this re-runs fresh. Sharing the -// blob substrate is load-bearing: a blob-spilled child output is only -// resolvable from the substrate that recorded it. +// (the parent's repoStore + blobs + effects). It resolves the child log to +// one of three states: +// - empty -> run the body fresh; +// - terminal -> idempotent replay: return the recorded outputs without +// re-running; +// - non-terminal -> throw (the caller fails the iteration). +// +// The body-ban forbids a loop body from SUSPENDING (awaitSignal / sleep / +// childWorkflow), so a body of purely-buffered steps flushes nothing until its +// terminal boundary: a mid-iteration crash drops the whole buffered segment, +// leaving an empty log this re-runs fresh. An ACTION body is the exception that +// makes the non-terminal case reachable: `runAction` flushes its `StepStarted` +// durably BEFORE invoking the handler, so a crash between the effect and the +// action's `StepCompleted` leaves a non-empty, non-terminal child log. On resume +// this hits the non-terminal throw -- the iteration fails loud and the handler +// is NOT re-invoked -- which is what gives an action-in-loop at-most-once +// semantics without a durable effect ledger (it rests on the store keeping the +// child's flushed `StepStarted` durable, i.e. child appends being as durable as +// the parent's -- an invariant the isogit store provides). +// +// Sharing the blob substrate is load-bearing: a blob-spilled child output is +// only resolvable from the substrate that recorded it. import { createNoopDrainController } from "../runtime/drain"; import type { diff --git a/vendor/intx/workflow/src/runlocal/run-local.ts b/vendor/intx/workflow/src/runlocal/run-local.ts index d141038c0..9fee7cb9f 100644 --- a/vendor/intx/workflow/src/runlocal/run-local.ts +++ b/vendor/intx/workflow/src/runlocal/run-local.ts @@ -189,7 +189,7 @@ export type ActionHandler = ( * `createNoopSpawnChild`: a silent stub would let action workflows pass * tests against effects that never ran. */ -function createDefaultActionInvoker( +export function createDefaultActionInvoker( authorize: WorkflowAuthorizeFn, effects: EffectLedger, resolver: ((ref: string) => ActionHandler) | undefined, @@ -213,7 +213,7 @@ function createDefaultActionInvoker( }; } -function createInMemoryEffectLedger(): EffectLedger { +export function createInMemoryEffectLedger(): EffectLedger { const store = new Map(); return { async lookup(effectKey) { diff --git a/vendor/intx/workflow/src/runtime/run.test.ts b/vendor/intx/workflow/src/runtime/run.test.ts deleted file mode 100644 index 4a5607bce..000000000 --- a/vendor/intx/workflow/src/runtime/run.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -// `onTrigger`'s non-fatal body-failure edge (`onBodyFailure`). -// -// These are unit tests of the runtime state machine, not the sidecar -// wiring: `runtimeRun` is exercised directly against a hand-built -// `WorkflowRuntimeEnv` (the same in-memory pieces `runLocal` wires, -// plus a fake `spawnSuspendableChild` this file controls per -// `childRunId`) rather than through `runLocal`, which does not expose a -// `spawnSuspendableChild` override. No real agent or substrate is -// involved -- this is the "runtime/run.test.ts" the discipline comment -// in `runlocal/run-local.ts` names as the intended home for this -// coverage. - -import { describe, test, expect } from "bun:test"; - -import { createDefaultDirectorRegistry } from "@intx/agent"; - -import type { OnTriggerPrimitive } from "../definition/primitives"; -import type { WorkflowDefinition } from "../definition/workflow"; -import { createInMemoryBlobSubstrate } from "../runlocal/blob-substrate"; -import { createInMemoryRepoStore } from "../runlocal/repo-store"; -import { createInMemoryScheduler } from "../runlocal/scheduler"; -import { createInMemorySignalChannel } from "../runlocal/signal-channel"; -import { createNoopDrainController } from "./drain"; -import { runtimeRun } from "./run"; -import type { - SpawnSuspendableChild, - SuspendableChildHandle, - WorkflowRuntimeEnv, -} from "./env"; -import { - controlParkKindOf, - resumeFromLog, - type RunState, - type WorkflowEvent, -} from "../state-machine/index"; - -const SECTION_ID = "section"; - -function definitionWith( - onTriggerOverrides: Partial = {}, -): WorkflowDefinition { - const primitive: OnTriggerPrimitive = { - kind: "onTrigger", - id: SECTION_ID, - on: { type: "manual" }, - body: { ref: "test-body" }, - ...onTriggerOverrides, - }; - return { - id: "wf-onbodyfailure", - triggers: [{ type: "manual" }], - steps: { [SECTION_ID]: primitive }, - stepOrder: [SECTION_ID], - }; -} - -type TerminalStatus = "completed" | "failed" | "cancelled"; - -/** - * A fake `spawnSuspendableChild` keyed by `childRunId`, each occurrence - * settling immediately on the terminal status the test scripted for it. - * `resume`/`deliverSignal` are unused by every scenario here (no body - * ever parks) so they throw if called, matching the pattern - * `apps/sidecar/test/workflow-substrate-factory-suspendable-child.test.ts` - * uses for handle members a scenario does not exercise. - */ -function fakeSpawn( - responses: Record, -): { spawn: SpawnSuspendableChild; spawnedChildRunIds: string[] } { - const spawnedChildRunIds: string[] = []; - const spawn: SpawnSuspendableChild = async ({ childRunId }) => { - spawnedChildRunIds.push(childRunId); - const terminalStatus = responses[childRunId]; - if (terminalStatus === undefined) { - throw new Error(`fakeSpawn: no scripted response for ${childRunId}`); - } - let delivered = false; - const handle: SuspendableChildHandle = { - async next() { - if (delivered) { - throw new Error( - `fakeSpawn: ${childRunId} next() called more than once`, - ); - } - delivered = true; - return { kind: "terminal", terminalStatus }; - }, - async resume() { - throw new Error(`fakeSpawn: ${childRunId} unexpected resume()`); - }, - async deliverSignal() { - throw new Error(`fakeSpawn: ${childRunId} unexpected deliverSignal()`); - }, - }; - return handle; - }; - return { spawn, spawnedChildRunIds }; -} - -function buildEnv(spawn: SpawnSuspendableChild): WorkflowRuntimeEnv { - const repoStore = createInMemoryRepoStore(); - const clock = () => new Date(); - let idCounter = 0; - const newId = (prefix: string): string => { - idCounter += 1; - return `${prefix}-${String(idCounter)}`; - }; - const definitionForDrain = definitionWith(); - return { - repoStore, - scheduler: createInMemoryScheduler({ repoStore, clock }), - signalChannel: createInMemorySignalChannel({ newId: () => newId("sig") }), - blobs: createInMemoryBlobSubstrate(), - directors: createDefaultDirectorRegistry(), - authorize: async () => ({ - effect: "allow", - matchingGrants: [], - resolvedBy: null, - }), - invokeStep: async () => { - throw new Error("no step primitive is exercised by these tests"); - }, - spawnChild: async () => { - throw new Error("no childWorkflow primitive is exercised by these tests"); - }, - spawnSuspendableChild: spawn, - clock, - newId, - drain: createNoopDrainController(definitionForDrain), - }; -} - -async function readState( - env: WorkflowRuntimeEnv, - runId: string, -): Promise { - const events = await env.repoStore.read(runId); - return resumeFromLog(runId, events); -} - -/** Poll the durable log until `predicate` holds. In-memory, so this settles fast. */ -async function waitFor( - env: WorkflowRuntimeEnv, - runId: string, - predicate: (state: RunState) => boolean, -): Promise { - for (let attempt = 0; attempt < 1000; attempt += 1) { - const state = await readState(env, runId); - if (predicate(state)) return state; - await new Promise((resolve) => setTimeout(resolve, 0)); - } - throw new Error("waitFor: predicate never became true"); -} - -function inputParkName(state: RunState): string | undefined { - const container = state.steps.get(SECTION_ID); - if (container === undefined || container.phase !== "awaiting-signal") { - return undefined; - } - if (container.awaitingSignal === undefined) return undefined; - if (controlParkKindOf(container.awaitingSignal) !== "input") return undefined; - return container.awaitingSignal.name; -} - -describe("onTrigger onBodyFailure", () => { - test("default policy: a failed body run ends the whole section run", async () => { - const { spawn, spawnedChildRunIds } = fakeSpawn({ - "section__0": "failed", - }); - const env = buildEnv(spawn); - const definition = definitionWith(); // no onBodyFailure -- default "end" - - const run = runtimeRun(definition, env, { triggerPayload: {} }); - const result = await run.complete; - - expect(result.terminalStatus).toBe("failed"); - // The failed occurrence is still durably recorded before the throw. - const events = await env.repoStore.read(run.runId); - const childCompleted = events.find( - (e): e is WorkflowEvent & { kind: "ChildCompleted" } => - e.kind === "ChildCompleted", - ); - expect(childCompleted?.terminalStatus).toBe("failed"); - // The section never re-arms for a second occurrence under the default. - expect(spawnedChildRunIds).toEqual(["section__0"]); - }); - - test('onBodyFailure: "continue" keeps the section alive through a failed occurrence', async () => { - const { spawn } = fakeSpawn({ - "section__0": "failed", - "section__1": "completed", - }); - const env = buildEnv(spawn); - const definition = definitionWith({ onBodyFailure: "continue" }); - - const run = runtimeRun(definition, env, { triggerPayload: { n: 0 } }); - - // The run does not settle terminal after occurrence 0 fails -- it - // re-arms on the input park instead. - const afterFirstFailure = await waitFor(env, run.runId, (state) => - inputParkName(state) !== undefined, - ); - expect(afterFirstFailure.phase).not.toBe("failed"); - expect(afterFirstFailure.children.get("section__0")?.terminalStatus).toBe( - "failed", - ); - - // The failed occurrence's ChildCompleted is a durable, loud audit event - // on the run's own log -- the section did not silently swallow it. - const eventsAfterFirstFailure = await env.repoStore.read(run.runId); - const childCompleted = eventsAfterFirstFailure.find( - (e): e is WorkflowEvent & { kind: "ChildCompleted" } => - e.kind === "ChildCompleted" && e.childRunId === "section__0", - ); - expect(childCompleted).toBeDefined(); - expect(childCompleted?.terminalStatus).toBe("failed"); - - const parkName = inputParkName(afterFirstFailure); - if (parkName === undefined) throw new Error("expected an input park"); - await run.signal(parkName, { n: 1 }); - - // Occurrence 1 spawns and succeeds normally -- the run proceeds, it - // does not throw. - await waitFor( - env, - run.runId, - (state) => state.children.get("section__1")?.terminalStatus === "completed", - ); - - await run.cancel("self", "test cleanup"); - const result = await run.complete; - expect(result.terminalStatus).toBe("cancelled"); - }); - - test('onBodyFailure: "continue" never swallows a cancelled body run', async () => { - const { spawn } = fakeSpawn({ - "section__0": "cancelled", - }); - const env = buildEnv(spawn); - const definition = definitionWith({ onBodyFailure: "continue" }); - - const run = runtimeRun(definition, env, { triggerPayload: {} }); - const result = await run.complete; - - // A cancelled body run still throws terminal-is-final -- it lands the - // section's own step as StepFailed, so the whole run's terminalStatus - // is "failed" (a thrown primitive error, not a run-level cancel); - // what matters here is that `onBodyFailure` did NOT swallow it into a - // re-arm the way it does for "failed". - expect(result.terminalStatus).toBe("failed"); - const events = await env.repoStore.read(run.runId); - const message = events.find( - (e): e is WorkflowEvent & { kind: "StepFailed" } => e.kind === "StepFailed", - )?.error.message; - expect(message).toContain("cancelled"); - }); - - test("crash-recovery honors onBodyFailure: a failed-but-continuing section resumes on the input re-arm", async () => { - // Hand-built seed log: the container's mid-flight state right after - // occurrence 0's ChildCompleted{failed} commits, but BEFORE the - // re-arm park lands -- the exact crash window `planOnTriggerResume`'s - // ordering comment describes. Built by hand (rather than captured off - // a live run) so the test is deterministic about which side of that - // race it exercises. - const definition = definitionWith({ onBodyFailure: "continue" }); - const seedLog: WorkflowEvent[] = [ - { - kind: "RunStarted", - seq: 1, - at: "2026-01-01T00:00:00.000Z", - runId: "resume-test", - definitionHash: "seed-hash", - trigger: { type: "manual", payload: {} }, - }, - { - kind: "StepStarted", - seq: 2, - at: "2026-01-01T00:00:00.000Z", - stepId: SECTION_ID, - attempt: 1, - input: { ref: "unused-input-ref" }, - }, - { - kind: "ChildSpawned", - seq: 3, - at: "2026-01-01T00:00:00.000Z", - stepId: SECTION_ID, - childRunId: "section__0", - childDefinitionRef: "test-body", - }, - { - kind: "ChildCompleted", - seq: 4, - at: "2026-01-01T00:00:00.000Z", - childRunId: "section__0", - terminalStatus: "failed", - }, - ]; - - // Resume a fresh env from that seed log with the same policy. The - // resume path (`planOnTriggerResume`) must take the reawait-input - // arm, not terminal-is-final, so the section keeps going. - const resumeSpawn = fakeSpawn({ - "section__0": "failed", - "section__1": "completed", - }); - const resumeEnv = buildEnv(resumeSpawn.spawn); - const resumeRun = runtimeRun(definition, resumeEnv, { - runId: "resume-test", - resumeFromEvents: seedLog, - }); - - const afterResume = await waitFor(resumeEnv, resumeRun.runId, (state) => - inputParkName(state) !== undefined, - ); - // Resume did not re-spawn occurrence 0's body -- it recovered position - // from the log rather than throwing terminal-is-final. - expect(resumeSpawn.spawnedChildRunIds).toEqual([]); - const parkName = inputParkName(afterResume); - if (parkName === undefined) throw new Error("expected an input park"); - await resumeRun.signal(parkName, { n: 1 }); - - await waitFor( - resumeEnv, - resumeRun.runId, - (state) => - state.children.get("section__1")?.terminalStatus === "completed", - ); - - await resumeRun.cancel("self", "test cleanup"); - const result = await resumeRun.complete; - expect(result.terminalStatus).toBe("cancelled"); - }); -}); diff --git a/vendor/intx/workflow/src/runtime/run.ts b/vendor/intx/workflow/src/runtime/run.ts index 81931d51e..b8d0111bb 100644 --- a/vendor/intx/workflow/src/runtime/run.ts +++ b/vendor/intx/workflow/src/runtime/run.ts @@ -14,6 +14,7 @@ import type { ApprovalSnapshot, ControlParkKind } from "@intx/types/runtime"; import type { ActionPrimitive, AwaitSignalPrimitive, + BodyFailurePolicy, ChildWorkflowPrimitive, EscalationPrimitive, GatePrimitive, @@ -2080,27 +2081,23 @@ async function runOnTrigger( } await flush(env, runId); - if (terminalStatus === "cancelled") { - // Terminal-is-final, unconditionally: a cancelled body run always ends - // the section. Cancellation reflects a drain/operator decision, not a - // turn-level error, so `onBodyFailure` never swallows it. - throw new Error( - `onTrigger ${primitive.id} body run ${childRunId} ended cancelled`, - ); - } - if (terminalStatus === "failed" && primitive.onBodyFailure !== "continue") { - // Terminal-is-final (default): a failed body run ends the whole - // section run. Throwing lands the parent terminal via - // `runPrimitiveSafe`; the run does not relaunch. + // Terminal-is-final unless the section tolerates a body failure. A cancelled + // body always ends the section (a drain/operator decision, never tolerated); + // a failed body ends it only under the default `end` policy. Under + // `tolerate`, a failed body falls through to the re-arm below -- the + // ChildCompleted{failed} committed above still records the occurrence. + if ( + terminalStatus === "cancelled" || + (terminalStatus === "failed" && + bodyFailurePolicyOf(primitive) !== "tolerate") + ) { + // Throwing lands the parent terminal via `runPrimitiveSafe`; the run does + // not relaunch. throw new Error( - `onTrigger ${primitive.id} body run ${childRunId} ended failed`, + `onTrigger ${primitive.id} body run ${childRunId} ended ` + + `${terminalStatus}`, ); } - // terminalStatus is "completed", or "failed" with onBodyFailure: - // "continue" -- the failed occurrence is already recorded (the - // ChildCompleted commit above this block carries - // `terminalStatus: "failed"`, the run's durable audit event for it); fall - // through to the same re-arm every completed occurrence takes. // Re-arm: park on a fresh input channel for the next event. The park is // snapshot-less (`kind: "input"`); the run's owner delivers the next @@ -2376,6 +2373,15 @@ type OnTriggerResumePlan = terminalStatus: "failed" | "cancelled"; }; +/** + * The section's body-failure policy, defaulting an absent field to `"end"` + * (terminal-is-final). Single source for the default so the steady-state drive + * loop and the resume planner cannot drift. + */ +function bodyFailurePolicyOf(primitive: OnTriggerPrimitive): BodyFailurePolicy { + return primitive.onBodyFailure ?? "end"; +} + function planOnTriggerResume( primitive: OnTriggerPrimitive, state: RunState, @@ -2407,29 +2413,31 @@ function planOnTriggerResume( // is already owned -- a body that then completed is caught HERE (reawait- // input), not by the in-flight throw. Inverting the order would wrongly fail a // post-abandon-completed body. - if (child.terminalStatus === "cancelled") { - return { - kind: "terminal-is-final", - eventIndex, - terminalStatus: "cancelled", - }; - } + // Terminal-is-final unless the section tolerates a body failure (mirrors the + // steady-state drive loop). A cancelled body always ends; a failed body ends + // only under the default `end` policy. A tolerated failure falls through to + // the completed block below, which re-adopts the SAME input park a completed + // body does -- never a bare new arm (which would wedge the section). if ( - child.terminalStatus === "failed" && - primitive.onBodyFailure !== "continue" + child.terminalStatus === "cancelled" || + (child.terminalStatus === "failed" && + bodyFailurePolicyOf(primitive) !== "tolerate") ) { return { kind: "terminal-is-final", eventIndex, - terminalStatus: "failed", + terminalStatus: child.terminalStatus, }; } const container = state.steps.get(primitive.id); - if (child.terminalStatus === "completed" || child.terminalStatus === "failed") { - // The event's body finished -- completed, or failed with - // `onBodyFailure: "continue"`, both of which are "the event is over" -- - // the section is idle on its input re-arm. Re-adopt the durable input - // park if it was committed, else re-arm fresh. + if ( + child.terminalStatus === "completed" || + (child.terminalStatus === "failed" && + bodyFailurePolicyOf(primitive) === "tolerate") + ) { + // The event's body finished (completed, or failed under a `tolerate` + // policy); the section is idle on its input re-arm. Re-adopt the durable + // input park if it was committed, else re-arm fresh. if ( container !== undefined && container.phase === "awaiting-signal" && diff --git a/vendor/intx/workflow/tsconfig.json b/vendor/intx/workflow/tsconfig.json index 957eb78ef..d3ef86835 100644 --- a/vendor/intx/workflow/tsconfig.json +++ b/vendor/intx/workflow/tsconfig.json @@ -1,12 +1,12 @@ { "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], "compilerOptions": { "types": [ "bun" ] - }, - "include": [ - "src/**/*.ts", - "test/**/*.ts" - ] + } } From f8e4e2614ef63aef0046aaa5d75befdb73645b8e Mon Sep 17 00:00:00 2001 From: x Date: Fri, 28 Aug 2026 12:14:49 -0700 Subject: [PATCH 2/2] kill-dates: record only the trees this change re-vendors --- scripts/checks/kill-dates.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/checks/kill-dates.txt b/scripts/checks/kill-dates.txt index c2c29bbed..0d7902fc6 100644 --- a/scripts/checks/kill-dates.txt +++ b/scripts/checks/kill-dates.txt @@ -16,7 +16,7 @@ apps/sidecar | sawyer | 2026-09-19 vendor/intx/agent | sawyer | 2026-10-26 | d0d56d9f452b78f4b541ad8f4e89f975e8069446bb98f2c8097b90de4b020243 vendor/intx/db | sawyer | 2026-09-19 | 0a4cdb9a8a6ff19d5d4713cbc4f5cc9257aad839b1fa393b2026e6d5afd828b9 -vendor/intx/hub-api | sawyer | 2026-09-19 | 4ee1c6b69304cdc4097a50b79b0ce02505dbdc918a36bd679b6f797e61eb5370 +vendor/intx/hub-api | sawyer | 2026-09-19 | f93a383cb5d6acdf50a461b43e4c8991dbdf7e13d34a4e5598e556eaee66308b vendor/intx/hub-sessions | sawyer | 2026-09-19 | e3344253ed6f2b0998599f9f8712d4e9b0757895b31a3e7df6a245c2bb76af3c vendor/intx/inference | sawyer | 2026-10-26 | 77fec29b078e8d03e686747c70e6b62ac1fd1434db0fb2c1e12e84b6dc71465f vendor/intx/mail-memory | sawyer | 2026-10-26 | 9f3601a7fb22e2d1c63daa976f3afccbd79af2187c155a0080c0d60c82450b92