From 982b7821640ff3d7f25b7e35be7c4f8bd54547f2 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 00:13:18 -0700 Subject: [PATCH 1/3] Add round-trip guard for persisted tool-call names (CL-6478) decodeToolName is deliberately total: a hallucinated or provider-mangled tool name comes back verbatim instead of throwing. encodeToolName is not total -- it throws when a name can't be put back on the wire. Add sanitizeToolNameForPersistence so any name that fails that round trip collapses to a stable placeholder instead of being written down as-is. --- .../src/sanitize-tool-name.test.ts | Bin 0 -> 2229 bytes .../hub-sessions/src/sanitize-tool-name.ts | 33 ++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 vendor/intx/hub-sessions/src/sanitize-tool-name.test.ts create mode 100644 vendor/intx/hub-sessions/src/sanitize-tool-name.ts diff --git a/vendor/intx/hub-sessions/src/sanitize-tool-name.test.ts b/vendor/intx/hub-sessions/src/sanitize-tool-name.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e05707ee2a0e9dfa5c5a311bbfc0da249f6e166c GIT binary patch literal 2229 zcmbtWZExBz5Z>qhic_=?i9~?CwG~^Zbz3VjQK&XPOx-3h$pO}ko!L&IL;Uxh6B0tY zm!cuHGS>O-x#ym{gD(=LEvz9%6KOud2H+~e$Tq;Dv9N)uRtxYZsqDB5@8!`E-@S=c zj6HSjaYt)7t>wGkh3SiAvVHsKi5=uhiB1B3O{K54_|GYcub+5WZIHNQZt!{aYx&xj{ zP%`c{V8K>BlruZ;!0~fJ%?xC($xFeC?-fysZ1?ey7e9cr*RUq&jwjzsm@#O8!4icC zr%Ep{h94;te9GzMz=8t{cCJ-An?ow2Ig>Mt_nbkFB{w|-FDKKzHesR+U$qS76EPi9 ziDH2royw7!Q%qt3=#!Vz|2ux)Aqj$}Rc|pryT_Y=D5&QnfeDQ)zJn##7#cRkhvo4w zAl?ptfv0+P4_2gvU`fujK=z%eEow~CWwQlJOJozv2*$FBKu83OaIOT&phk781w*oX z*9(uIpPYh5*Bl-Au|)Z-dD?k;GJ$OjJLd*S*NO_9=`La-uV*u3J(u6s_@J_Vm9JU} z(^L;j_ztOI9<5;QH+@CrtQED&-d2pv!cwIoCgDWrgi3^vKxG_S%@fMsbqWZ+;I_tI zo-?u0Q*uV#HvXqpAFsWY*Cd*eS^u;hY{rM=yN&dJ2`@Qi!(fpL%frc?MW8O!EF$vj z^DJUgDho6yS%S6PA1H1omC@I9{p9kv-QIWw^h`2;9EAYY!tF* Date: Fri, 21 Aug 2026 00:13:25 -0700 Subject: [PATCH 2/3] Sanitize tool-call names before writing them into turn history A malformed tool-call name (e.g. a small model leaking wire fragments into a function name) was being persisted verbatim. The next turn's outbound request re-encodes every persisted tool-call name, and that encode throws on a name it can't re-invert -- so the room died rebuilding its request, permanently, every turn after the bad one landed. event-collector.ts now runs a tool_call block's name through sanitizeToolNameForPersistence before it reaches turnPart, so a bad name fails its own turn cleanly instead of wedging the room. --- bun.lock | 19 +-- vendor/intx/hub-sessions/package.json | 1 + .../hub-sessions/src/event-collector.test.ts | 142 ++++++++++++++++++ .../intx/hub-sessions/src/event-collector.ts | 16 +- 4 files changed, 157 insertions(+), 21 deletions(-) diff --git a/bun.lock b/bun.lock index ceb7ac8b5..92e65e72a 100644 --- a/bun.lock +++ b/bun.lock @@ -1511,6 +1511,7 @@ "@intx/crypto": "0.3.0", "@intx/db": "workspace:*", "@intx/hub-common": "0.3.0", + "@intx/inference": "0.3.0", "@intx/log": "0.3.0", "@intx/mime": "0.3.0", "@intx/pack-transport": "0.3.0", @@ -3342,20 +3343,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/artifacts-hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - - "@corbits/bench-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/chat-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/context-menu/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/plugins-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/settings-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/tasks-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -3378,10 +3365,6 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - - "@workbench/web/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], diff --git a/vendor/intx/hub-sessions/package.json b/vendor/intx/hub-sessions/package.json index a0f4bde00..752a88d57 100644 --- a/vendor/intx/hub-sessions/package.json +++ b/vendor/intx/hub-sessions/package.json @@ -23,6 +23,7 @@ "@intx/crypto": "0.3.0", "@intx/db": "workspace:*", "@intx/hub-common": "0.3.0", + "@intx/inference": "0.3.0", "@intx/log": "0.3.0", "@intx/mime": "0.3.0", "@intx/pack-transport": "0.3.0", diff --git a/vendor/intx/hub-sessions/src/event-collector.test.ts b/vendor/intx/hub-sessions/src/event-collector.test.ts index 35e19986f..384a77e4c 100644 --- a/vendor/intx/hub-sessions/src/event-collector.test.ts +++ b/vendor/intx/hub-sessions/src/event-collector.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test } from "bun:test"; +import { encodeToolName } from "@intx/inference"; import type { InferenceEvent } from "@intx/types/runtime"; import type { DB } from "@intx/db"; import { createEventCollector, type UsageForwarded } from "./event-collector"; +import { MALFORMED_TOOL_NAME } from "./sanitize-tool-name"; // Minimal stand-in for the two drizzle call chains event-collector.ts // drives (insert().values() and update().set().where()) — enough to @@ -20,6 +22,39 @@ function createFakeDb(): DB["db"] { } as unknown as DB["db"]; } +// Same shape as createFakeDb, but records every turnPart insert so a test +// can inspect what actually landed in "history". +function createRecordingDb(): { + db: DB["db"]; + insertedParts: { type: string; metadata?: Record }[]; +} { + const insertedParts: { type: string; metadata?: Record }[] = + []; + const chain = { + set: () => chain, + where: () => Promise.resolve(), + }; + const db = { + insert: (table: unknown) => ({ + values: (values: { + type: string; + metadata?: Record; + }) => { + // inferenceTurn inserts don't carry a turnPart-shaped `type` + // discriminant matching the ones this suite cares about, but the + // fake table objects are opaque here, so filter on shape instead. + if (typeof values.type === "string") { + insertedParts.push(values); + } + return Promise.resolve(); + }, + ...chain, + }), + update: () => chain, + } as unknown as DB["db"]; + return { db, insertedParts }; +} + describe("createEventCollector inference.usage forwarding", () => { test("forwards turnId, provider, model, and usage to onUsage — CL-5879 kill-date 2026-09-05", async () => { const forwarded: UsageForwarded[] = []; @@ -102,3 +137,110 @@ describe("createEventCollector inference.usage forwarding", () => { expect(forwarded).toHaveLength(0); }); }); + +describe("CL-6478: malformed tool-call names never wedge the room", () => { + const OPENAI_LIMIT = { provider: "openai", maxLength: 64 } as const; + + test("a leaked provider fragment in a tool-call name persists as a re-encodable placeholder", async () => { + const { db, insertedParts } = createRecordingDb(); + const collector = createEventCollector({ + db, + sessionId: "session-1", + runId: "run-1", + tenantId: "tenant-acme", + }); + + await collector.onEvent({ + type: "inference.start", + seq: 1, + data: { model: "qwen3.8:27b" }, + } as InferenceEvent); + + await collector.onEvent({ + type: "inference.done", + seq: 2, + data: { + turn: { + content: [ + { + type: "tool_call", + id: "call-1", + // The CL-6478 report: qwen3.8:27b leaked this fragment into + // the function name decodeToolName then passed through + // verbatim. + name: "@intx/tools-posix/sidecar-bundle:run_shell\n part.metadata?.kind === "call", + ); + if (toolCallPart === undefined) throw new Error("expected a tool call part"); + const persistedName = toolCallPart.metadata?.name; + + // The malformed name was never written to history as-is. + expect(persistedName).toBe(MALFORMED_TOOL_NAME); + + // The regression that matters: whatever got persisted, the next + // turn's outbound request can put back on the wire. Before the fix, + // persisting the raw leaked name here meant this call threw, and the + // room could never accept another message. + expect(() => + encodeToolName(persistedName as string, OPENAI_LIMIT), + ).not.toThrow(); + + // And the collector itself is unaffected: a second turn in the same + // room proceeds normally. + await collector.onEvent({ + type: "inference.start", + seq: 3, + data: { model: "qwen3.8:27b" }, + } as InferenceEvent); + const secondTurnId = collector.getCurrentTurnId(); + expect(secondTurnId).not.toBeNull(); + }); + + test("a well-formed tool-call name still persists unchanged", async () => { + const { db, insertedParts } = createRecordingDb(); + const collector = createEventCollector({ + db, + sessionId: "session-1", + runId: "run-1", + tenantId: "tenant-acme", + }); + + await collector.onEvent({ + type: "inference.start", + seq: 1, + data: { model: "claude-sonnet" }, + } as InferenceEvent); + + await collector.onEvent({ + type: "inference.done", + seq: 2, + data: { + turn: { + content: [ + { + type: "tool_call", + id: "call-1", + name: "@intx/tools-posix/sidecar-bundle:run_shell", + arguments: {}, + }, + ], + }, + }, + } as InferenceEvent); + + const toolCallPart = insertedParts.find( + (part) => part.metadata?.kind === "call", + ); + expect(toolCallPart?.metadata?.name).toBe( + "@intx/tools-posix/sidecar-bundle:run_shell", + ); + }); +}); diff --git a/vendor/intx/hub-sessions/src/event-collector.ts b/vendor/intx/hub-sessions/src/event-collector.ts index cbde9c90d..81dbdbf20 100644 --- a/vendor/intx/hub-sessions/src/event-collector.ts +++ b/vendor/intx/hub-sessions/src/event-collector.ts @@ -17,6 +17,8 @@ import { type DB, parseTurnPartType } from "@intx/db"; import { generateId } from "@intx/hub-common"; +import { sanitizeToolNameForPersistence } from "./sanitize-tool-name"; + const log = getLogger(["hub", "event-collector"]); export type TurnToolCall = { @@ -318,16 +320,24 @@ export function createEventCollector( // string (observed: PROHIBITED_CONTENT). await insertPart("safety_rating", block.blockReason, null); break; - case "tool_call": - callNames.set(block.id, block.name); + case "tool_call": { + // block.name came through decodeToolName, which is deliberately + // total and returns a hallucinated or provider-mangled name + // verbatim. Persist only a name the next turn's request-builder + // can put back on the wire (see sanitize-tool-name.ts) so a bad + // tool-call name fails this turn cleanly instead of wedging the + // room forever (CL-6478). + const name = sanitizeToolNameForPersistence(block.name); + callNames.set(block.id, name); callArgs.set(block.id, block.arguments); await insertPart("tool", null, { kind: "call", callId: block.id, - name: block.name, + name, arguments: block.arguments, }); break; + } case "tool_result": // Tool results in the content block are echoes of earlier // tool.done events. Skip to avoid duplication. From 365cc53c72c3e8a6048638dea9fa3cd4689a126e Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 00:13:31 -0700 Subject: [PATCH 3/3] Update docs: record CL-6478 vendored delta Ledger row, VENDORED-FROM delta line, and kill-date tree hash for the sanitize-tool-name.ts addition to vendor/intx/hub-sessions. --- VENDORED.md | 16 ++++++++++++++-- scripts/checks/kill-dates.txt | 2 +- vendor/intx/hub-sessions/VENDORED-FROM | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/VENDORED.md b/VENDORED.md index fde85f80a..3bc6a817d 100644 --- a/VENDORED.md +++ b/VENDORED.md @@ -27,7 +27,7 @@ never a convenience. | `apps/sidecar` | Derived from upstream's own `apps/sidecar`: 11 shared modules, of which `signing-keypair.ts` is near-verbatim and the rest (`index.ts`, `config.ts`, `tool-materialization.ts`, `workflow-run-pack-client.ts`, …) are 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) @ `b5580a02` (v0.3.0) | An app is never npm-published, so no publish can cover the execution host; retired by consuming an upstream-published host, or by renewing this row deliberately | sawyer | 2026-09-19 | `check:killdates` | | `vendor/intx/db` | `@intx/db` source (`src/`, `migrations/`, drizzle config, manifest, tsconfigs) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | 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); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` | | `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the `needs-you` approval-route reservation or the exported null-principal `resolveApproval` (CL-6345); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` | -| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the usage forward (CL-5879), pack-acceptance fixes, adopted deploy front, wire-projection writer, event-collector serialization, or anchor ordering | sawyer | 2026-09-19 | `check:killdates` | +| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the usage forward (CL-5879), pack-acceptance fixes, adopted deploy front, wire-projection writer, event-collector serialization, anchor ordering, or malformed tool-call-name sanitization (CL-6478) | sawyer | 2026-09-19 | `check:killdates` | | `vendor/intx/workflow` | `@intx/workflow` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the `onBodyFailure` trigger policy and its projection (CL-6326, CL-6324); retired when upstream absorbs the delta | sawyer | 2026-09-19 | `check:killdates` | | `vendor/intx/workflow-deploy` | `@intx/workflow-deploy` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | Carries no delta of its own, but must bind against the vendored `@intx/workflow` (whose `onBodyFailure` field flows through the projection it hashes); retired with the workflow delta | sawyer | 2026-09-19 | `check:killdates` | | `vendor/intx/workflow-host` | `@intx/workflow-host` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `b5580a02` (v0.3.0) | npm 0.3.0 covers the base package but not the empty-mail drop (CL-6164), the action/loop runtime bind (CL-6325; its adapters live in `packages/workflow-host-actions` since CL-6435), or the body-spawn authorize/credential threading (CL-6448); retired when upstream absorbs the deltas | sawyer | 2026-09-19 | `check:killdates` | @@ -195,7 +195,19 @@ already-spawned child on the missing-anchor `path_violation` path. where a guard clause or the `conn.send()` call itself fails before the frame could have reached the wire; `deployCodeSourcedWorkflow` deletes the row only on that error and otherwise keeps the row and logs a -reconciliation line. +reconciliation line. `vendor/intx/hub-sessions` (CL-6478) adds a +`sanitize-tool-name.ts` module and calls it from `event-collector.ts`'s +`tool_call` handling: `@intx/inference`'s `decodeToolName` is deliberately +total and returns a hallucinated or provider-mangled function name +verbatim, but `encodeToolName` throws when that same name is put back on +the wire for the next turn's outbound request — persisting a decoded name +unchecked meant the room's very next turn died rebuilding its request, +permanently, once the bad name was durable. Only a name `encodeToolName` +can re-invert is now persisted as-is; anything else collapses to a stable +`malformed_tool_call` placeholder before it reaches `turnPart`, so a single +bad tool-call name fails that turn cleanly instead of wedging the room. +`@intx/inference` is added to `vendor/intx/hub-sessions`'s own +`package.json` dependencies for this. Each package's `VENDORED-FROM` file restates its own delta. `apps/sidecar` records `b5580a02` (v0.3.0): the fork tracks the diff --git a/scripts/checks/kill-dates.txt b/scripts/checks/kill-dates.txt index 5d5db193e..49895ab6b 100644 --- a/scripts/checks/kill-dates.txt +++ b/scripts/checks/kill-dates.txt @@ -16,7 +16,7 @@ apps/sidecar | sawyer | 2026-09-19 vendor/intx/db | sawyer | 2026-09-19 | 35b49059e1efc52025e2b3d38e81f0a9911200eec5c685b13e4d561ddadf3945 vendor/intx/hub-api | sawyer | 2026-09-19 | f60df0650a87529068450abd6b1d482439cd82984dd4ad52ed71680748adaeee -vendor/intx/hub-sessions | sawyer | 2026-09-19 | 4898613ce9d77771a6207bf82c6ea5084537df7afc1890ec4343db377ecbb48c +vendor/intx/hub-sessions | sawyer | 2026-09-19 | ac63efef43d612a610af470a632298a3358fff6f9400cf4ce4b4fa431ef10015 vendor/intx/workflow | sawyer | 2026-09-19 | 34628e7bbd0587f131a07e3a206141983881106963a20ab607e68aeed1135593 vendor/intx/workflow-deploy | sawyer | 2026-09-19 | 95711adf282180852b0daec1cac39d00a4dc24aff15f9a515e07eb3d2ca749f9 vendor/intx/workflow-host | sawyer | 2026-09-19 | 6e6717e784cc55035a595320b2b8e6ea01b49ac42d4c77b444a0dc59e354b8d0 diff --git a/vendor/intx/hub-sessions/VENDORED-FROM b/vendor/intx/hub-sessions/VENDORED-FROM index e795a6ab1..0460d8975 100644 --- a/vendor/intx/hub-sessions/VENDORED-FROM +++ b/vendor/intx/hub-sessions/VENDORED-FROM @@ -4,3 +4,4 @@ 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-5879: event-collector.ts's inference.usage case (previously falling into the "not persisted" default) now forwards {turnId, provider, model, usage} to an optional `onUsage` callback, threaded through event-collector-registry.ts's EventCollectorRegistryConfig as `onUsage(agentAddress, tenantId, sessionId, usage)` — the collector's own turn/tenant state is the only place these identifiers meet an inference.usage event. No persistence added upstream; the app wires the callback to @corbits/insights' usage sink. Terminal-anchor pack acceptance: hub-session-lookups.ts's receiveWorkflowRunPack no longer gates the anchor lookup on liveWorkflowRunStatuses — the ownership gate is the exported pure helper ownsWorkflowRunRepo (self-anchored row with a routable address), so a terminal run can still land the inbox-enqueue and markConsumed-rejection packs that retire mail which arrived in its teardown window. Upstream's live-status gate made that pair unresolvable: pack rejected as path_violation -> ack withheld -> hub redelivers, forever. CL-6324: a third code-sourced deploy front, `deployAdoptedCodeSourcedWorkflow` (plus the `deployAdoptedWorkflowFromSource` service method and its `AdoptingWorkflowDeployer` type), deploys onto shared capacity while ADOPTING an anchor `workflow_run` row the caller already owns. Upstream's two fronts cannot: `deployWorkflowFromSource` INSERTs its anchor (a primary-key collision against a folded run's existing row) and threads no `credentialCipher`, and `deployPreparedCodeSourcedWorkflow` does both correctly but only under the allocation-ownership lock. The new front composes the same private halves (`emitSourceRefDeployFrame`, `buildInertProjectionStepSources`) and follows the prepared front's semantics minus the allocation lock: ownership is the anchor row's own tenant plus self-anchoring, checked before the frame and re-asserted on the guarded UPDATE that stamps `definitionId`/`publicKey`. See VENDORED.md and docs/revendor-inventory.md. CL-6388: `deployCodeSourcedWorkflow` now INSERTs its anchor `workflow_run` row BEFORE emitting the source-ref deploy frame (publicKey null until the ack stamps it; a failed emit deletes the row). Upstream's frame-then-insert ordering let the spawned child's first refs/heads/events pack push race the deploy ack, and receiveWorkflowRunPack fails closed (path_violation) on the missing anchor row, so every fresh deployment's first events pack was rejected and the durable event log never bootstrapped. CL-6395: CL-6388's "a failed emit deletes the row" was too broad — any rejection from `emitSourceRefDeployFrame`, including an ack-timeout or socket-drop that fires strictly AFTER the `agent.deploy` frame already reached the sidecar, deleted the anchor row and permanently orphaned an already-spawned child on the missing-anchor `path_violation` path. `ws/sidecar-handler.ts` now exports `DeployFrameNotSentError`, thrown only by a guard clause that runs before `conn.send()` or by `conn.send()` itself throwing synchronously — the sole cases that provably never reached the wire; every other deploy rejection (timeout, disconnect, reconnect takeover, ack-processing failure) is raised through the pending-deploy's `reject()`, which by construction only fires after the send. `deployCodeSourcedWorkflow` deletes the pre-inserted row only on `DeployFrameNotSentError`; any other failure keeps the row and logs one reconciliation line. Also corrects an overclaiming comment in hub-session-lookups.ts: `markTerminal`'s null return means no row in a LIVE status (`deployed` or `running`) matched, not specifically "running". +CL-6478: `event-collector.ts`'s `tool_call` handling in `handleInferenceDone` now runs `block.name` through a new `sanitize-tool-name.ts` module before persisting it. `@intx/inference`'s `decodeToolName` is deliberately total — a hallucinated or provider-mangled function name is returned verbatim rather than throwing — but `encodeToolName` throws when that same name is later put back on the wire to build the next turn's outbound request, so persisting a decoded name unchecked wedged the room forever once the bad name was durable. `sanitizeToolNameForPersistence` round-trips the name through `encodeToolName` before it is written; a name that cannot be re-encoded collapses to a stable `malformed_tool_call` placeholder instead. `@intx/inference` is added to this package's own `package.json` dependencies for the check.