From b13f40d59a71713c2dd8838d710c8ec76b1c539c Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 10:00:50 -0700 Subject: [PATCH 1/3] Add tests for ask_user suspending on a message_response gate Covers the suspend/resume mechanism CL-7190 introduces: interaction-tools' own beforeToolExtension decision shape, and a new reactor-level regression suite (vendor/intx/inference had zero tests before this) driving a real createReactor cycle to prove a suspended tool call produces no tool.done and no re-inference, that a correlated reply clears the gate and drives exactly one more infer, and that two concurrent questions answered out of order each resolve their own gate. --- packages/interaction-tools/src/tool.test.ts | 109 +++++--- vendor/intx/inference/src/reactor.test.ts | 260 ++++++++++++++++++++ vendor/intx/inference/src/testing/fakes.ts | 209 ++++++++++++++++ 3 files changed, 540 insertions(+), 38 deletions(-) create mode 100644 vendor/intx/inference/src/reactor.test.ts create mode 100644 vendor/intx/inference/src/testing/fakes.ts diff --git a/packages/interaction-tools/src/tool.test.ts b/packages/interaction-tools/src/tool.test.ts index 31ac223778..a07f74f8ac 100644 --- a/packages/interaction-tools/src/tool.test.ts +++ b/packages/interaction-tools/src/tool.test.ts @@ -1,4 +1,6 @@ import { expect, test } from "bun:test"; +import type { ToolBundle } from "@intx/agent"; +import type { ToolCall } from "@intx/types/runtime"; import { ASK_USER_TOOL, interactionTools } from "./tool"; import type { AskUserEnv } from "./tool"; @@ -24,6 +26,16 @@ async function withFetch( } } +function beforeTool(bundle: ToolBundle, call: ToolCall) { + const extension = bundle.beforeToolExtension; + if (extension === undefined) { + throw new Error( + "interactionTools did not contribute a beforeToolExtension", + ); + } + return extension.beforeTool(call, {} as never, new AbortController().signal); +} + test("declares exactly ask_user, with no approval gate", () => { expect(interactionTools.definitions).toEqual([{ name: ASK_USER_TOOL }]); }); @@ -36,7 +48,7 @@ test("requires the sanctioned env keys", () => { ]); }); -test("ask_user posts a question block and returns immediately, never the answer", async () => { +test("ask_user posts a question block and suspends on a message_response gate", async () => { let posted = false; const fetchImpl = (async () => { posted = true; @@ -47,24 +59,29 @@ test("ask_user posts a question block and returns immediately, never the answer" }) as unknown as typeof fetch; const bundle = interactionTools(testEnv()); - const result = await withFetch(fetchImpl, () => - bundle.run( - { - id: "call_1", - name: ASK_USER_TOOL, - arguments: { - question: "Which environment?", - options: ["Staging", "Production"], - }, - }, - new AbortController().signal, - ), - ); + const call = { + id: "call_1", + name: ASK_USER_TOOL, + arguments: { + question: "Which environment?", + options: ["Staging", "Production"], + }, + }; + const before = Date.now(); + const decision = await withFetch(fetchImpl, () => beforeTool(bundle, call)); expect(posted).toBe(true); - expect(result.isError).toBe(false); - expect(result.content).not.toContain("Staging"); - expect(String(result.content)).toContain("next message"); + if (decision.type !== "suspend") { + throw new Error(`expected a suspend decision, got ${decision.type}`); + } + expect(decision.gate.type).toBe("message_response"); + expect(decision.gate.timeoutAt).toBeGreaterThan(before); + expect(decision.pendingOp.kind).toBe("message_response"); + expect(decision.pendingOp.suspendedCall).toEqual(call); + expect(decision.pendingOp.correlationId).toBe(decision.gate.correlationId); + // Minted by `postQuestion` (`q_`), not a separately-minted + // `crypto.randomUUID()` — the gate and the question card share one id. + expect(decision.gate.correlationId).toMatch(/^q_[0-9a-f]{32}$/); }); test("ask_user rejects fewer than 2 options before ever posting", async () => { @@ -75,22 +92,19 @@ test("ask_user rejects fewer than 2 options before ever posting", async () => { }) as unknown as typeof fetch; const bundle = interactionTools(testEnv()); - const result = await withFetch(fetchImpl, () => - bundle.run( - { - id: "call_1", - name: ASK_USER_TOOL, - arguments: { question: "Q?", options: ["only one"] }, - }, - new AbortController().signal, - ), + const decision = await withFetch(fetchImpl, () => + beforeTool(bundle, { + id: "call_1", + name: ASK_USER_TOOL, + arguments: { question: "Q?", options: ["only one"] }, + }), ); expect(posted).toBe(false); - expect(result.isError).toBe(true); + expect(decision.type).toBe("block"); }); -test("ask_user surfaces a no-own-channel failure as an error result, not a throw", async () => { +test("ask_user surfaces a no-own-channel failure as a blocked call, not a throw", async () => { const fetchImpl = (async () => new Response( JSON.stringify({ error: { code: "not_found", message: "no channel" } }), @@ -98,19 +112,38 @@ test("ask_user surfaces a no-own-channel failure as an error result, not a throw )) as unknown as typeof fetch; const bundle = interactionTools(testEnv()); - const result = await withFetch(fetchImpl, () => - bundle.run( - { - id: "call_1", - name: ASK_USER_TOOL, - arguments: { question: "Q?", options: ["a", "b"] }, - }, - new AbortController().signal, - ), + const decision = await withFetch(fetchImpl, () => + beforeTool(bundle, { + id: "call_1", + name: ASK_USER_TOOL, + arguments: { question: "Q?", options: ["a", "b"] }, + }), ); + if (decision.type !== "block") { + throw new Error(`expected a block decision, got ${decision.type}`); + } + expect(decision.reason).toContain("no channel"); +}); + +test("a call for another tool name is allowed through unsuspended", async () => { + const bundle = interactionTools(testEnv()); + const decision = await beforeTool(bundle, { + id: "call_1", + name: "some_other_tool", + arguments: {}, + }); + expect(decision).toEqual({ type: "allow" }); +}); + +test("run() is never the ask_user path: reaching it fails loud", async () => { + const bundle = interactionTools(testEnv()); + const result = await bundle.run( + { id: "call_1", name: ASK_USER_TOOL, arguments: {} }, + new AbortController().signal, + ); expect(result.isError).toBe(true); - expect(String(result.content)).toContain("no channel"); + expect(String(result.content)).toContain("beforeToolExtension"); }); test("an unknown tool name returns an honest error", async () => { diff --git a/vendor/intx/inference/src/reactor.test.ts b/vendor/intx/inference/src/reactor.test.ts new file mode 100644 index 0000000000..f4a7425f7b --- /dev/null +++ b/vendor/intx/inference/src/reactor.test.ts @@ -0,0 +1,260 @@ +// Regression coverage for the `message_response` gate mechanism CL-7190 +// introduces: a `BeforeToolExtension` returning `suspend` must park a tool +// call structurally — no `tool.done`, no re-inference — until a correlated +// inbound message resolves it, at which point exactly one re-inference +// follows. This is the reactor's own dispatch loop, driven for real (not a +// mocked decision shape), because the bug this guards against +// (CL-7190/CL-7191) lives in that loop: a mocked reactor passes on the +// broken code just as easily as the fixed one. +import { expect, test } from "bun:test"; +import type { + BeforeToolExtension, + ReactorCapabilities, + ReactorDirector, + ToolCall, +} from "@intx/types/runtime"; + +import { createReactor } from "./reactor"; +import type { ReactorEmittedEvent } from "./reactor"; +import { + assistantTurnWithToolCall, + buildInboundMessage, + createFakeInferenceSource, + createInMemoryContextStore, + createRecordingToolRunner, + createScriptedInferenceRunner, + createUnusedDependencies, +} from "./testing/fakes"; + +/** + * A director simple enough to have no opinions of its own: infer on a + * fresh message or a correlated answer landing in history, and turn an + * assistant turn's `tool_call` blocks into `execute_tools`. Everything + * else waits. This is deliberately NOT `WorkbenchDirector` or + * `DefaultDirector` — the point of this test is that the suspend/resume + * mechanism itself (owned by the reactor, `@intx/agent`, and whichever + * `BeforeToolExtension` a tool package contributes) needs no director + * cooperation to behave correctly; CL-7190's own bug lived in a director + * that mishandled this, but the fix does not depend on director logic. + */ +function createMinimalDirector(): ReactorDirector { + return { + decide(event, _state, capabilities: ReactorCapabilities) { + switch (event.type) { + case "message.received": + return Promise.resolve(capabilities.infer()); + case "inference.done": { + const calls: ToolCall[] = event.turn.content + .filter( + (block): block is Extract => + block.type === "tool_call", + ) + .map((block) => ({ + id: block.id, + name: block.name, + arguments: block.arguments, + })); + if (calls.length > 0) { + return Promise.resolve(capabilities.executeTools(calls, false)); + } + // No tool call named: an ordinary reply completed. `wait()` + // returns the reactor to idle for this message — `done()` would + // shut the whole reactor down, which a routine reply must not do. + return Promise.resolve(capabilities.wait()); + } + case "resume.tool_result": + return Promise.resolve(capabilities.infer()); + case "resume.execute_tools": + return Promise.resolve(capabilities.executeTools(event.calls)); + default: + return Promise.resolve(capabilities.wait()); + } + }, + }; +} + +/** Always suspends whatever call it's given on a `message_response` gate + * keyed by the call's own id — mirrors `@corbits/interaction-tools`' + * `beforeAskUser`, generically, with no name-based special-casing. */ +function alwaysSuspendExtension(): BeforeToolExtension { + return { + beforeTool: (call) => { + const timeoutAt = Date.now() + 60_000; + return Promise.resolve({ + type: "suspend", + gate: { + type: "message_response", + gateId: `pending-${call.id}`, + correlationId: call.id, + timeoutAt, + }, + pendingOp: { + correlationId: call.id, + kind: "message_response", + registeredAt: Date.now(), + gateId: `pending-${call.id}`, + timeoutAt, + suspendedCall: call, + }, + }); + }, + }; +} + +function collectEvents() { + const events: ReactorEmittedEvent[] = []; + return { events, onEvent: (e: ReactorEmittedEvent) => events.push(e) }; +} + +test("a suspended tool call produces no tool.done and no re-inference", async () => { + const call: ToolCall = { id: "call_1", name: "ask_user", arguments: {} }; + const { runner, callCount } = createScriptedInferenceRunner([ + assistantTurnWithToolCall(call), + ]); + const toolRunner = createRecordingToolRunner(); + const { events, onEvent } = collectEvents(); + + const reactor = createReactor({ + sessionId: "session-1", + director: createMinimalDirector(), + source: createFakeInferenceSource(), + toolRunner, + contextStore: createInMemoryContextStore(), + onEvent, + deps: createUnusedDependencies(), + inferenceRunner: runner, + beforeToolExtensions: [alwaysSuspendExtension()], + }); + + reactor.start(); + reactor.deliver(buildInboundMessage({ content: "What should I do?" })); + + // Let the async start()/deliver() chain settle: load -> message.received + // -> infer -> inference.done -> execute_tools -> suspend -> commit. + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(callCount()).toBe(1); + expect(toolRunner.calls).toEqual([]); + expect(events.some((e) => e.type === "tool.done")).toBe(false); + + const blocked = events.find((e) => e.type === "reactor.gate.blocked"); + expect(blocked).toBeDefined(); + expect( + (blocked as Extract) + .data.correlationId, + ).toBe(call.id); +}); + +test("a correlated reply clears the gate and drives exactly one more infer", async () => { + const call: ToolCall = { id: "call_2", name: "ask_user", arguments: {} }; + const { runner, callCount } = createScriptedInferenceRunner([ + assistantTurnWithToolCall(call), + assistantTurnWithToolCall({ id: "call_2b", name: "noop", arguments: {} }), + ]); + const toolRunner = createRecordingToolRunner(); + const { events, onEvent } = collectEvents(); + + const reactor = createReactor({ + sessionId: "session-2", + director: createMinimalDirector(), + source: createFakeInferenceSource(), + toolRunner, + contextStore: createInMemoryContextStore(), + onEvent, + deps: createUnusedDependencies(), + inferenceRunner: runner, + beforeToolExtensions: [alwaysSuspendExtension()], + }); + + reactor.start(); + reactor.deliver(buildInboundMessage({ content: "What should I do?" })); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(callCount()).toBe(1); + + reactor.deliver( + buildInboundMessage({ content: "Staging", correlationId: call.id }), + ); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(callCount()).toBe(2); + // The correlated reply resolves the parked call directly — it never + // becomes an ordinary `message.received` the director would infer twice + // over, and it never reaches `toolRunner.run` (there is nothing left to + // execute once the user has answered). + expect(events.filter((e) => e.type === "message.received").length).toBe(1); + expect(toolRunner.calls).toEqual([]); + expect(events.some((e) => e.type === "message.correlated")).toBe(true); +}); + +test("two concurrent questions answered out of order each resolve their own gate", async () => { + const callA: ToolCall = { id: "call_a", name: "ask_user", arguments: {} }; + const callB: ToolCall = { id: "call_b", name: "ask_user", arguments: {} }; + // Three inference cycles: the first two each surface one of the two + // ask_user calls (as if two questions were asked back to back before + // either was answered); the reactor parks each on its own gate keyed by + // its own call id, so no scripted turn is needed for a re-infer beyond + // this test's two correlated replies driving `resume.tool_result` + // directly rather than a fresh `inference.done`. + const plainReply = (text: string) => ({ + role: "assistant" as const, + content: [{ type: "text" as const, text }], + model: "fake-model", + timestamp: Date.now(), + }); + const { runner, callCount } = createScriptedInferenceRunner([ + assistantTurnWithToolCall(callA), + assistantTurnWithToolCall(callB), + // Each correlated answer below drives one more re-infer + // (`resume.tool_result` -> `capabilities.infer()`); neither reply names + // a further tool call, so the director's `inference.done` branch calls + // `capabilities.done()` and the run settles. + plainReply("got B's answer"), + plainReply("got A's answer"), + ]); + const toolRunner = createRecordingToolRunner(); + const { events, onEvent } = collectEvents(); + + const reactor = createReactor({ + sessionId: "session-3", + director: createMinimalDirector(), + source: createFakeInferenceSource(), + toolRunner, + contextStore: createInMemoryContextStore(), + onEvent, + deps: createUnusedDependencies(), + inferenceRunner: runner, + beforeToolExtensions: [alwaysSuspendExtension()], + }); + + reactor.start(); + reactor.deliver(buildInboundMessage({ content: "Question A?" })); + await new Promise((resolve) => setTimeout(resolve, 50)); + reactor.deliver(buildInboundMessage({ content: "Question B?" })); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(callCount()).toBe(2); + + // Answer B first, then A — reverse of arrival order. Cross-wired + // correlation would hand B's answer to A's parked call (or vice versa); + // asserting the resume.tool_result event body proves each answer lands + // on its own call id. + reactor.deliver( + buildInboundMessage({ content: "Answer to B", correlationId: callB.id }), + ); + await new Promise((resolve) => setTimeout(resolve, 50)); + reactor.deliver( + buildInboundMessage({ content: "Answer to A", correlationId: callA.id }), + ); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(callCount()).toBe(4); + + const correlated = events.filter((e) => e.type === "message.correlated") as Extract< + ReactorEmittedEvent, + { type: "message.correlated" } + >[]; + expect(correlated).toHaveLength(2); + expect(correlated[0]?.data.correlationId).toBe(callB.id); + expect(correlated[0]?.data.message.content).toBe("Answer to B"); + expect(correlated[1]?.data.correlationId).toBe(callA.id); + expect(correlated[1]?.data.message.content).toBe("Answer to A"); +}); diff --git a/vendor/intx/inference/src/testing/fakes.ts b/vendor/intx/inference/src/testing/fakes.ts new file mode 100644 index 0000000000..1d6409ff43 --- /dev/null +++ b/vendor/intx/inference/src/testing/fakes.ts @@ -0,0 +1,209 @@ +// Minimal fakes for driving a real `createReactor(...)` cycle in tests, +// mirroring `@intx/agent/testing`'s pattern of no-op env-contract +// implementations one level up in this same vendored monorepo. Nothing +// here is provider- or tool-specific: a test wires in whichever +// `ReactorDirector`/`BeforeToolExtension`/turn content it needs. +import type { + AssistantTurn, + ContextCommit, + ContextStore, + ConversationTurn, + InboundMessage, + InferenceEvent, + InferenceSource, + LastCycleSource, + PendingOperation, + TokenUsage, + ToolCall, + ToolResult, + ToolRunner, +} from "@intx/types/runtime"; + +import type { Dependencies } from "../harness"; + +/** + * In-memory `ContextStore`: keeps the last-written turns/manifest/metadata + * in plain fields and answers `commit`/`log`/`readAt` off an in-memory + * commit list. Enough for the reactor's own per-cycle checkpoint sequence + * (`writeTurns` → `writeManifest` → `writeMetadata` → `commit`) — nothing + * here is a durability model, it just has to not throw. + */ +export function createInMemoryContextStore(): ContextStore { + const commits: ContextCommit[] = []; + let nextHash = 1; + + return { + load: () => + Promise.resolve({ + turns: [] as ConversationTurn[], + pendingOperations: [] as PendingOperation[], + tokenUsage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + thinking: 0, + }, + connectorState: null, + }), + setConnectorState: () => {}, + commit: ({ message }) => { + const commit: ContextCommit = { + hash: `commit-${String(nextHash)}`, + message, + timestamp: Date.now(), + }; + nextHash += 1; + commits.push(commit); + return Promise.resolve(commit); + }, + branch: () => Promise.resolve(), + log: (limit) => Promise.resolve(commits.slice(-(limit ?? commits.length))), + readAt: () => Promise.resolve([]), + writeBlob: () => Promise.resolve(), + readBlob: () => Promise.reject(new Error("no blob written")), + writePrompt: () => Promise.resolve(), + writeResponse: () => Promise.resolve(), + writeManifest: () => Promise.resolve(), + writeTurns: () => Promise.resolve(), + writeMetadata: () => Promise.resolve(), + readManifestHistory: () => Promise.resolve([]), + }; +} + +/** + * A `ToolRunner` that records every call it receives. A suspended call + * never reaches `run()` (the reactor answers it via the gate instead), so a + * test asserting the suspend path stays clean asserts `calls` is empty. + */ +export function createRecordingToolRunner( + resultFor: (call: ToolCall) => ToolResult = (call) => ({ + callId: call.id, + content: "", + isError: false, + }), +): ToolRunner & { calls: ToolCall[] } { + const calls: ToolCall[] = []; + return { + calls, + run: (call) => { + calls.push(call); + return Promise.resolve(resultFor(call)); + }, + }; +} + +/** A syntactically valid `InferenceSource` that no fake `inferenceRunner` + * ever actually dials out to. */ +export function createFakeInferenceSource(): InferenceSource { + return { + id: "fake-source", + provider: "fake", + baseURL: "https://fake.invalid", + apiKey: "fake-key", + model: "fake-model", + }; +} + +/** A `Dependencies` bag satisfying the reactor's config shape. Never + * actually invoked by a test that supplies its own `inferenceRunner` + * (which receives — and can ignore — these as part of its harness opts). */ +export function createUnusedDependencies(): Dependencies { + return { + fetch: () => Promise.reject(new Error("fetch should never be called")), + scheduler: { + now: () => Date.now(), + setTimeout: (fn, ms) => { + const id = globalThis.setTimeout(fn, ms); + return () => globalThis.clearTimeout(id); + }, + }, + adapters: { + resolve: () => { + throw new Error("adapters.resolve should never be called"); + }, + } as unknown as Dependencies["adapters"], + }; +} + +const FAKE_LAST_CYCLE_SOURCE: LastCycleSource = { + sourceId: "fake-source", + provider: "fake", + model: "fake-model", +}; + +const ZERO_USAGE: TokenUsage = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + thinking: 0, +}; + +/** An `AssistantTurn` whose only content is one `tool_call` block — the + * shape the director needs to see to decide `executeTools`. */ +export function assistantTurnWithToolCall(call: ToolCall): AssistantTurn { + return { + role: "assistant", + content: [{ type: "tool_call", id: call.id, name: call.name, arguments: call.arguments }], + model: "fake-model", + timestamp: Date.now(), + }; +} + +/** + * A scripted `inferenceRunner`: each call to `runner(...)` pops the next + * queued `AssistantTurn` and yields exactly one `inference.done` event for + * it. `callCount` lets a test assert exactly how many times inference ran — + * the whole point of the suspend/resume regression coverage this supports. + */ +export function createScriptedInferenceRunner(turns: AssistantTurn[]): { + runner: () => AsyncGenerator; + callCount: () => number; +} { + const queue = [...turns]; + let calls = 0; + + async function* runner(): AsyncGenerator { + calls += 1; + const turn = queue.shift(); + if (turn === undefined) { + throw new Error( + `createScriptedInferenceRunner: no scripted turn left for call #${String(calls)}`, + ); + } + yield { + type: "inference.done", + seq: 0, + data: { turn, usage: ZERO_USAGE, source: FAKE_LAST_CYCLE_SOURCE }, + }; + } + + return { runner, callCount: () => calls }; +} + +/** Builds a real `InboundMessage`, optionally carrying an + * `interchangeCorrelationId` header — the one field `tryCorrelate` + * (`../reactor.ts`) keys a resumption on. */ +export function buildInboundMessage(opts: { + content: string; + correlationId?: string; + messageId?: string; +}): InboundMessage { + return { + ref: { uid: 1, mailbox: "INBOX" }, + headers: { + from: "user@local", + to: ["agent@local"], + date: new Date().toISOString(), + messageId: opts.messageId ?? `msg-${crypto.randomUUID()}`, + interchangeType: "conversation.message", + ...(opts.correlationId !== undefined + ? { interchangeCorrelationId: opts.correlationId } + : {}), + }, + flags: [], + content: opts.content, + signatureStatus: "missing", + }; +} From 062acb54c65731b0d1419d4336120ce07ffa785b Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 10:01:04 -0700 Subject: [PATCH 2/3] ask_user: suspend on a message_response gate instead of answering synchronously MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The director re-inferred as soon as ask_user's tool call resolved, so the agent regularly answered its own question — the only thing telling it to wait was prose in the tool result. ask_user now contributes a beforeToolExtension that posts the question card, then returns a suspend decision on a message_response gate, reaching the reactor's existing suspendOnGate — the same mechanism approvals already use. The turn parks with no tool.done and no re-inference until a correlated reply arrives (or the gate's explicit timeout fires with a synthetic error result); neither the reactor nor the director special-cases ask_user by name. ToolBundle gains an optional beforeToolExtension a bundle can contribute independent of the tool's name, composed into the reactor's extension list alongside the authz extension. signals.ts adds "message_response" to SignalKind. registerSignalCorrelation now fails loud for any signal kind other than "approval" rather than silently mis-persisting one it has no co-write for (ask_user's suspend never reaches this RPC — it's fully local via suspendOnGate/gates.register). --- packages/interaction-tools/package.json | 4 +- packages/interaction-tools/src/tool.ts | 118 +++++++++++++----- vendor/intx/agent/src/agent.ts | 18 ++- vendor/intx/agent/src/tool.ts | 10 ++ .../hub-sessions/src/hub-session-lookups.ts | 10 ++ vendor/intx/inference/src/reactor.ts | 112 +++++++++++------ vendor/intx/types/src/signals.ts | 4 +- workflows/assistant/src/index.ts | 2 +- 8 files changed, 202 insertions(+), 76 deletions(-) diff --git a/packages/interaction-tools/package.json b/packages/interaction-tools/package.json index 426bdeecd2..8223d7b2f9 100644 --- a/packages/interaction-tools/package.json +++ b/packages/interaction-tools/package.json @@ -1,8 +1,8 @@ { "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 returns immediately, letting the answer arrive as the responding user's next message", - "version": "0.0.2", + "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", "license": "LGPL-2.1-or-later", "type": "module", "exports": { diff --git a/packages/interaction-tools/src/tool.ts b/packages/interaction-tools/src/tool.ts index e9248d3bbb..7a8804482b 100644 --- a/packages/interaction-tools/src/tool.ts +++ b/packages/interaction-tools/src/tool.ts @@ -1,14 +1,20 @@ // The `ask_user` tool: poses an interview question in-thread as an // interactive `question` block (`@corbits/chat`'s `blocks.ts`) instead of -// prose bullet options, then returns immediately — the answer is never -// awaited synchronously. It arrives as the responding user's own next -// message in this same channel (`packages/chat/src/routes.ts`'s question -// response handling relays it there), so the calling agent reads it on its -// next turn exactly like any other reply. Read-only-ish UI action: no -// approval gate, mirroring `list_agents` rather than `create_agent`. +// prose bullet options, then structurally parks the turn on a +// `message_response` gate — the reactor neither runs nor answers the call +// until a correlated reply arrives. The answer surfaces as the responding +// user's own next message in this same channel +// (`packages/chat/src/routes.ts`'s question response handling relays it +// there), which clears the gate and becomes the call's tool result. No +// synchronous guess: the turn cannot proceed until it does. import { defineTool } from "@intx/agent"; import type { BaseEnv } from "@intx/agent"; -import type { ToolCall, ToolResult } from "@intx/types/runtime"; +import type { + BeforeToolDecision, + PendingOperation, + ToolCall, + ToolResult, +} from "@intx/types/runtime"; import { type } from "arktype"; import { postQuestion, NoOwnChannelError } from "./client"; @@ -16,6 +22,10 @@ import type { AskUserClientConfig } from "./client"; export const ASK_USER_TOOL = "ask_user"; +/** How long a posted question waits for an answer before the gate times out + * and the parked call is answered with a synthetic error. */ +export const ASK_USER_TIMEOUT_MS = 3_600_000; + export interface AskUserEnv extends BaseEnv { readonly hubChatUrl: string; readonly sidecarToken: string; @@ -46,43 +56,74 @@ function clientConfig(env: AskUserEnv): AskUserClientConfig { }; } -async function runAskUser( +/** + * `ask_user`'s `BeforeToolExtension.beforeTool`: posts the question card, + * then parks the call on a `message_response` gate rather than answering it. + * The reactor registers the gate and durably persists `pendingOp` before + * returning to its loop, so the suspension survives a hub restart; it clears + * only when a correlated reply arrives (or the gate times out). + */ +async function beforeAskUser( env: AskUserEnv, call: ToolCall, -): Promise { +): Promise { + if (call.name !== ASK_USER_TOOL) { + return { type: "allow" }; + } + const parsed = AskUserInput(call.arguments); if (parsed instanceof type.errors) { - return errorResult( - call.id, - new Error(`${ASK_USER_TOOL} received invalid input: ${parsed.summary}`), - ); + return { + type: "block", + reason: `${ASK_USER_TOOL} received invalid input: ${parsed.summary}`, + }; } + let questionId: string; try { - await postQuestion(clientConfig(env), parsed); + ({ questionId } = await postQuestion(clientConfig(env), parsed)); } catch (err) { if (err instanceof NoOwnChannelError) { - return errorResult(call.id, err); + return { type: "block", reason: err.message }; } - return errorResult(call.id, err); + return { + type: "block", + reason: err instanceof Error ? err.message : String(err), + }; } + // `postQuestion` already mints `questionId` and stamps it on the outbound + // question card's `data.questionId`; reusing it as the gate's own + // `correlationId` (rather than minting a second, unrelated id) is what + // lets the answer route resolve this exact gate later (CL-7191) — the + // block a person answers is keyed on `blockId`, which for a question + // block IS `questionId` (`packages/chat/src/schema.ts`'s "agent-authored + // pollId/formId" comment applies identically here). + const correlationId = questionId; + const timeoutAt = Date.now() + ASK_USER_TIMEOUT_MS; + const gateId = `pending-${correlationId}`; + const pendingOp: PendingOperation = { + correlationId, + kind: "message_response", + registeredAt: Date.now(), + gateId, + timeoutAt, + suspendedCall: call, + }; + return { - callId: call.id, - isError: false, - content: - "The question has been shown to the user as an interactive card. " + - "Do not repeat or restate it in prose. Their answer will arrive as " + - "their next message in this conversation — wait for it rather than " + - "guessing.", + type: "suspend", + gate: { type: "message_response", gateId, correlationId, timeoutAt }, + pendingOp, }; } /** * The `@corbits/interaction-tools` bundle factory: one tool, `ask_user`, * for posing an enumerable-option interview question as an in-thread card - * instead of a prose list. No approval — showing a question is not an - * external side effect. + * instead of a prose list. No approval gate — showing a question is not an + * external side effect — but its own `message_response` gate parks the turn + * until the user answers. */ export const interactionTools = defineTool({ id: "@corbits/interaction-tools/ask-user", @@ -97,9 +138,9 @@ export const interactionTools = defineTool({ "options, rendered as an interactive card in the conversation " + "instead of a prose list. Use this whenever interviewing the " + "user with a small set of enumerable options (2-6), rather " + - "than writing the options out as text. Returns immediately: " + - "the user's answer arrives as their next message, not as this " + - "call's result.", + "than writing the options out as text. Parks the turn until " + + "the user answers: the answer becomes this call's result, not " + + "a separate message to watch for.", inputSchema: { type: "object", properties: { @@ -129,18 +170,33 @@ export const interactionTools = defineTool({ }, }, ], + beforeToolExtension: { + beforeTool: (call: ToolCall) => beforeAskUser(env, call), + }, run: (call: ToolCall, _signal: AbortSignal) => { - if (call.name !== ASK_USER_TOOL) { + // `beforeToolExtension` above intercepts every ask_user call and parks + // it before dispatch ever reaches here; reaching this arm means + // `interactionTools`'s `beforeToolExtension` was never composed into + // `ResolvedTools.beforeToolExtensions` (`vendor/intx/agent/src/agent.ts`) + // — a re-pin or refactor dropped that wiring — not merely "unreachable". + if (call.name === ASK_USER_TOOL) { return Promise.resolve( errorResult( call.id, new Error( - `@corbits/interaction-tools: unknown tool "${call.name}"`, + `${ASK_USER_TOOL}'s beforeToolExtension was not composed ` + + "into ResolvedTools.beforeToolExtensions — a re-pin or " + + "refactor dropped the wiring in vendor/intx/agent/src/agent.ts", ), ), ); } - return runAskUser(env, call); + return Promise.resolve( + errorResult( + call.id, + new Error(`@corbits/interaction-tools: unknown tool "${call.name}"`), + ), + ); }, }), }); diff --git a/vendor/intx/agent/src/agent.ts b/vendor/intx/agent/src/agent.ts index 03b8bbe69f..b7d235b106 100644 --- a/vendor/intx/agent/src/agent.ts +++ b/vendor/intx/agent/src/agent.ts @@ -53,6 +53,7 @@ import type { ErrorRecord } from "@intx/types/audit"; import type { ApprovalSnapshot, AssistantTurn, + BeforeToolExtension, BlobReader, ContextCommit, ContextStore, @@ -247,6 +248,14 @@ interface ResolvedTools { * standard `ToolBundle` contract. */ readonly bundles: readonly ToolBundle[]; + /** + * Before-tool extensions contributed by bundles via `ToolBundle.beforeToolExtension`, + * in factory order. Composed with the authz extension by `createReactorAssembly` + * (authz first, so policy denial/ask still takes precedence over a tool's own + * gate) so a tool package can suspend on its own gate without the reactor or + * any app-level wiring knowing the tool's name. + */ + readonly beforeToolExtensions: readonly BeforeToolExtension[]; } /** @@ -344,7 +353,11 @@ function resolveTools( }, }; - return { definitions, runner, bundles: constructed }; + const beforeToolExtensions = constructed + .map((bundle) => bundle.beforeToolExtension) + .filter((ext): ext is BeforeToolExtension => ext !== undefined); + + return { definitions, runner, bundles: constructed, beforeToolExtensions }; } function resolveDirector( @@ -695,6 +708,9 @@ export async function createAgent( auditStore, authorize, toolDefinitions: resolvedTools.definitions, + ...(resolvedTools.beforeToolExtensions.length > 0 + ? { beforeToolExtensions: [...resolvedTools.beforeToolExtensions] } + : {}), onShutdown: async () => { try { await flushErrors(); diff --git a/vendor/intx/agent/src/tool.ts b/vendor/intx/agent/src/tool.ts index d4467b7c21..3cebe2450d 100644 --- a/vendor/intx/agent/src/tool.ts +++ b/vendor/intx/agent/src/tool.ts @@ -34,6 +34,7 @@ import type { GrantEffect } from "@intx/types"; import type { + BeforeToolExtension, ToolCall, ToolDefinition, ToolResult, @@ -127,6 +128,15 @@ export interface ToolBundle { readonly definitions: readonly ToolDefinition[]; run(call: ToolCall, signal: AbortSignal): Promise; dispose?(): Promise; + /** + * A before-tool extension this bundle contributes to the reactor, checked + * ahead of dispatch for every call regardless of which bundle owns the + * call's name (mirrors how the authz extension inspects every call). Use + * this when a tool's own semantics -- not a cross-cutting policy like + * authz -- require suspending on a gate before the tool ever runs. Absent + * for the common case of a bundle whose tools always answer synchronously. + */ + readonly beforeToolExtension?: BeforeToolExtension; } /** diff --git a/vendor/intx/hub-sessions/src/hub-session-lookups.ts b/vendor/intx/hub-sessions/src/hub-session-lookups.ts index 2bcad1e644..67f001dbfd 100644 --- a/vendor/intx/hub-sessions/src/hub-session-lookups.ts +++ b/vendor/intx/hub-sessions/src/hub-session-lookups.ts @@ -290,6 +290,16 @@ export function createHubSessionLookups( kind, approvalSnapshot, }) { + // This co-write is approval-only: it always writes an `approval` row, + // built from `approvalSnapshot`, alongside the correlation. A signal + // kind this RPC has no persistence for fails loud here rather than + // writing a spurious approval row for it. + if (kind !== "approval") { + throw new Error( + `registerSignalCorrelation only supports "approval" signals; got "${kind}" for ${correlationId}`, + ); + } + // Resolve tenancy and co-write both rows in one transaction so a resolver // never sees a correlation without its approval or vice versa. Both // inserts are idempotent on their dedup key (the signal_correlation diff --git a/vendor/intx/inference/src/reactor.ts b/vendor/intx/inference/src/reactor.ts index 9c6cb9542c..bd87080e7e 100644 --- a/vendor/intx/inference/src/reactor.ts +++ b/vendor/intx/inference/src/reactor.ts @@ -42,6 +42,7 @@ import type { import { getLogger } from "@intx/log"; import { ApprovalDecision, signalKindToGateType } from "@intx/types"; +import type { SignalKind } from "@intx/types"; import { canonicalJsonStringify } from "@intx/types/wire-definition-hash"; import { type } from "arktype"; import { runInference } from "./harness"; @@ -72,6 +73,20 @@ function assertNever(x: never): never { throw new Error(`Unhandled resume case: ${JSON.stringify(x)}`); } +// Synthetic error text for a parked call whose gate timed out with no reply. +// Exhaustive over SignalKind so a newly added kind must supply its own wording +// rather than inheriting another kind's message. +function timeoutMessageFor(kind: SignalKind): string { + switch (kind) { + case "approval": + return "approval timed out"; + case "message_response": + return "question timed out with no answer"; + default: + return assertNever(kind); + } +} + function buildHarnessOpts( turns: ConversationTurn[], source: InferenceSource, @@ -446,19 +461,21 @@ export function createReactor(config: ReactorConfig): Reactor { type ResumeDispatch = | { mode: "redispatch"; calls: ToolCall[] } | { mode: "gate-cleared" } - | { mode: "error_result"; result: ToolResult }; - - // Decide how a correlated approval-kind pending operation resumes, granting - // any one-shot bypass synchronously so no delivery can interleave between the - // grant and the re-dispatch enqueued by the caller. An operation that carries - // a `suspendedCall` is an ask-flow suspension: the approver's decision routes - // it down the re-dispatch rail. An operation without one is an async-tool - // pending marker, which resumes on the normal gate-cleared rail. + | { mode: "error_result"; result: ToolResult } + | { mode: "answer_result"; result: ToolResult }; + + // Decide how a correlated ask-flow pending operation resumes. An operation + // that carries a `suspendedCall` is an ask-flow suspension: the correlated + // reply routes it down whichever rail its `kind` defines. An operation + // without one is an async-tool pending marker, which resumes on the normal + // gate-cleared rail. // - // The nested switch is total: the outer `assertNever(op.kind)` rejects a - // future SignalKind at compile time, and the inner `assertNever` rejects a - // future decision outcome. A malformed decision body fails loud at the parse - // boundary before the switch. + // The outer switch is total: `assertNever(op.kind)` rejects a future + // SignalKind at compile time. `approval` additionally parses its reply as a + // structured `ApprovalDecision`, failing loud at that parse boundary rather + // than reaching its own nested `assertNever`; `message_response` takes the + // reply's body verbatim, since a question's answer is prose, not a decision + // to parse. function resumePendingOperation( op: PendingOperation, message: InboundMessage, @@ -468,29 +485,29 @@ export function createReactor(config: ReactorConfig): Reactor { } const suspendedCall = op.suspendedCall; - if (message.content === undefined) { - throw new Error( - `Correlated approval decision for ${op.correlationId} has no body to parse`, - ); - } - let raw: unknown; - try { - raw = JSON.parse(message.content); - } catch (cause) { - throw new Error( - `Correlated approval decision for ${op.correlationId} is not valid JSON`, - { cause }, - ); - } - const decision = ApprovalDecision(raw); - if (decision instanceof type.errors) { - throw new Error( - `Correlated approval decision for ${op.correlationId} is malformed: ${decision.summary}`, - ); - } - switch (op.kind) { - case "approval": + case "approval": { + if (message.content === undefined) { + throw new Error( + `Correlated approval decision for ${op.correlationId} has no body to parse`, + ); + } + let raw: unknown; + try { + raw = JSON.parse(message.content); + } catch (cause) { + throw new Error( + `Correlated approval decision for ${op.correlationId} is not valid JSON`, + { cause }, + ); + } + const decision = ApprovalDecision(raw); + if (decision instanceof type.errors) { + throw new Error( + `Correlated approval decision for ${op.correlationId} is malformed: ${decision.summary}`, + ); + } + switch (decision.outcome) { case "approved": // Authorize the exact parked call to run once, then re-dispatch it. @@ -517,6 +534,19 @@ export function createReactor(config: ReactorConfig): Reactor { default: return assertNever(decision.outcome); } + } + case "message_response": + // The correlated reply IS the answer: hand its body straight back as + // the parked call's result rather than re-running the call, since + // there is nothing left to execute once the user has answered. + return { + mode: "answer_result", + result: { + callId: suspendedCall.id, + content: message.content ?? "", + isError: false, + }, + }; default: return assertNever(op.kind); } @@ -585,11 +615,13 @@ export function createReactor(config: ReactorConfig): Reactor { enqueue({ type: "resume.execute_tools", calls: dispatch.calls }); break; } - case "error_result": { - // The approver denied the call. Clear the gate SILENTLY (like the - // approved redispatch) so it cannot also trip onGateCleared and enqueue - // a second continuation. The synthetic error result answers the parked - // call; the director appends it and re-infers once. + case "error_result": + case "answer_result": { + // Either the approver denied the call or a parked question got its + // answer. Clear the gate SILENTLY (like the approved redispatch) so + // it cannot also trip onGateCleared and enqueue a second + // continuation. The result answers the parked call directly; the + // director appends it and re-infers once. if (gate !== undefined) { gates.clearSilently(gate.gateId); if (stateManager !== null) { @@ -1145,7 +1177,7 @@ export function createReactor(config: ReactorConfig): Reactor { type: "resume.tool_result", result: { callId: op.suspendedCall.id, - content: "approval timed out", + content: timeoutMessageFor(op.kind), isError: true, }, }); diff --git a/vendor/intx/types/src/signals.ts b/vendor/intx/types/src/signals.ts index 53855c26da..2f517d5a63 100644 --- a/vendor/intx/types/src/signals.ts +++ b/vendor/intx/types/src/signals.ts @@ -8,7 +8,7 @@ import type { GateType } from "./runtime"; * iterable and can be composed into wire validators) and a derived * TypeScript union. */ -export const signalKinds = ["approval"] as const; +export const signalKinds = ["approval", "message_response"] as const; export const SignalKind = type.enumerated(...signalKinds); export type SignalKind = typeof SignalKind.infer; @@ -53,6 +53,8 @@ export function signalKindToGateType(kind: SignalKind): GateType { switch (kind) { case "approval": return "approval"; + case "message_response": + return "message_response"; default: return assertNever(kind); } diff --git a/workflows/assistant/src/index.ts b/workflows/assistant/src/index.ts index c5190ad449..9174e295db 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.2" }, + { name: "@corbits/interaction-tools", version: "0.0.3" }, { name: "@corbits/manus-tools", version: "0.0.11" }, ]; From 71b1078c4770f5fe3b19b6695ed609a35a60a167 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 30 Aug 2026 10:01:15 -0700 Subject: [PATCH 3/3] Update docs: message_response gate vendor deltas and stale ask_user example Records the new local deltas this introduces in vendor/intx/agent, vendor/intx/inference, vendor/intx/types, and vendor/intx/hub-sessions in both VENDORED.md and each package's VENDORED-FROM file, with matching tree-hash updates in scripts/checks/kill-dates.txt. DESIGN.md's disclosure example referenced ask_user's now-removed "do not repeat this in prose" instruction; swapped for request_connection's equivalent, still-live one. --- DESIGN.md | 8 +++--- VENDORED.md | 34 +++++++++++++------------- scripts/checks/kill-dates.txt | 8 +++--- vendor/intx/agent/VENDORED-FROM | 2 +- vendor/intx/hub-sessions/VENDORED-FROM | 2 +- vendor/intx/inference/VENDORED-FROM | 2 +- vendor/intx/types/VENDORED-FROM | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 6d4685f3d6..f051193128 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -260,10 +260,10 @@ phrase destructive. **Disclosure.** A caret exists only when there is human-readable detail. A chip with nothing to disclose offers no control at all. Expanded detail is quiet inset prose under that chip. Raw JSON, JSON strings, and -model-facing instructions (e.g. ask_user's "do not repeat this in prose") -never reach a reader — parse JSON into a count ("3 results.") or omit the -disclosure. The only exception is code the user actually asked for, which -is prose, not machinery. +model-facing instructions (e.g. request_connection's "keep helping in +the meantime") never reach a reader — parse JSON into a count +("3 results.") or omit the disclosure. The only exception is code the +user actually asked for, which is prose, not machinery. **Chrome.** Hit area ≥40px via an invisible `::before`. Radius `--radius`. Motion via `--duration-standard` / `--ease-out`. Scale-on-press diff --git a/VENDORED.md b/VENDORED.md index d88930f66a..907185f7ad 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; 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/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) or the sealed-run terminal-status backfill (CL-6595); 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`); 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) @ `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), 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` | 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/scripts/checks/kill-dates.txt b/scripts/checks/kill-dates.txt index d278092798..5740abcdd6 100644 --- a/scripts/checks/kill-dates.txt +++ b/scripts/checks/kill-dates.txt @@ -14,17 +14,17 @@ # drift: editing a vendored tree means updating this hash and the # package's VENDORED-FROM delta line in the same change. apps/sidecar | sawyer | 2026-10-26 -vendor/intx/agent | sawyer | 2026-10-26 | d0d56d9f452b78f4b541ad8f4e89f975e8069446bb98f2c8097b90de4b020243 +vendor/intx/agent | sawyer | 2026-10-26 | 2264716cfaa126ef3205f4f9887f3a580072ab8552d6bbc351bd8ca1638bfd74 vendor/intx/db | sawyer | 2026-10-26 | 0a4cdb9a8a6ff19d5d4713cbc4f5cc9257aad839b1fa393b2026e6d5afd828b9 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 | 53addc3090ad9f54bc4bac8fb50ad8d567ccf46f30bb5403d447351cb16b4fb6 -vendor/intx/inference | sawyer | 2026-10-26 | 77fec29b078e8d03e686747c70e6b62ac1fd1434db0fb2c1e12e84b6dc71465f +vendor/intx/hub-sessions | sawyer | 2026-10-26 | 19fcc2c0bf6af1dd9a1b267513bea777968c454a323cc402fc5ec1cca8097523 +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 vendor/intx/mime | sawyer | 2026-10-26 | d02e5f8f1429eac7c27d3a37eec31111f8a1053c91fbfae77ac58a0d63c823ed -vendor/intx/types | sawyer | 2026-10-26 | ec1de14b859007b4db137da1533d4ce79d11024ad69c8b36938017319d6d8e86 +vendor/intx/types | sawyer | 2026-10-26 | beef009a180cb7747c65014ed38c732a124851c81f60029ccd0c33075896b93f vendor/intx/workflow | sawyer | 2026-10-26 | 4b51b9bd6a124cfaa0c916e2b26c04ac9170618bb092f0c8e1c312263fc84fdf vendor/intx/workflow-deploy | sawyer | 2026-10-26 | 960a2ae408223649fe8be0e3b9d63f2b0cca25259bc0ae06761bca521bb738e5 vendor/intx/workflow-host | sawyer | 2026-10-26 | aff0342a526387ea9ccb52827fd4f13d9dd52645b37795362ce9b2fd912a5da5 diff --git a/vendor/intx/agent/VENDORED-FROM b/vendor/intx/agent/VENDORED-FROM index 793972d109..896d3664d1 100644 --- a/vendor/intx/agent/VENDORED-FROM +++ b/vendor/intx/agent/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/agent) 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. 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. diff --git a/vendor/intx/hub-sessions/VENDORED-FROM b/vendor/intx/hub-sessions/VENDORED-FROM index b4c90f0503..0f7d6a4aa9 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). +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. diff --git a/vendor/intx/inference/VENDORED-FROM b/vendor/intx/inference/VENDORED-FROM index df126e8550..507bec5c0d 100644 --- a/vendor/intx/inference/VENDORED-FROM +++ b/vendor/intx/inference/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/inference) 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. providers/google-genai-files.ts builds its upload body as `new Uint8Array(bytes)`: TS 6's lib.dom `BodyInit` rejects `Uint8Array` and workbench compiles this source under DOM-lib packages (upstream compiles ESNext-only under TS 5.9). +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. providers/google-genai-files.ts builds its upload body as `new Uint8Array(bytes)`: TS 6's lib.dom `BodyInit` rejects `Uint8Array` and workbench compiles this source under DOM-lib packages (upstream compiles ESNext-only under TS 5.9). CL-7190: a message_response resume branch in reactor.ts's resumePendingOperation/timeoutMessageFor, plus reactor.test.ts and testing/fakes.ts, a new regression-test harness driving createReactor directly (this package previously had zero tests). diff --git a/vendor/intx/types/VENDORED-FROM b/vendor/intx/types/VENDORED-FROM index d7f88c771f..f9ad8bee39 100644 --- a/vendor/intx/types/VENDORED-FROM +++ b/vendor/intx/types/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/types) 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. CL-7190: "message_response" added to signals.ts's signalKinds, alongside signalKindToGateType.