From b8d626ed7a51e518d5c6970e7adebbd5fbb5c96e Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 11:38:18 -0700 Subject: [PATCH 1/4] Add tests for child-run-id lookup on overlapping agent turns Two simultaneously running rows for the same (workbench, agent) must close the named occurrence, not the newest one. Old sidecars that omit childRunId still finish the newest overlapping turn. --- packages/chat/src/agent-turns.test.ts | 25 ++ packages/chat/test/chat-orchestrator.test.ts | 275 +++++++++++++++++++ 2 files changed, 300 insertions(+) diff --git a/packages/chat/src/agent-turns.test.ts b/packages/chat/src/agent-turns.test.ts index 2adf0be95..2e0112d9e 100644 --- a/packages/chat/src/agent-turns.test.ts +++ b/packages/chat/src/agent-turns.test.ts @@ -111,6 +111,31 @@ describe("createInMemoryAgentTurnStore", () => { expect(listed.map((turn) => turn.id)).toEqual([second.id, first.id]); }); + // CL-6396: two simultaneously-running rows for the same (workbench, agent) + // are no longer a coin flip when the caller names the occurrence. The + // newest-occurrence pick remains the documented fallback for a lookup + // that omits childRunId (an old sidecar's agent.event frames). + test("findRunningTurn names a specific occurrence when childRunId is given", async () => { + const store = createInMemoryAgentTurnStore(); + const first = await store.startTurn(BASE); + const second = await store.startTurn(BASE); + expect(first.childRunId).toBe("turn__0"); + expect(second.childRunId).toBe("turn__1"); + expect(first.status).toBe("running"); + expect(second.status).toBe("running"); + + expect((await store.findRunningTurn(BASE))?.id).toBe(second.id); + expect( + (await store.findRunningTurn({ ...BASE, childRunId: "turn__0" }))?.id, + ).toBe(first.id); + expect( + (await store.findRunningTurn({ ...BASE, childRunId: "turn__1" }))?.id, + ).toBe(second.id); + expect( + await store.findRunningTurn({ ...BASE, childRunId: "turn__9" }), + ).toBeUndefined(); + }); + // CL-7200: listing used to sort by startedAt string alone, while // findRunningTurn sorted by occurrence. Two turns sharing a timestamp // (same-millisecond ISO strings compare equal) then disagreed — listing diff --git a/packages/chat/test/chat-orchestrator.test.ts b/packages/chat/test/chat-orchestrator.test.ts index fa40d7178..aa395d5de 100644 --- a/packages/chat/test/chat-orchestrator.test.ts +++ b/packages/chat/test/chat-orchestrator.test.ts @@ -732,6 +732,281 @@ describe("createChatOrchestrator", () => { orchestrator.dispose(); }); + // CL-6396: two overlapping running rows for the same (workbench, agent) + // used to make `findRunningTurn`'s newest-occurrence pick a coin flip. + // A reply that names turn__0 must close that row, never the later one. + test("a reply named for turn__0 does not close an overlapping turn__1", async () => { + const room = fakeRoom(); + const agentTurns = createInMemoryAgentTurnStore(); + const agentAddress = "ins_echo1@ten1.workbench.test"; + const first = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_0"], + }); + const second = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_1"], + }); + expect(first.childRunId).toBe("turn__0"); + expect(second.childRunId).toBe("turn__1"); + const events = createSidecarEmitter(); + const orchestrator = createChatOrchestrator({ + db: createFakeDb({ id: "ins_echo1", tenantId: "ten_1" }) as never, + store: { + listWorkbenchSettings: async () => [ + workbenchRow("ins_workbench1", [agentAddress]), + ], + }, + roomMessages: room.roomMessages, + publish: room.publish, + platform: fakeMail().platform, + events, + agentTurns, + claims: fakeClaims(), + approvals: { findByCorrelationId: async () => null }, + }); + + events.emit("agent.event", { + agentAddress, + sessionId: "ses_1", + childRunId: "turn__0", + event: { type: "connector.reply", data: { content: "first reply" } }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(room.posted).toHaveLength(1); + expect(room.posted[0]).toMatchObject({ + runId: "turn__0", + parts: [{ kind: "text", text: "first reply" }], + }); + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: first.id })) + ?.status, + ).toBe("completed"); + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: second.id })) + ?.status, + ).toBe("running"); + + orchestrator.dispose(); + }); + + // CL-6396: an old sidecar's agent.event frames omit childRunId. The one + // documented fallback is newest-occurrence — the later overlapping row + // receives the reply, not a spray and not a late-reply invent. + test("a reply with no childRunId still finishes the newest overlapping turn", async () => { + const room = fakeRoom(); + const agentTurns = createInMemoryAgentTurnStore(); + const agentAddress = "ins_echo1@ten1.workbench.test"; + const first = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_0"], + }); + const second = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_1"], + }); + const events = createSidecarEmitter(); + const orchestrator = createChatOrchestrator({ + db: createFakeDb({ id: "ins_echo1", tenantId: "ten_1" }) as never, + store: { + listWorkbenchSettings: async () => [ + workbenchRow("ins_workbench1", [agentAddress]), + ], + }, + roomMessages: room.roomMessages, + publish: room.publish, + platform: fakeMail().platform, + events, + agentTurns, + claims: fakeClaims(), + approvals: { findByCorrelationId: async () => null }, + }); + + events.emit("agent.event", { + agentAddress, + sessionId: "ses_1", + event: { type: "connector.reply", data: { content: "legacy reply" } }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(room.posted).toHaveLength(1); + expect(room.posted[0]?.runId).toBe("turn__1"); + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: first.id })) + ?.status, + ).toBe("running"); + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: second.id })) + ?.status, + ).toBe("completed"); + + orchestrator.dispose(); + }); + + // CL-6396: two turns on one sidecar session must not share the + // reply-parts accumulator. Turn 0's late `message.run.ended` used to + // `take()` the session-keyed bucket after turn 1 had already started + // accumulating, swallowing turn 1's structured parts. + test("sequential turns on one sessionId with distinct childRunIds do not steal reply-parts", async () => { + const room = fakeRoom(); + const agentTurns = createInMemoryAgentTurnStore(); + const agentAddress = "ins_echo1@ten1.workbench.test"; + const first = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_0"], + }); + const events = createSidecarEmitter(); + const orchestrator = createChatOrchestrator({ + db: createFakeDb({ id: "ins_echo1", tenantId: "ten_1" }) as never, + store: { + listWorkbenchSettings: async () => [ + workbenchRow("ins_workbench1", [agentAddress]), + ], + }, + roomMessages: room.roomMessages, + publish: room.publish, + platform: fakeMail().platform, + events, + agentTurns, + claims: fakeClaims(), + approvals: { findByCorrelationId: async () => null }, + }); + + events.emit("agent.event", { + agentAddress, + sessionId: "ses_shared", + childRunId: "turn__0", + event: { + type: "inference.done", + data: { turn: { content: [{ type: "text", text: "first thought" }] } }, + }, + }); + events.emit("agent.event", { + agentAddress, + sessionId: "ses_shared", + childRunId: "turn__0", + event: { + type: "connector.reply", + data: { content: "first thought" }, + }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: first.id })) + ?.status, + ).toBe("completed"); + + const second = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_1"], + }); + expect(second.childRunId).toBe("turn__1"); + + events.emit("agent.event", { + agentAddress, + sessionId: "ses_shared", + childRunId: "turn__1", + event: { + type: "inference.done", + seq: 1, + data: { + turn: { + content: [ + { type: "text", text: "Let me check that." }, + { + type: "tool_call", + id: "call_1", + name: "web_search", + arguments: { query: "second thought" }, + }, + ], + }, + }, + }, + }); + events.emit("agent.event", { + agentAddress, + sessionId: "ses_shared", + childRunId: "turn__1", + event: { + type: "tool.done", + seq: 2, + data: { + result: { callId: "call_1", content: "3 results found" }, + }, + }, + }); + events.emit("agent.event", { + agentAddress, + sessionId: "ses_shared", + childRunId: "turn__1", + event: { + type: "inference.done", + seq: 3, + data: { + turn: { content: [{ type: "text", text: "second thought" }] }, + }, + }, + }); + // Late bracket-close for the first turn, still on the shared session. + events.emit("agent.event", { + agentAddress, + sessionId: "ses_shared", + childRunId: "turn__0", + event: { + type: "message.run.ended", + data: { status: "completed", messageRunId: "mr_0", messageId: "msg_0" }, + }, + }); + events.emit("agent.event", { + agentAddress, + sessionId: "ses_shared", + childRunId: "turn__1", + event: { + type: "connector.reply", + data: { content: "second thought" }, + }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(room.posted).toHaveLength(2); + expect(room.posted[0]?.parts).toEqual([ + { kind: "text", text: "first thought" }, + ]); + expect(room.posted[1]?.parts).toEqual([ + { kind: "text", text: "Let me check that." }, + { + kind: "tool-trace", + name: "web_search", + input: { query: "second thought" }, + status: "success", + output: "3 results found", + }, + { kind: "text", text: "second thought" }, + ]); + expect(room.posted[1]?.runId).toBe("turn__1"); + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: second.id })) + ?.status, + ).toBe("completed"); + + orchestrator.dispose(); + }); + // CL-6378: a turn's `inference.done` events already split the model's // output into prose and tool calls (see `event-collector.ts`'s // `handleInferenceDone`), and `tool.done` resolves each call's From 940e709dc9857719ad7865855e462aaa11891585 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 11:38:47 -0700 Subject: [PATCH 2/4] Stamp childRunId on agent.event so overlapping turns close the right row The workflow-host event channel carries the occurrence id through the sidecar and hub. findRunningTurn uses it as an exact lookup, and the chat orchestrator keys reply-parts by occurrence instead of session alone. An omitted childRunId still falls back to newest-occurrence. --- .../sidecar/src/workflow-host-wiring/index.ts | 11 ++++- packages/chat/src/agent-turns.ts | 47 ++++++++++++------- packages/chat/src/chat-orchestrator.ts | 38 ++++++++++----- .../hub-agent/src/sidecar-orchestrator.ts | 6 ++- vendor/intx/hub-agent/src/ws/hub-link.ts | 9 +++- .../hub-sessions/src/ws/sidecar-events.ts | 2 + .../hub-sessions/src/ws/sidecar-handler.ts | 3 ++ vendor/intx/types/src/sidecar.ts | 1 + .../intx/workflow-host/src/child/run-child.ts | 15 +++--- vendor/intx/workflow-host/src/ipc/envelope.ts | 13 ++++- .../workflow-host/src/ipc/event-channel.ts | 29 +++++++++--- vendor/intx/workflow-host/src/ipc/index.ts | 1 + .../workflow-host/src/supervisor/recycle.ts | 11 +++-- .../src/supervisor/supervisor.ts | 17 +++---- 14 files changed, 143 insertions(+), 60 deletions(-) diff --git a/apps/sidecar/src/workflow-host-wiring/index.ts b/apps/sidecar/src/workflow-host-wiring/index.ts index 242db0269..a362fb3d3 100644 --- a/apps/sidecar/src/workflow-host-wiring/index.ts +++ b/apps/sidecar/src/workflow-host-wiring/index.ts @@ -444,6 +444,7 @@ export function createSidecarDeployRouter(deps: { agentAddress: string, event: InferenceEvent, sessionId: string | undefined, + childRunId?: string, ) => void; /** * Optional override for the multi-step branch's per-step mail-address @@ -617,6 +618,7 @@ export function createSidecarDeployRouter(deps: { _address: string, _event: InferenceEvent, _sessionId: string | undefined, + _childRunId?: string, ): void => { /* no-op default: tests and production-without-a-publisher deployments do not consume events. */ @@ -1081,7 +1083,7 @@ export function createSidecarDeployRouter(deps: { stepOrder, definitionHash, warmKeep, - onInferenceEvent: (event) => { + onInferenceEvent: (event, childRunId) => { // The event arrives HMAC-verified over the child's event channel. // Re-narrow it to the hub's `InferenceEvent` union; a parse // failure means upstream corruption, so drop it loudly rather @@ -1091,7 +1093,12 @@ export function createSidecarDeployRouter(deps: { logger.warn`dropping workflow inference event for ${spec.agentAddress}: ${validated.summary}`; return; } - publishInferenceEvent(spec.agentAddress, validated, spec.sessionId); + publishInferenceEvent( + spec.agentAddress, + validated, + spec.sessionId, + childRunId, + ); }, }; diff --git a/packages/chat/src/agent-turns.ts b/packages/chat/src/agent-turns.ts index 74be717d9..bee52b4c5 100644 --- a/packages/chat/src/agent-turns.ts +++ b/packages/chat/src/agent-turns.ts @@ -79,26 +79,34 @@ export interface AgentTurnStore { */ finishTurn(input: FinishAgentTurnInput): Promise; /** - * The newest turn still `running` for (workbench, agent) — what the - * reply path stamps onto the message it posts. The sidecar's - * `agent.event` frames carry the agent's address and nothing finer, - * so this is how a reply finds the occurrence that produced it. - * Unambiguous only because `waitUntilFree` (below) is what - * `dispatchTurn` (`./workbench-service.ts`) now calls before ever - * opening a second occurrence for the same (workbench, agent) — the - * one-in-flight-turn-per-workbench claim (`./turn-queue.ts`) is a - * different, coarser guarantee (it only spans the fast dispatch - * handoff, not the agent's actual reply) and on its own was NOT - * enough: two messages sent to the same agent a few seconds apart - * could each win that claim in turn and open a second `running` row - * while the first agent turn was still generating (CL-6670), making - * this method's "newest" pick a coin flip between the two turns' - * real replies. + * The newest turn still `running` for (workbench, agent), or — when + * `childRunId` is present — that exact occurrence. The reply path stamps + * the row it finds onto the message it posts. An old sidecar's + * `agent.event` frames omit `childRunId`; those callers keep the + * newest-occurrence pick, which is the one documented fallback. + * + * `waitUntilFree` (below) is what `dispatchTurn` (`./workbench-service.ts`) + * calls before opening a second occurrence for the same (workbench, + * agent) — the one-in-flight-turn-per-workbench claim (`./turn-queue.ts`) + * is a different, coarser guarantee (it only spans the fast dispatch + * handoff, not the agent's actual reply) and on its own was NOT enough: + * two messages sent to the same agent a few seconds apart could each win + * that claim in turn and open a second `running` row while the first + * agent turn was still generating (CL-6670), making the newest pick a + * coin flip between the two turns' real replies. */ findRunningTurn(input: { readonly tenantId: string; readonly workbenchId: string; readonly agentAddress: string; + /** + * When present, this is an exact occurrence lookup — the row whose + * `childRunId` matches, still `running`. An old sidecar's `agent.event` + * frames omit this (CL-6396); those callers keep the newest-occurrence + * pick below, which is the one documented fallback, not a second path + * that posts a late reply. + */ + readonly childRunId?: string; }): Promise; /** * Every `running` turn for a workbench, across every agent — the @@ -301,6 +309,7 @@ export function createInMemoryAgentTurnStore( readonly tenantId: string; readonly workbenchId: string; readonly agentAddress: string; + readonly childRunId?: string; }): AgentTurn | undefined { expireStaleTurns(); return [...rows.values()] @@ -309,7 +318,9 @@ export function createInMemoryAgentTurnStore( turn.tenantId === input.tenantId && turn.workbenchId === input.workbenchId && turn.agentAddress === input.agentAddress && - turn.status === "running", + turn.status === "running" && + (input.childRunId === undefined || + turn.childRunId === input.childRunId), ) .sort(compareTurnsNewestFirst)[0]; } @@ -505,6 +516,7 @@ export function createDrizzleAgentTurnStore< readonly tenantId: string; readonly workbenchId: string; readonly agentAddress: string; + readonly childRunId?: string; }): Promise { await expireStaleTurns(input); const [row] = await db @@ -516,6 +528,9 @@ export function createDrizzleAgentTurnStore< eq(agentTurns.workbenchId, input.workbenchId), eq(agentTurns.agentAddress, input.agentAddress), eq(agentTurns.status, "running"), + ...(input.childRunId !== undefined + ? [eq(agentTurns.childRunId, input.childRunId)] + : []), ), ) .orderBy(desc(agentTurns.startedAt), desc(agentTurns.occurrence)) diff --git a/packages/chat/src/chat-orchestrator.ts b/packages/chat/src/chat-orchestrator.ts index 7b056301f..ad5bdecfd 100644 --- a/packages/chat/src/chat-orchestrator.ts +++ b/packages/chat/src/chat-orchestrator.ts @@ -96,8 +96,10 @@ export type ChatOrchestratorDeps = { * The turn projection (CL-6329). A room agent runs as an `onTrigger` * section, so the reply this orchestrator posts belongs to one * occurrence — its own child run — and the projection is what names - * that child: the sidecar's `agent.event` frames carry the agent's - * address and nothing finer. Omitted, replies carry no run id at all + * that child. `agent.event` frames carry an optional `childRunId` + * (`turn__`) so a reply can name the occurrence that produced it; + * an old sidecar omits that field and this module falls back to the + * newest running turn. Omitted store, replies carry no run id at all * rather than a guessed one; there is no second way to name a run. */ agentTurns?: Pick< @@ -214,11 +216,17 @@ function gateBlockedCorrelationId(event: unknown): string | undefined { * `agent.event` frame carries a `sessionId` regardless of which inner * event type it wraps (`vendor/intx/hub-sessions/src/ws/sidecar-events.ts`'s * `SidecarEventMap["agent.event"]`), scoped to the one sidecar connection - * that turn's occurrence runs on — the correlator this module's in-flight - * turn state (below) keys on instead of the address alone. + * that turn's occurrence runs on. When the frame also names an occurrence + * (`childRunId`), that is the correlator — two overlapping turns share a + * session and must not share this key. An old sidecar omits `childRunId` + * and today's session id remains the key. */ -function turnKeyFor(agentAddress: string, sessionId: string): string { - return `${agentAddress}:${sessionId}`; +function turnKeyFor( + agentAddress: string, + sessionId: string, + childRunId?: string, +): string { + return `${agentAddress}:${childRunId ?? sessionId}`; } /** @@ -575,6 +583,7 @@ async function postReply( parts: readonly Part[], outcome: TurnOutcome, event?: unknown, + childRunId?: string, ): Promise { const resolved = await resolveMemberWorkbenches(deps, agentAddress); if (resolved === undefined) return; @@ -585,6 +594,7 @@ async function postReply( tenantId: resolved.tenantId, workbenchId, agentAddress: resolved.roomAddress, + ...(childRunId !== undefined ? { childRunId } : {}), }); members.push({ workbenchId, @@ -647,6 +657,7 @@ async function postReply( tenantId: resolved.tenantId, workbenchId, agentAddress: resolved.roomAddress, + ...(childRunId !== undefined ? { childRunId } : {}), }); const posted = await postRoomMessage(deps, { tenantId: resolved.tenantId, @@ -1108,12 +1119,13 @@ export function createChatOrchestrator( // Every turn with a `connector.reply` pending delivery — added the // moment reply content is seen, cleared the moment that same turn's own // `message.run.ended` bracket closes (see below). Keyed by `turnKeyFor` - // (agent address + sidecar session id), never by address alone: one + // (agent address + occurrence run id when the sidecar supplied one, + // otherwise the sidecar session id), never by address alone: one // address can be mid-turn in several benches at once // (`resolveMemberWorkbenches` returns workbench ids plural by design), - // and each such turn runs its own sidecar connection with its own - // `sessionId` — the correlator every `agent.event` frame carries - // regardless of which inner event type it wraps. + // two overlapping turns on one sidecar share a `sessionId`, and each + // `agent.event` frame carries that session id regardless of which + // inner event type it wraps. const repliedTurns = new Set(); // Process-lifetime idempotency guard for the turn-drop notice below, @@ -1142,14 +1154,14 @@ export function createChatOrchestrator( const unsubscribe = deps.events.on( "agent.event", - ({ agentAddress, sessionId, event }) => { + ({ agentAddress, sessionId, event, childRunId }) => { // Any event at all counts as activity, not just `connector.reply` // below — an agent mid-inference must never be undeployed out // from under itself by the idle sweep just because it hasn't // replied yet. deps.recordActivity?.(agentAddress); - const turnKey = turnKeyFor(agentAddress, sessionId); + const turnKey = turnKeyFor(agentAddress, sessionId, childRunId); if (messageRunStarted(event)) { notifiedDropTurns.delete(turnKey); @@ -1196,6 +1208,7 @@ export function createChatOrchestrator( status: "completed", }, event, + childRunId, ).catch((cause: unknown) => { log.error`chat orchestrator: failed to post ${agentAddress}'s reply: ${ cause instanceof Error ? cause.message : String(cause) @@ -1253,6 +1266,7 @@ export function createChatOrchestrator( [{ kind: "text", text: noticeContent }], { status: "failed", error: errorMessage }, event, + childRunId, ).catch((cause: unknown) => { log.error`chat orchestrator: failed to post ${agentAddress}'s turn-drop notice: ${ cause instanceof Error ? cause.message : String(cause) diff --git a/vendor/intx/hub-agent/src/sidecar-orchestrator.ts b/vendor/intx/hub-agent/src/sidecar-orchestrator.ts index d968fa056..236e73b2c 100644 --- a/vendor/intx/hub-agent/src/sidecar-orchestrator.ts +++ b/vendor/intx/hub-agent/src/sidecar-orchestrator.ts @@ -75,6 +75,7 @@ export type CreateDeployRouter = (deps: { agentAddress: string, event: InferenceEvent, sessionId: string | undefined, + childRunId?: string, ) => void; /** * Control-plane suspension sink the multi-step branch routes a @@ -254,6 +255,7 @@ export function createSidecarOrchestrator( agentAddress: string, sessionId: string, event: InferenceEvent, + childRunId?: string, ) => void = () => { /* replaced after HubLink construction */ }; @@ -286,7 +288,7 @@ export function createSidecarOrchestrator( // is observed. A sessionless event is dropped rather than guessed // onto an arbitrary session -- the hub timeline is session-keyed and // a forged session id would mis-route the event. - publishWorkflowInferenceEvent: (agentAddress, event, sessionId) => { + publishWorkflowInferenceEvent: (agentAddress, event, sessionId, childRunId) => { if (sessionId === undefined) { log.warn( "Dropping workflow inference event for {agentAddress}: deploy carried no sessionId", @@ -294,7 +296,7 @@ export function createSidecarOrchestrator( ); return; } - dispatchEvent(agentAddress, sessionId, event); + dispatchEvent(agentAddress, sessionId, event, childRunId); }, // Route a supervisor's suspension registration up the hub-link so the // hub co-writes the parked run's routing + approval rows. diff --git a/vendor/intx/hub-agent/src/ws/hub-link.ts b/vendor/intx/hub-agent/src/ws/hub-link.ts index def792e1d..81ff76730 100644 --- a/vendor/intx/hub-agent/src/ws/hub-link.ts +++ b/vendor/intx/hub-agent/src/ws/hub-link.ts @@ -59,6 +59,7 @@ export type SessionEventSink = ( agentAddress: string, sessionId: string, event: InferenceEvent, + childRunId?: string, ) => void; const logger = getLogger(["interchange", "hub-agent", "ws"]); @@ -1757,12 +1758,18 @@ export function createHubLink(config: HubLinkConfig): HubLink { } } - const sendEvent: SessionEventSink = (agentAddress, sessionId, event) => { + const sendEvent: SessionEventSink = ( + agentAddress, + sessionId, + event, + childRunId, + ) => { send({ type: "agent.event", agentAddress, sessionId, event, + ...(childRunId !== undefined ? { childRunId } : {}), }); }; diff --git a/vendor/intx/hub-sessions/src/ws/sidecar-events.ts b/vendor/intx/hub-sessions/src/ws/sidecar-events.ts index 08ea169d0..87cd5e814 100644 --- a/vendor/intx/hub-sessions/src/ws/sidecar-events.ts +++ b/vendor/intx/hub-sessions/src/ws/sidecar-events.ts @@ -82,6 +82,8 @@ export type SidecarEventMap = { agentAddress: string; sessionId: string; event: unknown; + /** Occurrence child run id (`turn__`) when the sidecar supplied it. */ + childRunId?: string; }; /** Notification. Emitted once when a sidecar's connection closes, diff --git a/vendor/intx/hub-sessions/src/ws/sidecar-handler.ts b/vendor/intx/hub-sessions/src/ws/sidecar-handler.ts index 6e89ae956..52a849cd1 100644 --- a/vendor/intx/hub-sessions/src/ws/sidecar-handler.ts +++ b/vendor/intx/hub-sessions/src/ws/sidecar-handler.ts @@ -1132,6 +1132,9 @@ export function createSidecarRouter( agentAddress: frame.agentAddress, sessionId: frame.sessionId, event: frame.event, + ...(frame.childRunId !== undefined + ? { childRunId: frame.childRunId } + : {}), }); dispatchToSubscribers(frame.agentAddress, frame.event); return; diff --git a/vendor/intx/types/src/sidecar.ts b/vendor/intx/types/src/sidecar.ts index ca693e021..d0c89794e 100644 --- a/vendor/intx/types/src/sidecar.ts +++ b/vendor/intx/types/src/sidecar.ts @@ -117,6 +117,7 @@ export const AgentEventFrame = type({ agentAddress: "string", sessionId: "string", event: InferenceEvent, + "childRunId?": "string", }); export type AgentEventFrame = typeof AgentEventFrame.infer; diff --git a/vendor/intx/workflow-host/src/child/run-child.ts b/vendor/intx/workflow-host/src/child/run-child.ts index e9a2b7db8..a59fd891c 100644 --- a/vendor/intx/workflow-host/src/child/run-child.ts +++ b/vendor/intx/workflow-host/src/child/run-child.ts @@ -901,8 +901,8 @@ export async function runWorkflowChild( warmCache, sourcesRef, credentialWiring, - onEvent: (event) => { - void eventSender.send(event).catch((cause) => { + onEvent: (event, childRunId) => { + void eventSender.send(event, childRunId).catch((cause) => { logger.error`event-channel send failed during resume run ${run.runId}: ${String(cause)}`; }); }, @@ -1171,8 +1171,8 @@ async function handleControlPayload( warmCache: ctx.warmCache, sourcesRef: ctx.sourcesRef, credentialWiring: ctx.credentialWiring, - onEvent: (event) => { - void ctx.eventSender.send(event).catch((cause) => { + onEvent: (event, childRunId) => { + void ctx.eventSender.send(event, childRunId).catch((cause) => { logger.error`event-channel send failed during run ${payload.data.runId}: ${String(cause)}`; }); }, @@ -1622,7 +1622,7 @@ function buildRuntimeEnv(args: { warmCache: WarmAgentCache | undefined; sourcesRef: SourcesSnapshotRef; credentialWiring: CredentialWiring; - onEvent: (event: EventPayload) => void; + onEvent: (event: EventPayload, childRunId?: string) => void; upstreamSender: ControlChannelSender; }): WorkflowRuntimeEnv { const signalChannel = createWorkflowHostSignalChannel({ @@ -1681,7 +1681,10 @@ function buildRuntimeEnv(args: { const spawnSuspendableChild: SpawnSuspendableChild | undefined = hostSuspendable === undefined ? undefined - : (spawnInput) => hostSuspendable(spawnInput, args.onEvent); + : (spawnInput) => + hostSuspendable(spawnInput, (event) => { + args.onEvent(event, spawnInput.childRunId); + }); const env: WorkflowRuntimeEnv = { repoStore: args.runtimeRepoStore, scheduler: args.bindings.scheduler, diff --git a/vendor/intx/workflow-host/src/ipc/envelope.ts b/vendor/intx/workflow-host/src/ipc/envelope.ts index ea931c983..4555a9fd9 100644 --- a/vendor/intx/workflow-host/src/ipc/envelope.ts +++ b/vendor/intx/workflow-host/src/ipc/envelope.ts @@ -16,7 +16,7 @@ // signer signs and the verifier verifies. // // Canonical JSON: keys appear in fixed insertion order (seq, -// channelId, payload). The payload's internal structure is preserved +// channelId, payload, then childRunId when the event channel set it). The payload's internal structure is preserved // as the sender produced it; no recursive canonicalization is // performed because the verifier never compares two structurally // different serializations of the same logical value. Senders and @@ -35,6 +35,7 @@ export const FrameEnvelope = type({ seq: "number", channelId: "string", payload: "unknown", + "childRunId?": "string", }); export type FrameEnvelope = typeof FrameEnvelope.infer; @@ -69,11 +70,19 @@ export type MacedEnvelope = typeof MacedEnvelope.infer; * fixed-shape object. */ export function encodeEnvelope(envelope: FrameEnvelope): Uint8Array { - const ordered = { + const ordered: { + seq: number; + channelId: string; + payload: unknown; + childRunId?: string; + } = { seq: envelope.seq, channelId: envelope.channelId, payload: envelope.payload, }; + if (envelope.childRunId !== undefined) { + ordered.childRunId = envelope.childRunId; + } return new TextEncoder().encode(JSON.stringify(ordered)); } diff --git a/vendor/intx/workflow-host/src/ipc/event-channel.ts b/vendor/intx/workflow-host/src/ipc/event-channel.ts index 835b0c7d2..30588f22e 100644 --- a/vendor/intx/workflow-host/src/ipc/event-channel.ts +++ b/vendor/intx/workflow-host/src/ipc/event-channel.ts @@ -46,6 +46,16 @@ import { signHmac, verifyHmac } from "./crypto"; export const EventPayload = InferenceEvent; export type EventPayload = typeof EventPayload.infer; +/** + * One verified event-channel frame. `childRunId` is the occurrence the + * body child stamped (`turn__`); omitted for a top-level step event + * that is not an occurrence, and for a child that never stamped one. + */ +export type ReceivedEvent = { + readonly event: EventPayload; + readonly childRunId?: string; +}; + export interface FrameWriter { write(bytes: Uint8Array): Promise | void; } @@ -61,7 +71,7 @@ export interface EventChannelSenderOpts { } export interface EventChannelSender { - send(payload: EventPayload): Promise; + send(payload: EventPayload, childRunId?: string): Promise; readonly seq: number; } @@ -88,7 +98,7 @@ export function createEventChannelSender( get seq() { return seq; }, - send(payload: EventPayload): Promise { + send(payload: EventPayload, childRunId?: string): Promise { const previous = tail; let release: () => void = () => undefined; tail = new Promise((resolve) => { @@ -102,6 +112,7 @@ export function createEventChannelSender( seq, channelId: opts.channelId, payload, + ...(childRunId !== undefined ? { childRunId } : {}), }; const envelopeBytes = encodeEnvelope(envelope); const tag = await signHmac(envelopeBytes, opts.hmacKey); @@ -144,18 +155,19 @@ export interface EventChannelReceiverOpts { /** * Construct the supervisor-side event-channel receiver. Yields one - * verified, in-order `EventPayload` per call. Any frame that fails + * verified, in-order event per call, carrying the occurrence + * `childRunId` when the sender stamped it. Any frame that fails * HMAC verification, carries a non-current channelId, arrives out * of order, or arrives faster than the consumer drains -- triggers * `onCrash` and ends the iterator. */ export async function* receiveEventChannel( opts: EventChannelReceiverOpts, -): AsyncGenerator { +): AsyncGenerator { const limit = opts.bufferLimit ?? DEFAULT_EVENT_BUFFER_LIMIT; let highestSeq = 0; - const buffer: EventPayload[] = []; + const buffer: ReceivedEvent[] = []; let crashed = false; let producerDone = false; let waiter: (() => void) | null = null; @@ -311,7 +323,12 @@ export async function* receiveEventChannel( wake(); return true; } - buffer.push(payload); + buffer.push({ + event: payload, + ...(maced.envelope.childRunId !== undefined + ? { childRunId: maced.envelope.childRunId } + : {}), + }); wake(); return false; } diff --git a/vendor/intx/workflow-host/src/ipc/index.ts b/vendor/intx/workflow-host/src/ipc/index.ts index ab849e6b0..8944f716c 100644 --- a/vendor/intx/workflow-host/src/ipc/index.ts +++ b/vendor/intx/workflow-host/src/ipc/index.ts @@ -163,6 +163,7 @@ export { type EventChannelReceiverOpts, type FrameReader, type FrameWriter, + type ReceivedEvent, } from "./event-channel"; export { diff --git a/vendor/intx/workflow-host/src/supervisor/recycle.ts b/vendor/intx/workflow-host/src/supervisor/recycle.ts index 86544fd48..ce3b45f5a 100644 --- a/vendor/intx/workflow-host/src/supervisor/recycle.ts +++ b/vendor/intx/workflow-host/src/supervisor/recycle.ts @@ -103,6 +103,7 @@ import { type ControlChannelSender, type ControlPayload, type EventPayload, + type ReceivedEvent, } from "../ipc/index"; import { @@ -194,7 +195,7 @@ export interface RecycleContext { */ readonly warmKeep: boolean; /** Forward target for InferenceEvents the new child publishes. */ - readonly onInferenceEvent: (event: EventPayload) => void; + readonly onInferenceEvent: (event: EventPayload, childRunId?: string) => void; /** Live child wiring on entry; replaced before return. */ readonly current: ChildWiring; /** Supervisor-side drain primitive; sends the existing drain mail. */ @@ -583,11 +584,11 @@ async function waitForReady( } async function pumpEvents( - iter: AsyncGenerator, - onInferenceEvent: (event: EventPayload) => void, + iter: AsyncGenerator, + onInferenceEvent: (event: EventPayload, childRunId?: string) => void, ): Promise { - for await (const event of iter) { - onInferenceEvent(event); + for await (const received of iter) { + onInferenceEvent(received.event, received.childRunId); } } diff --git a/vendor/intx/workflow-host/src/supervisor/supervisor.ts b/vendor/intx/workflow-host/src/supervisor/supervisor.ts index fe52871a9..ad1723bb0 100644 --- a/vendor/intx/workflow-host/src/supervisor/supervisor.ts +++ b/vendor/intx/workflow-host/src/supervisor/supervisor.ts @@ -92,6 +92,7 @@ import { type ControlPayload, type EventPayload, type OutboundMessagePayload, + type ReceivedEvent, } from "../ipc/index"; import { @@ -356,7 +357,7 @@ export type SpawnOpts = { * the child publishes. Mirrors the existing `agent.event` event * sink the host exposes; the supervisor is the in-host translator. */ - onInferenceEvent: (event: EventPayload) => void; + onInferenceEvent: (event: EventPayload, childRunId?: string) => void; }; export type SpawnResult = { @@ -2143,7 +2144,7 @@ export function createWorkflowSupervisor( hmacKey: Uint8Array; ipcKeypair: { privateKey: Uint8Array; publicKey: Uint8Array }; handle: SubprocessHandle; - onInferenceEvent: (event: EventPayload) => void; + onInferenceEvent: (event: EventPayload, childRunId?: string) => void; }): Promise<{ wiring: ChildWiring; readyPromise: Promise<{ childPid: number }>; @@ -4254,7 +4255,7 @@ type ActiveState = { controlSender: ControlChannelSender; channelId: string; eventPump: Promise; - onInferenceEvent: (event: EventPayload) => void; + onInferenceEvent: (event: EventPayload, childRunId?: string) => void; mailUnsubscribe: (() => void) | null; credentialsSnapshot: CredentialsSnapshot | null; /** @@ -4325,7 +4326,7 @@ type SpawnContext = { definitionHash: string; /** Warm-keep flag carried on respawn env (unchanged across recycle). */ warmKeep: boolean; - onInferenceEvent: (event: EventPayload) => void; + onInferenceEvent: (event: EventPayload, childRunId?: string) => void; spawnedAt: number; }; @@ -4375,11 +4376,11 @@ function defaultNow(): number { * to the supervisor's shutdown path. */ async function pumpEvents( - iter: AsyncGenerator, - onInferenceEvent: (event: EventPayload) => void, + iter: AsyncGenerator, + onInferenceEvent: (event: EventPayload, childRunId?: string) => void, ): Promise { - for await (const event of iter) { - onInferenceEvent(event); + for await (const received of iter) { + onInferenceEvent(received.event, received.childRunId); } } From 41e2017d13122b690aa519d2e382b4cf505e8fae Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 15:00:40 -0700 Subject: [PATCH 3/4] Add tests for drizzle, HMAC, and failed-turn child-run-id paths The in-memory store already named a childRunId occurrence. These tests pin the same lookup on the Drizzle store, prove the stamped id survives the HMAC-signed event channel, and close a failed turn__0 notice without touching a still-running turn__1. --- .../event-channel-hmac.test.ts | 112 ++++++++++++++++++ .../chat/test/agent-turns.drizzle.test.ts | 36 ++++++ packages/chat/test/chat-orchestrator.test.ts | 67 +++++++++++ 3 files changed, 215 insertions(+) create mode 100644 apps/sidecar/src/workflow-host-wiring/event-channel-hmac.test.ts diff --git a/apps/sidecar/src/workflow-host-wiring/event-channel-hmac.test.ts b/apps/sidecar/src/workflow-host-wiring/event-channel-hmac.test.ts new file mode 100644 index 000000000..b5759268f --- /dev/null +++ b/apps/sidecar/src/workflow-host-wiring/event-channel-hmac.test.ts @@ -0,0 +1,112 @@ +// CL-6396: a body child's stamped `childRunId` is inside the HMAC-signed +// envelope, not a sidecar-side annotation after verify. This roundtrip +// is the production event-channel path (`encodeEnvelope` → HMAC → +// `receiveEventChannel` → `ReceivedEvent`), so a missing field cannot +// hide behind an in-process object that never went over the wire. +import { expect, test } from "bun:test"; +import { + createEventChannelSender, + decodeEnvelope, + encodeEnvelope, + generateChannelId, + generateHmacKey, + receiveEventChannel, + type EventPayload, + type FrameReader, + type FrameWriter, +} from "@intx/workflow-host"; + +const EVENT = { + type: "inference.start" as const, + seq: 1, + data: { model: "stub" }, +}; + +function createMemoryPipe(): { + writer: FrameWriter; + reader: FrameReader; + close: () => void; +} { + const chunks: Uint8Array[] = []; + let closed = false; + let wake: (() => void) | null = null; + function notify() { + const waiting = wake; + wake = null; + waiting?.(); + } + return { + writer: { + write(bytes) { + chunks.push(bytes); + notify(); + }, + }, + reader: { + async *read() { + for (;;) { + while (chunks.length > 0) { + const next = chunks.shift(); + if (next !== undefined) yield next; + } + if (closed) return; + await new Promise((resolve) => { + wake = resolve; + }); + } + }, + }, + close() { + closed = true; + notify(); + }, + }; +} + +test("stamped childRunId survives encodeEnvelope, HMAC verify, and ReceivedEvent", async () => { + const hmacKey = generateHmacKey(); + const channelId = generateChannelId(); + const envelopeBytes = encodeEnvelope({ + seq: 1, + channelId, + payload: EVENT, + childRunId: "turn__0", + }); + expect(decodeEnvelope(envelopeBytes).childRunId).toBe("turn__0"); + + const pipe = createMemoryPipe(); + const sender = createEventChannelSender({ + hmacKey, + channelId, + writer: pipe.writer, + }); + const received: { event: EventPayload; childRunId?: string }[] = []; + const crashes: string[] = []; + const consume = (async () => { + for await (const frame of receiveEventChannel({ + hmacKey, + channelId, + reader: pipe.reader, + onCrash: (reason) => crashes.push(reason), + })) { + received.push(frame); + } + })(); + + await sender.send(EVENT, "turn__0"); + const deadline = Date.now() + 2_000; + while (received.length === 0) { + if (crashes.length > 0) { + throw new Error(`event channel crashed: ${crashes.join("; ")}`); + } + if (Date.now() > deadline) { + throw new Error("timed out waiting for HMAC-verified event"); + } + await new Promise((resolve) => setTimeout(resolve, 5)); + } + pipe.close(); + await consume; + + expect(crashes).toEqual([]); + expect(received).toEqual([{ event: EVENT, childRunId: "turn__0" }]); +}); diff --git a/packages/chat/test/agent-turns.drizzle.test.ts b/packages/chat/test/agent-turns.drizzle.test.ts index c23543667..c14eb490d 100644 --- a/packages/chat/test/agent-turns.drizzle.test.ts +++ b/packages/chat/test/agent-turns.drizzle.test.ts @@ -199,4 +199,40 @@ describeIfDb("createDrizzleAgentTurnStore", () => { await sql.end(); } }); + + // CL-6396: the in-memory store already names a specific occurrence when + // `childRunId` is given. This is that same lookup against the production + // Drizzle store — `eq(agentTurns.childRunId)` must pick the named row, + // never the newest-occurrence fallback, while two rows are still running. + test("findRunningTurn names a specific occurrence when childRunId is given", async () => { + const sql = postgres(scratchUrl, { max: 5, onnotice: () => undefined }); + try { + const store = createDrizzleAgentTurnStore(drizzle(sql)); + const input = { + tenantId: TENANT, + workbenchId: "run_child_run_id", + agentAddress: AGENT, + requestMessageIds: ["msg_lookup"], + }; + const first = await store.startTurn(input); + const second = await store.startTurn(input); + expect(first.childRunId).toBe("turn__0"); + expect(second.childRunId).toBe("turn__1"); + expect(first.status).toBe("running"); + expect(second.status).toBe("running"); + + expect((await store.findRunningTurn(input))?.id).toBe(second.id); + expect( + (await store.findRunningTurn({ ...input, childRunId: "turn__0" }))?.id, + ).toBe(first.id); + expect( + (await store.findRunningTurn({ ...input, childRunId: "turn__1" }))?.id, + ).toBe(second.id); + expect( + await store.findRunningTurn({ ...input, childRunId: "turn__9" }), + ).toBeUndefined(); + } finally { + await sql.end(); + } + }); }); diff --git a/packages/chat/test/chat-orchestrator.test.ts b/packages/chat/test/chat-orchestrator.test.ts index aa395d5de..d96108b18 100644 --- a/packages/chat/test/chat-orchestrator.test.ts +++ b/packages/chat/test/chat-orchestrator.test.ts @@ -795,6 +795,73 @@ describe("createChatOrchestrator", () => { orchestrator.dispose(); }); + // CL-6396: a failed-turn notice is the same `postReply` path as a + // successful `connector.reply`. Naming turn__0 must close that row + // failed, leave the overlapping turn__1 running, and stamp the notice + // with turn__0 — never the newest-occurrence pick. + test("a failed-turn notice named for turn__0 does not close an overlapping turn__1", async () => { + const room = fakeRoom(); + const agentTurns = createInMemoryAgentTurnStore(); + const agentAddress = "ins_echo1@ten1.workbench.test"; + const first = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_0"], + }); + const second = await agentTurns.startTurn({ + tenantId: "ten_1", + workbenchId: "ins_workbench1", + agentAddress, + requestMessageIds: ["msg_1"], + }); + expect(first.childRunId).toBe("turn__0"); + expect(second.childRunId).toBe("turn__1"); + const events = createSidecarEmitter(); + const orchestrator = createChatOrchestrator({ + db: createFakeDb({ id: "ins_echo1", tenantId: "ten_1" }) as never, + store: { + listWorkbenchSettings: async () => [ + workbenchRow("ins_workbench1", [agentAddress]), + ], + }, + roomMessages: room.roomMessages, + publish: room.publish, + platform: fakeMail().platform, + events, + agentTurns, + claims: fakeClaims(), + approvals: { findByCorrelationId: async () => null }, + }); + + events.emit("agent.event", { + agentAddress, + sessionId: "ses_1", + childRunId: "turn__0", + event: { + type: "message.run.ended", + data: { status: "failed", error: { message: "provider timed out" } }, + }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(room.posted).toHaveLength(1); + expect(room.posted[0]).toMatchObject({ + runId: "turn__0", + parts: [{ kind: "text", text: "provider timed out" }], + }); + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: first.id })) + ?.status, + ).toBe("failed"); + expect( + (await agentTurns.getTurn({ tenantId: "ten_1", turnId: second.id })) + ?.status, + ).toBe("running"); + + orchestrator.dispose(); + }); + // CL-6396: an old sidecar's agent.event frames omit childRunId. The one // documented fallback is newest-occurrence — the later overlapping row // receives the reply, not a spray and not a late-reply invent. From f62287fe23005a020ea8e8f931a211a8407fee93 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 15:48:41 -0700 Subject: [PATCH 4/4] Update docs: record the childRunId vendored delta (CL-6396) --- VENDORED.md | 34 ++++++++++++------------- scripts/checks/kill-dates.txt | 8 +++--- vendor/intx/hub-agent/VENDORED-FROM | 2 +- vendor/intx/hub-sessions/VENDORED-FROM | 2 +- vendor/intx/types/VENDORED-FROM | 2 +- vendor/intx/workflow-host/VENDORED-FROM | 2 +- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/VENDORED.md b/VENDORED.md index f0b01ddf3..609db403d 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), 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` | +| 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; one local delta (CL-6396): `SessionEventSink` / `publishWorkflowInferenceEvent` stamp optional `childRunId` on `agent.event`; 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), or the CL-6396 `childRunId` forward on `agent.event`; 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`; another (CL-6396): optional `childRunId` on `AgentEventFrame`; 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, or the CL-6396 event-channel `childRunId` stamp so overlapping turns HMAC-bind the occurrence they belong to; retired when upstream absorbs the deltas | 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 c184aeeee..3e0675e69 100644 --- a/scripts/checks/kill-dates.txt +++ b/scripts/checks/kill-dates.txt @@ -17,17 +17,17 @@ apps/sidecar | sawyer | 2026-10-26 vendor/intx/agent | sawyer | 2026-10-26 | f29297427016f6cb0c10ba49a846e2e445bdd38b6aabfc47854a1cc85ce8e002 vendor/intx/db | sawyer | 2026-10-26 | e1a41e59050ca32d6b590b33f559221584b3e78c71d4b61b3b3d10df82a50096 vendor/intx/harness | sawyer | 2026-10-26 | 5daababe006d9cf8e678c0eed22c2daa99cc519f35ed7a32ec2b977da9f2fb71 -vendor/intx/hub-agent | sawyer | 2026-10-26 | 617f93b05da6a02415ea3b319526137b56a1d5cc3689ca4d33ab324d8f5807d3 +vendor/intx/hub-agent | sawyer | 2026-10-26 | f0f01b50973355d435f2f4543760190a0f87b419d5a2fc47aab64c8845ee05be vendor/intx/hub-api | sawyer | 2026-10-26 | 10fd46c7bf0b058618a8124f6f43d7e779fd5861ff0146fe8d7c2886b836c40e -vendor/intx/hub-sessions | sawyer | 2026-10-26 | c884af15f80f228acb95b95d415a035bc8d6bae579d199c00fb98a511ba42219 +vendor/intx/hub-sessions | sawyer | 2026-10-26 | dbe424a39424d9848742903e6dd8aef5129b6355a4ece31af71bcabf07b24225 vendor/intx/inference | sawyer | 2026-10-26 | 3754de556eac6387a427454d57a22f46ad42dc22096c9c76f993cea74f97f50a vendor/intx/mail-memory | sawyer | 2026-10-26 | 99e15f6b256f36562dcf4b0cbd84e412b1a8985f00019e3e1748ecd18332b74d vendor/intx/mailbox | sawyer | 2026-10-26 | 9647f7c0cda5a9fce7d687b2901a92769f18a442dcd4d1ffe8334eeb71b7dbb0 vendor/intx/mime | sawyer | 2026-10-26 | ef4ad0c981b976bdfd88afd3c170d09fc534f04a263648eb9e1b440e9e6b5a07 -vendor/intx/types | sawyer | 2026-10-26 | 38b6e88e7b1710e865b783f9d8b842fe3603eb54b4d9f8f945ba64f5e654d708 +vendor/intx/types | sawyer | 2026-10-26 | f6d90f1dbbc604d1ead7727d46b8405f91929538fc0f08c730ab2d0da901f54b vendor/intx/workflow | sawyer | 2026-10-26 | 0d8b76651b9890ee39d271bf5d14e25a2a6496821d2a99d0712db0d75c736193 vendor/intx/workflow-deploy | sawyer | 2026-10-26 | dd144cb4ca55373597a681d08b84fffc5084ec1fc7150ad25963d8e8273c49d9 -vendor/intx/workflow-host | sawyer | 2026-10-26 | 85eb308faadb576db975050545f9d7306c3c1a05bbbc16648782691c62ac8798 +vendor/intx/workflow-host | sawyer | 2026-10-26 | d94bacae4d56a0dc55a686e5d2ed0533489e7bac603104aaa345b1a368d4614c packages/folded-runs | sawyer | 2026-11-01 diff --git a/vendor/intx/hub-agent/VENDORED-FROM b/vendor/intx/hub-agent/VENDORED-FROM index 9be44faa5..c023d0da6 100644 --- a/vendor/intx/hub-agent/VENDORED-FROM +++ b/vendor/intx/hub-agent/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/hub-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. No source delta. +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. CL-6396: SessionEventSink and createSidecarOrchestrator's dispatchEvent/publishWorkflowInferenceEvent take an optional childRunId, and createHubLink's sendEvent stamps it on agent.event so overlapping turns close the right occurrence row. diff --git a/vendor/intx/hub-sessions/VENDORED-FROM b/vendor/intx/hub-sessions/VENDORED-FROM index d36678b34..ac2f80fd9 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. 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. +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. CL-6396: sidecar-events.ts's agent.event map and sidecar-handler.ts's agent.event dispatch forward an optional childRunId from the sidecar frame so the hub can close the matching occurrence row. diff --git a/vendor/intx/types/VENDORED-FROM b/vendor/intx/types/VENDORED-FROM index f9ad8bee3..2bf2e87a9 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. CL-7190: "message_response" added to signals.ts's signalKinds, alongside signalKindToGateType. +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. CL-6396: AgentEventFrame gains an optional childRunId so a sidecar can name the occurrence (turn__) the event belongs to. diff --git a/vendor/intx/workflow-host/VENDORED-FROM b/vendor/intx/workflow-host/VENDORED-FROM index a14b96fb0..92edf7fe1 100644 --- a/vendor/intx/workflow-host/VENDORED-FROM +++ b/vendor/intx/workflow-host/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/workflow-host) Commit: a8bc06ae38661c5e0ed91ded8559bf09f502213d (origin/main, 2026-08-27) License: LGPL-2.1-only (see vendor/intx/LICENSE) -Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. CL-6448: the suspendable-child (onTrigger body) spawn seam threads the parent child's credentials-backed authorize, live CredentialWiring and MailPartReader: RunSuspendableChild's input and createInMemorySpawnSuspendableChild's opts gain the three optional fields (adapters/spawn-child.ts), and child/run-child.ts hoists `authorize` above the body resolver and passes all three when building it, so a tool-bearing body agent gates its tool calls through the same per-step grant snapshot a top-level step does, resolves credentials, and reads an attachments-only inbound mail's parts instead of throwing. Upstream runs bodies toolless and has no analog. child/run-child.ts also resolves a step's grants entry through findStepGrantsEntry (head collapse: a body step whose own id is absent from a single-step deployment's snapshot resolves to the sole entry). Retired at this pin: the empty-mail drop (CL-6164; upstream 81ef5ad9 decodes mail and omits empty content) and the action/loop runtime bind (CL-6325; upstream 3bd5b837/1ea2f39b load closure exports natively, and no workbench workflow authors an action step). +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. CL-6448: the suspendable-child (onTrigger body) spawn seam threads the parent child's credentials-backed authorize, live CredentialWiring and MailPartReader: RunSuspendableChild's input and createInMemorySpawnSuspendableChild's opts gain the three optional fields (adapters/spawn-child.ts), and child/run-child.ts hoists `authorize` above the body resolver and passes all three when building it, so a tool-bearing body agent gates its tool calls through the same per-step grant snapshot a top-level step does, resolves credentials, and reads an attachments-only inbound mail's parts instead of throwing. Upstream runs bodies toolless and has no analog. child/run-child.ts also resolves a step's grants entry through findStepGrantsEntry (head collapse: a body step whose own id is absent from a single-step deployment's snapshot resolves to the sole entry). Retired at this pin: the empty-mail drop (CL-6164; upstream 81ef5ad9 decodes mail and omits empty content) and the action/loop runtime bind (CL-6325; upstream 3bd5b837/1ea2f39b load closure exports natively, and no workbench workflow authors an action step). CL-6396: the event-channel envelope, sender, and receiver carry an optional childRunId; run-child stamps spawnInput.childRunId onto body-child events so overlapping turns HMAC-bind the occurrence they belong to.