diff --git a/VENDORED.md b/VENDORED.md index 907185f7a..262cc7782 100644 --- a/VENDORED.md +++ b/VENDORED.md @@ -22,23 +22,23 @@ 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`: six modules byte-identical (`default-harness.ts`, `signing-keypair.ts`, `source-asset-delivery.ts`, `workflow-closure-apply.ts`, `workflow-probe-handler.ts`, `workflow-run-pack-restore.ts`), three near-verbatim (`atomic-write.ts` differs only by the repo-wide logger namespace), the rest (`index.ts`, `config.ts`, `tool-materialization.ts`, `step-agent-tools.ts`, `workflow-host-wiring/`, `workflow-substrate-factory/`, …) substantially rewritten, plus workbench-only modules. A living fork, not a frozen copy, so this row carries no tree hash. | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | 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-10-26 | `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; one local delta (CL-7190): `ToolBundle.beforeToolExtension` (`tool.ts`) and its composition into `ResolvedTools.beforeToolExtensions` (`agent.ts`), so a tool package can contribute its own suspend-capable extension without the reactor or director special-casing it by name; 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/harness` | `@intx/harness` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the connector reply drain (`driveConnectorReplies`, `ConnectorReplyDrain`, `AgentEventStream`; `11590e66`) the sidecar's warm mail loop drives; no local delta; retired by the next `@intx/harness` publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/hub-agent` | `@intx/hub-agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the `agentDir` path export (`927556de`) the sidecar's deploy-tree lookup uses, and its own `@intx/mail-memory`/`@intx/harness` pins must resolve the vendored copies; no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | -| `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the null-principal `resolveApproval` for policy-resolved decisions (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-10-26 | `check:killdates` | -| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the pack-acceptance fixes (`ownsWorkflowRunRepo`, `anchorAddressForPackSource`, `decideTerminalRunFlip`), the adopted deploy front + `sourceRef` (CL-6324), the wire-projection writer (CL-6324), malformed tool-call-name sanitization (CL-6478), the sealed-run terminal-status backfill (CL-6595), or the CL-7190 `registerSignalCorrelation` approval-only guard (fails loud on a future `SignalKind` this RPC has no persistence for, rather than silently mis-persisting one); retired when upstream absorbs the deltas | sawyer | 2026-10-26 | `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`); local deltas: `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); and CL-7190's `message_response` resume branch in `reactor.ts`'s `resumePendingOperation`/`timeoutMessageFor`, plus its `reactor.test.ts`/`testing/fakes.ts` regression harness (this package previously had zero tests); 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`); one local delta (CL-7190): `"message_response"` added to `signals.ts`'s `signalKinds`, alongside `signalKindToGateType`; 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) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the body-spawn authorize/credential/mail-part-reader threading and grants head-collapse (CL-6448) that let the fork run tool-bearing onTrigger bodies; retired when upstream absorbs the delta | sawyer | 2026-10-26 | `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`: six modules byte-identical (`default-harness.ts`, `signing-keypair.ts`, `source-asset-delivery.ts`, `workflow-closure-apply.ts`, `workflow-probe-handler.ts`, `workflow-run-pack-restore.ts`), three near-verbatim (`atomic-write.ts` differs only by the repo-wide logger namespace), the rest (`index.ts`, `config.ts`, `tool-materialization.ts`, `step-agent-tools.ts`, `workflow-host-wiring/`, `workflow-substrate-factory/`, …) substantially rewritten, plus workbench-only modules. A living fork, not a frozen copy, so this row carries no tree hash. | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | 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-10-26 | `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; one local delta (CL-7190): `ToolBundle.beforeToolExtension` (`tool.ts`) and its composition into `ResolvedTools.beforeToolExtensions` (`agent.ts`), so a tool package can contribute its own suspend-capable extension without the reactor or director special-casing it by name; 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/harness` | `@intx/harness` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the connector reply drain (`driveConnectorReplies`, `ConnectorReplyDrain`, `AgentEventStream`; `11590e66`) the sidecar's warm mail loop drives; no local delta; retired by the next `@intx/harness` publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/hub-agent` | `@intx/hub-agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 predates the `agentDir` path export (`927556de`) the sidecar's deploy-tree lookup uses, and its own `@intx/mail-memory`/`@intx/harness` pins must resolve the vendored copies; no local delta; retired by the next publish | sawyer | 2026-10-26 | `check:killdates` | +| `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the null-principal `resolveApproval` for policy-resolved decisions (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-10-26 | `check:killdates` | +| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the pack-acceptance fixes (`ownsWorkflowRunRepo`, `anchorAddressForPackSource`, `decideTerminalRunFlip`), the adopted deploy front + `sourceRef` (CL-6324), the wire-projection writer (CL-6324), malformed tool-call-name sanitization (CL-6478), the sealed-run terminal-status backfill (CL-6595), the CL-7190 `registerSignalCorrelation` approval-only guard (fails loud on a future `SignalKind` this RPC has no persistence for, rather than silently mis-persisting one), or the CL-7191 `UserMessageParams.correlationId` plumbing into `sendUserMessage`'s `headers.interchangeCorrelationId` (a plain chat message can now resolve a parked `message_response` gate); retired when upstream absorbs the deltas | sawyer | 2026-10-26 | `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`); local deltas: `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); and CL-7190's `message_response` resume branch in `reactor.ts`'s `resumePendingOperation`/`timeoutMessageFor`, plus its `reactor.test.ts`/`testing/fakes.ts` regression harness (this package previously had zero tests); 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`); one local delta (CL-7190): `"message_response"` added to `signals.ts`'s `signalKinds`, alongside `signalKindToGateType`; 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) @ `a8bc06ae` (origin/main, 2026-08-27) | npm 0.3.0 covers the base package but not the body-spawn authorize/credential/mail-part-reader threading and grants head-collapse (CL-6448) that let the fork run tool-bearing onTrigger bodies; retired when upstream absorbs the delta | sawyer | 2026-10-26 | `check:killdates` | The pin is `a8bc06ae` (upstream `origin/main`, 2026-08-27), 72 commits past the `v0.3.0` release tag `b5580a02`. npm is still `0.3.0`, so every tree a diff --git a/packages/chat/src/platform-adapter.ts b/packages/chat/src/platform-adapter.ts index 8f15235bc..6989bd365 100644 --- a/packages/chat/src/platform-adapter.ts +++ b/packages/chat/src/platform-adapter.ts @@ -1214,6 +1214,9 @@ export function createHubChatPlatform( domain, content: input.content.content, cryptoProvider, + ...(input.correlationId !== undefined + ? { correlationId: input.correlationId } + : {}), }; const withAttachments = attachments !== undefined diff --git a/packages/chat/src/platform-port.ts b/packages/chat/src/platform-port.ts index ecf525a75..a05df780a 100644 --- a/packages/chat/src/platform-port.ts +++ b/packages/chat/src/platform-port.ts @@ -174,6 +174,13 @@ export interface WorkbenchMail { * a principal address has no mailbox: a reply to it vanishes. */ readonly fromWorkbenchId?: string; + /** + * The MIME `Interchange-Correlation-ID` header to stamp on this mail + * (`vendor/intx/mime/src/mail-builder.ts` already accepts this as + * `opts.correlationId`) — set when this message is an answer that must + * resolve a specific parked `message_response` gate, absent otherwise. + */ + readonly correlationId?: string; }): Promise; fetchBlob(workbenchId: string, blobId: string): Promise; diff --git a/packages/chat/src/routes.ts b/packages/chat/src/routes.ts index 94656ae36..d38fed2cb 100644 --- a/packages/chat/src/routes.ts +++ b/packages/chat/src/routes.ts @@ -2269,6 +2269,13 @@ export function createChatRoutes(deps: CreateChatRoutesDeps): Hono { senderAddress: senderAddressOf(c), workbenchId, messageParts: [{ kind: "text", text: payload.answer }], + // A question block's `blockId` IS its `questionId` + // (`ask_user`'s `postQuestion` mints one id and reuses it as + // both), which is also the `message_response` gate's own + // correlationId (`beforeAskUser`, `@corbits/interaction-tools`) + // — so this is the exact id that resolves the gate this answer + // is for, not merely "whichever gate is next". + correlationId: blockId, }, ); deps.onMessageFanout?.(answer.fanoutDelivered); diff --git a/packages/chat/src/turn-queue.ts b/packages/chat/src/turn-queue.ts index e362b6301..d5b921fbf 100644 --- a/packages/chat/src/turn-queue.ts +++ b/packages/chat/src/turn-queue.ts @@ -41,6 +41,10 @@ export type QueuedTurn = { readonly principalId: string; readonly recipients: readonly string[]; readonly parts: readonly PartType[]; + /** The id an answer must carry to resolve the `message_response` gate it + * answers (see `SendWorkbenchMessageInput.correlationId` in + * `./workbench-service.ts`). Absent for every ordinary message. */ + readonly correlationId?: string; }; export type WorkbenchTurnQueueDeps = { diff --git a/packages/chat/src/workbench-service.ts b/packages/chat/src/workbench-service.ts index 31a2c613f..899bf0f32 100644 --- a/packages/chat/src/workbench-service.ts +++ b/packages/chat/src/workbench-service.ts @@ -1381,6 +1381,16 @@ export type SendWorkbenchMessageInput = { * into the run the room already has. */ readonly forcedRecipientAddress?: string; + /** + * The id an answer must carry to resolve the gate it answers — a + * `message_response` gate's own correlationId (`@corbits/interaction-tools`'s + * `beforeAskUser`), threaded down to `headers.interchangeCorrelationId` + * on the InboundMessage the reactor's `tryCorrelate` reads. Sourced from + * the answered block's own id for a question response + * (`packages/chat/src/routes.ts`'s blocks/responses route); absent for + * every ordinary message, which carries no correlation at all. + */ + readonly correlationId?: string; }; export type SendWorkbenchMessageResult = { @@ -1620,6 +1630,9 @@ async function routeToRecipients( principalId: input.principalId, recipients, parts: turnParts, + ...(input.correlationId !== undefined + ? { correlationId: input.correlationId } + : {}), }, (batch) => dispatchTurnBatch(deps, input.tenantId, input.workbenchId, batch), @@ -1664,6 +1677,18 @@ async function dispatchTurnBatch( const last = batch[batch.length - 1]; if (last === undefined) return; const messageIds = batch.map((turn) => turn.messageId); + // An answer's correlationId must survive batching regardless of where in + // the batch it landed — unlike `principalId`, which is legitimately + // "whoever sent last," a gate answer is a specific message a specific + // queued turn carries, not necessarily the batch's final one (a further + // unrelated message queued behind the answer, before this batch drains, + // would otherwise make `last.correlationId` undefined and silently strand + // the gate on its timeout instead of resolving it). At most one queued + // turn in a batch carries a correlationId in practice — a batch answering + // more than one live question is not a shape this dispatch produces. + const batchCorrelationId = batch.find( + (turn) => turn.correlationId !== undefined, + )?.correlationId; // CL-6644: unconditional entry marker — see the matching note on the // caller's own recipient-resolution log. This is the one line that @@ -1730,6 +1755,15 @@ async function dispatchTurnBatch( agentAddress, parts, requestMessageIds: messageIds, + // A batch concatenating more than one queued message's parts + // still stamps the whole combined body as the answer when any + // one of them carries a correlationId — acceptable (the user + // did answer), but a batch mixing the actual answer with an + // unrelated follow-up hands the gate the whole blob, not just + // the answer. + ...(batchCorrelationId !== undefined + ? { correlationId: batchCorrelationId } + : {}), }), turnDispatchTimeoutMs, turnDispatchTimeoutMessage(agentAddress, turnDispatchTimeoutMs), @@ -1774,6 +1808,9 @@ export type DispatchTurnInput = { readonly parts: PartType[]; /** The room messages this turn answers, in arrival order. */ readonly requestMessageIds: readonly string[]; + /** See `SendWorkbenchMessageInput.correlationId`; threaded straight + * through to `WorkbenchMail.sendMail`. */ + readonly correlationId?: string; }; /** @@ -1812,6 +1849,9 @@ export async function dispatchTurn( principalId: input.principalId, content: encodeParts(input.parts, { replyTo: input.workbenchId }), fromWorkbenchId: input.workbenchId, + ...(input.correlationId !== undefined + ? { correlationId: input.correlationId } + : {}), }); } catch (err) { if (turn !== undefined) { diff --git a/packages/chat/test/block-responses-routes.test.ts b/packages/chat/test/block-responses-routes.test.ts index 327dc07de..93c31b257 100644 --- a/packages/chat/test/block-responses-routes.test.ts +++ b/packages/chat/test/block-responses-routes.test.ts @@ -14,6 +14,7 @@ import { createWorkbench, fakePlatform, mountAs, + settleFanout, TENANT, timelineEvents, timelineOf, @@ -306,6 +307,116 @@ describe("block response routes — question answers", () => { }); }); + test("answering a question stamps the answer's mail with the block's own id as its correlationId", async () => { + const platform = fakePlatform(); + const deps = buildDeps({ + platform, + blockResponses: createInMemoryBlockResponseStore(), + }); + const app = mountAs(createChatRoutes(deps), "prn_alice"); + const { body: workbench } = await createWorkbench(app, { + kind: "workbench", + participants: ["ins_echo1@acme.example"], + }); + + const post = await app.request( + responsesUrl(workbench.id, "m1", "blk_question1"), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ kind: "question", answer: "Staging" }), + }, + ); + expect(post.status).toBe(200); + await settleFanout(); + + // The block's own id (`blk_question1`, the same id `postQuestion` + // mints as `questionId`) rides as the answer mail's correlationId — + // the exact id `tryCorrelate` (`vendor/intx/inference/src/reactor.ts`) + // needs to resolve the `message_response` gate this answers, rather + // than "whichever gate is next". + expect(platform.sentMail).toHaveLength(1); + expect(platform.sentMail[0]?.correlationId).toBe("blk_question1"); + }); + + test("a question's correlationId survives batching even when it isn't the batch's last queued message", async () => { + // A held first dispatch forces the answer (queued 2nd) and a further + // plain follow-up (queued 3rd, landing last) into one batched turn — + // proving the batch's correlationId comes from whichever queued turn + // carries one, not from `batch[batch.length - 1]`, which here is the + // follow-up and carries none. + let releaseHold: () => void = () => {}; + const held = new Promise((resolve) => { + releaseHold = resolve; + }); + let resolveFirstDispatchStarted: () => void = () => {}; + const firstDispatchStarted = new Promise((resolve) => { + resolveFirstDispatchStarted = resolve; + }); + let holdConsumed = false; + + const platform = fakePlatform(); + const deliverMail = platform.sendMail.bind(platform); + platform.sendMail = async (input) => { + if (!holdConsumed) { + holdConsumed = true; + resolveFirstDispatchStarted(); + await held; + } + return deliverMail(input); + }; + + const deps = buildDeps({ + platform, + blockResponses: createInMemoryBlockResponseStore(), + }); + const app = mountAs(createChatRoutes(deps), "prn_alice"); + const { body: workbench } = await createWorkbench(app, { + kind: "workbench", + participants: ["ins_echo1@acme.example"], + }); + + // Message 1 dispatches immediately and is held open. + const first = await app.request(`/workbenches/${workbench.id}/messages`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ parts: [{ kind: "text", text: "hello" }] }), + }); + expect(first.status).toBe(201); + await firstDispatchStarted; + + // Queued 2nd, while the first dispatch is still held: the answer, + // carrying the block's id as its correlationId. + const post = await app.request( + responsesUrl(workbench.id, "m1", "blk_question1"), + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ kind: "question", answer: "Staging" }), + }, + ); + expect(post.status).toBe(200); + + // Queued 3rd, landing last in the batch: an unrelated follow-up with + // no correlationId of its own. + const third = await app.request(`/workbenches/${workbench.id}/messages`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + parts: [{ kind: "text", text: "also, one more thing" }], + }), + }); + expect(third.status).toBe(201); + + releaseHold(); + await settleFanout(); + + // First dispatch (message 1), then one batched dispatch covering + // both the answer and the follow-up. + expect(platform.sentMail).toHaveLength(2); + expect(platform.sentMail[1]?.correlationId).toBe("blk_question1"); + }); + test("a question response without answer text is rejected", async () => { const deps = buildDeps({ blockResponses: createInMemoryBlockResponseStore(), diff --git a/packages/chat/test/test-support.ts b/packages/chat/test/test-support.ts index cf2b8c438..930a29649 100644 --- a/packages/chat/test/test-support.ts +++ b/packages/chat/test/test-support.ts @@ -83,6 +83,7 @@ export function fakePlatform( principalId?: string; content: MailContent; fromWorkbenchId?: string; + correlationId?: string; }[]; launchInviteCalls: { tenantId: string; @@ -96,6 +97,7 @@ export function fakePlatform( principalId?: string; content: MailContent; fromWorkbenchId?: string; + correlationId?: string; }[] = []; const launchInviteCalls: { tenantId: string; @@ -172,10 +174,14 @@ export function fakePlatform( input.principalId !== undefined ? { ...sentMailEntryBase, principalId: input.principalId } : sentMailEntryBase; - sentMail.push( + const withFromWorkbench = input.fromWorkbenchId !== undefined ? { ...withPrincipal, fromWorkbenchId: input.fromWorkbenchId } - : withPrincipal, + : withPrincipal; + sentMail.push( + input.correlationId !== undefined + ? { ...withFromWorkbench, correlationId: input.correlationId } + : withFromWorkbench, ); const id = `mail_${++mailCounter}`; const createdAt = new Date().toISOString(); diff --git a/packages/folded-runs/src/mail.ts b/packages/folded-runs/src/mail.ts index d1e583944..a6cb1d259 100644 --- a/packages/folded-runs/src/mail.ts +++ b/packages/folded-runs/src/mail.ts @@ -25,6 +25,9 @@ export type SendFoldedMailParams = { attachments?: MessageAttachment[]; replyTo?: string; cryptoProvider: CryptoProvider; + /** The MIME `Interchange-Correlation-ID` header — set when this message + * answers a specific parked `message_response` gate, absent otherwise. */ + correlationId?: string; }; /** @@ -50,6 +53,9 @@ async function deliverFoldedMailMIME( sessionId: params.sessionId, tenantId: params.tenantId, cryptoProvider: params.cryptoProvider, + ...(params.correlationId !== undefined + ? { correlationId: params.correlationId } + : {}), }; const withAttachments = params.attachments !== undefined diff --git a/packages/interaction-tools/package.json b/packages/interaction-tools/package.json index 8223d7b2f..d52be8cc1 100644 --- a/packages/interaction-tools/package.json +++ b/packages/interaction-tools/package.json @@ -2,7 +2,7 @@ "name": "@corbits/interaction-tools", "private": true, "description": "The ask_user tool: an @intx/agent bundle that poses an interview question in-thread as a question block and parks the turn on a message_response gate until the user answers", - "version": "0.0.3", + "version": "0.0.4", "license": "LGPL-2.1-or-later", "type": "module", "exports": { diff --git a/packages/interaction-tools/src/client.ts b/packages/interaction-tools/src/client.ts index e88eb75c8..20695654a 100644 --- a/packages/interaction-tools/src/client.ts +++ b/packages/interaction-tools/src/client.ts @@ -57,12 +57,16 @@ const PostedMessageResponse = type({ id: "string", createdAt: "string" }); /** * Posts a `question` block into the caller's own channel. Mints the * block's `questionId` here (never trusts the model to supply a stable, - * collision-free id) and returns it, since `ask_user`'s tool result names - * it so a caller can correlate a later answer, and the route persists - * responses keyed by `(messageId, blockId)`. `@intx/hub-common`'s - * `generateId` is a closed enum of platform id kinds (vendored, read-only - * source) with no "question" entry, so this mints its own `q_`-prefixed - * id the same way `packages/chat/src/threads.ts`'s `thr_` ids do. + * collision-free id) and returns it: `beforeAskUser` (`./tool.ts`) reuses + * it verbatim as the `message_response` gate's own `correlationId`, and + * the block-response route persists (and later relays) an answer keyed on + * this same id as `blockId` — a question block's `blockId` IS its + * `questionId`, the same way a poll's is its `pollId` + * (`packages/chat/src/schema.ts`'s `block_responses` table comment). + * `@intx/hub-common`'s `generateId` is a closed enum of platform id kinds + * (vendored, read-only source) with no "question" entry, so this mints its + * own `q_`-prefixed id the same way `packages/chat/src/threads.ts`'s + * `thr_` ids do. */ export async function postQuestion( config: AskUserClientConfig, diff --git a/scripts/checks/kill-dates.txt b/scripts/checks/kill-dates.txt index 5740abcdd..bc46993fd 100644 --- a/scripts/checks/kill-dates.txt +++ b/scripts/checks/kill-dates.txt @@ -19,7 +19,7 @@ vendor/intx/db | sawyer | 2026-10-26 | 0a4cdb9a8a6ff19d5d4713cbc4f5cc9257aad839b vendor/intx/harness | sawyer | 2026-10-26 | 867f2b0eb4a360c68bf552d5b95a9530411e2c1a2d046d1f5ce718d4a9be36a8 vendor/intx/hub-agent | sawyer | 2026-10-26 | 30d5050511f22bc73b3c5d34728dfdf5b791de203452b83d4333a1dc762afceb vendor/intx/hub-api | sawyer | 2026-10-26 | 42ee33e027559b236065382cb94f393bbcfee69625615894f82be778f34f7aa1 -vendor/intx/hub-sessions | sawyer | 2026-10-26 | 19fcc2c0bf6af1dd9a1b267513bea777968c454a323cc402fc5ec1cca8097523 +vendor/intx/hub-sessions | sawyer | 2026-10-26 | 79a6f534a5166dfc5904d1b82214d1a0f857f2b2bde0b845115e0618f84745f2 vendor/intx/inference | sawyer | 2026-10-26 | f32792856f555b0fed40ce75badde7630a45a6452ad20cfc61058192f8376f11 vendor/intx/mail-memory | sawyer | 2026-10-26 | 9f3601a7fb22e2d1c63daa976f3afccbd79af2187c155a0080c0d60c82450b92 vendor/intx/mailbox | sawyer | 2026-10-26 | d36d7ffcc32018571276e4922a8c2714b7ee0bb5deb80b01e73859245975d4c6 diff --git a/vendor/intx/hub-sessions/VENDORED-FROM b/vendor/intx/hub-sessions/VENDORED-FROM index 0f7d6a4aa..d36678b34 100644 --- a/vendor/intx/hub-sessions/VENDORED-FROM +++ b/vendor/intx/hub-sessions/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/hub-sessions) 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. Terminal-anchor pack acceptance: hub-session-lookups.ts's receiveWorkflowRunPack gates the anchor lookup on the exported pure helper ownsWorkflowRunRepo (self-anchored row with a routable address, no liveness requirement) instead of upstream's `status in (deployed, running)`, so a terminal run can still land the inbox-enqueue and markConsumed-rejection packs that retire mail arriving in its teardown window; CL-6361 widens the same lookup to peel a per-step pack source address back to its base run's anchor via anchorAddressForPackSource; CL-6379 classifies an accepted pack's newly-terminal runs through decideTerminalRunFlip so a section occurrence's repo-local child run (turn__) is skipped quietly. CL-6324: a third code-sourced deploy front, deployAdoptedCodeSourcedWorkflow (plus the deployAdoptedWorkflowFromSource service method and its AdoptingWorkflowDeployer / DeployAdoptedWorkflowFromSourceParams types), deploys onto shared capacity while ADOPTING an anchor workflow_run row the caller already owns; it composes upstream's emitSourceRefDeployFrame and a guarded UPDATE. DeployWorkflowFromSourceParams / DeployPreparedCodeSourcedWorkflowParams gain an optional sourceRef threaded through bindAssetAttachmentResolver / bindSourceAttachmentResolver so a per-run source tree on refs/heads/runs/ packs the pinned commit. CL-6324: workflow-probe-gate.ts's PersistFrozenApprovalFn carries the inert projection and createDbFrozenApprovalWriter stamps workflow_definition_version.wire_projection in the same transaction as approved_wire_hash. CL-6478: sanitize-tool-name.ts + event-collector.ts's tool_call case persist only a tool-call name encodeToolName can re-invert (anything else collapses to MALFORMED_TOOL_NAME), so one bad name fails its turn instead of wedging the room; @intx/inference is a dependency for this. CL-6595: workflow-run-kind.ts's validatePush also reports a run sealed from birth (combined events.jsonl with no per-event blobs) as newly terminal, and the new readCommittedWorkflowRunTerminalStatus export backs a same-push markTerminal backfill in hub-session-lookups.ts; both classify through upstream's classifyTerminalEvent. Retired at this pin: the per-event inference.usage forward (upstream fab86ca9 emits TurnUsage once per turn), the collector-level event serialization (upstream a1d419c3 serializes at the registry) and the anchor-before-frame ordering with DeployFrameNotSentError (upstream a203a057 + 9e11829f, isDeployFrameFailure). CL-7190: registerSignalCorrelation (hub-session-lookups.ts) now throws for any signal kind other than "approval", failing loud instead of silently mis-persisting a future SignalKind it has no co-write for. +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. Terminal-anchor pack acceptance: hub-session-lookups.ts's receiveWorkflowRunPack gates the anchor lookup on the exported pure helper ownsWorkflowRunRepo (self-anchored row with a routable address, no liveness requirement) instead of upstream's `status in (deployed, running)`, so a terminal run can still land the inbox-enqueue and markConsumed-rejection packs that retire mail arriving in its teardown window; CL-6361 widens the same lookup to peel a per-step pack source address back to its base run's anchor via anchorAddressForPackSource; CL-6379 classifies an accepted pack's newly-terminal runs through decideTerminalRunFlip so a section occurrence's repo-local child run (turn__) is skipped quietly. CL-6324: a third code-sourced deploy front, deployAdoptedCodeSourcedWorkflow (plus the deployAdoptedWorkflowFromSource service method and its AdoptingWorkflowDeployer / DeployAdoptedWorkflowFromSourceParams types), deploys onto shared capacity while ADOPTING an anchor workflow_run row the caller already owns; it composes upstream's emitSourceRefDeployFrame and a guarded UPDATE. DeployWorkflowFromSourceParams / DeployPreparedCodeSourcedWorkflowParams gain an optional sourceRef threaded through bindAssetAttachmentResolver / bindSourceAttachmentResolver so a per-run source tree on refs/heads/runs/ packs the pinned commit. CL-6324: workflow-probe-gate.ts's PersistFrozenApprovalFn carries the inert projection and createDbFrozenApprovalWriter stamps workflow_definition_version.wire_projection in the same transaction as approved_wire_hash. CL-6478: sanitize-tool-name.ts + event-collector.ts's tool_call case persist only a tool-call name encodeToolName can re-invert (anything else collapses to MALFORMED_TOOL_NAME), so one bad name fails its turn instead of wedging the room; @intx/inference is a dependency for this. CL-6595: workflow-run-kind.ts's validatePush also reports a run sealed from birth (combined events.jsonl with no per-event blobs) as newly terminal, and the new readCommittedWorkflowRunTerminalStatus export backs a same-push markTerminal backfill in hub-session-lookups.ts; both classify through upstream's classifyTerminalEvent. Retired at this pin: the per-event inference.usage forward (upstream fab86ca9 emits TurnUsage once per turn), the collector-level event serialization (upstream a1d419c3 serializes at the registry) and the anchor-before-frame ordering with DeployFrameNotSentError (upstream a203a057 + 9e11829f, isDeployFrameFailure). CL-7190: registerSignalCorrelation (hub-session-lookups.ts) now throws for any signal kind other than "approval", failing loud instead of silently mis-persisting a future SignalKind it has no co-write for. CL-7191: UserMessageParams gains an optional correlationId (session-service.ts) stamped as sendUserMessage's headers.interchangeCorrelationId, so a plain chat message can resolve a parked message_response gate. diff --git a/vendor/intx/hub-sessions/src/session-service.ts b/vendor/intx/hub-sessions/src/session-service.ts index 455bcf79a..03860dfd0 100644 --- a/vendor/intx/hub-sessions/src/session-service.ts +++ b/vendor/intx/hub-sessions/src/session-service.ts @@ -334,6 +334,11 @@ export type UserMessageParams = { sessionId: string; tenantId: string; cryptoProvider: CryptoProvider; + /** Stamped as the MIME `Interchange-Correlation-ID` header (CL-7191) so + * the reactor's `tryCorrelate` can resolve a parked `message_response` + * gate against this message rather than only ordering. Absent for every + * ordinary message. */ + correlationId?: string; }; export type SessionServiceDeps = { @@ -2281,6 +2286,7 @@ export function createSessionService( sessionId, tenantId, cryptoProvider, + correlationId, } = params; const headers: MessageHeaders = { @@ -2294,7 +2300,7 @@ export function createSessionService( references, mimeVersion: "1.0", interchangeType: "conversation.message", - interchangeCorrelationId: undefined, + interchangeCorrelationId: correlationId, interchangeTenantId: tenantId, interchangeAgentId: undefined, interchangeSessionId: sessionId, diff --git a/workflows/assistant/src/index.ts b/workflows/assistant/src/index.ts index 9174e295d..d9a6a36c4 100644 --- a/workflows/assistant/src/index.ts +++ b/workflows/assistant/src/index.ts @@ -52,7 +52,7 @@ export const ASSISTANT_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ { name: "@corbits/catalog-tools", version: "0.0.1" }, { name: "@corbits/skills-tools", version: "0.0.6" }, { name: "@corbits/mcp-tools", version: "0.0.10" }, - { name: "@corbits/interaction-tools", version: "0.0.3" }, + { name: "@corbits/interaction-tools", version: "0.0.4" }, { name: "@corbits/manus-tools", version: "0.0.11" }, ];