diff --git a/bun.lock b/bun.lock index 877eeb8c..856999ac 100644 --- a/bun.lock +++ b/bun.lock @@ -444,6 +444,7 @@ "@corbits/agent-runtime": "workspace:*", "@corbits/approvals": "workspace:*", "@corbits/commands": "workspace:*", + "@corbits/error-sink": "workspace:*", "@corbits/folded-runs": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", @@ -3483,8 +3484,6 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@workbench/hub/@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#caa5214", { "dependencies": { "@hono/standard-validator": "0.2.3", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "@intx/mime": "^0.2.2", "@intx/types": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-caa5214", "sha512-z8DRBFgA4ukM8p29COeaMjfKZYe5jAUF4OBMiaIQFuW592+DGD/y6Ws6SjGlXmR9azkHNWh8oTzjlWlRP24vsQ=="], - "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], "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=="], diff --git a/packages/chat/package.json b/packages/chat/package.json index cc34917e..b600582f 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -29,6 +29,7 @@ "@corbits/agent-runtime": "workspace:*", "@corbits/approvals": "workspace:*", "@corbits/commands": "workspace:*", + "@corbits/error-sink": "workspace:*", "@corbits/folded-runs": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", diff --git a/packages/chat/src/workbench-service.ts b/packages/chat/src/workbench-service.ts index 8d8e6dc9..a26a4205 100644 --- a/packages/chat/src/workbench-service.ts +++ b/packages/chat/src/workbench-service.ts @@ -8,6 +8,7 @@ // full `ChatPlatform`/`ChatStore`. import { generateId } from "@intx/hub-common"; import { getLogger } from "@intx/log"; +import { reportError } from "@corbits/error-sink"; import { InferenceResolutionError } from "@corbits/folded-runs"; import { workbenchTemplate } from "@corbits/workflow-catalog"; import { encodeParts } from "./codec"; @@ -1216,14 +1217,22 @@ async function dispatchTurnBatch( requestMessageIds: messageIds, }); } catch (err) { + const refId = reportError(err, { + operation: "chat.dispatchTurn", + tenantId, + roomId: workbenchId, + agentId: agentAddress, + extra: { messageIds }, + }); fanoutLog.error( "Asking {agentAddress} for a turn failed for workbench " + - "{workbenchId}'s message(s) {messageIds}; posting an " + - "undelivered notice in its voice: {err}", + "{workbenchId}'s message(s) {messageIds} (ref {refId}); " + + "posting an undelivered notice in its voice: {err}", { agentAddress, workbenchId, messageIds, + refId, err, }, ); @@ -1232,6 +1241,7 @@ async function dispatchTurnBatch( workbenchId, agentAddress, cause: err, + refId, }); } }), @@ -1351,9 +1361,16 @@ async function postUndeliveredNotice( readonly workbenchId: string; readonly agentAddress: string; readonly cause: unknown; + /** The `reportError` refId for the cause the caller already logged — + * a person can quote this to support instead of the notice reading + * as unexplainable silence. */ + readonly refId: string; }, ): Promise { try { + const notice = isCredentialDispatchFailure(input.cause) + ? CREDENTIAL_UNDELIVERED_NOTICE + : RETRYABLE_UNDELIVERED_NOTICE; await postRoomMessage(deps, { tenantId: input.tenantId, workbenchId: input.workbenchId, @@ -1362,9 +1379,7 @@ async function postUndeliveredNotice( parts: [ { kind: "text", - text: isCredentialDispatchFailure(input.cause) - ? CREDENTIAL_UNDELIVERED_NOTICE - : RETRYABLE_UNDELIVERED_NOTICE, + text: `${notice} (ref ${input.refId})`, turnFailed: true, }, ], diff --git a/packages/chat/test/workbench-service.test.ts b/packages/chat/test/workbench-service.test.ts index c80fdeb9..a2069aef 100644 --- a/packages/chat/test/workbench-service.test.ts +++ b/packages/chat/test/workbench-service.test.ts @@ -370,6 +370,15 @@ describe("message fan-out", () => { expect(text).toContain("send it again"); expect(text).not.toContain("model key"); }); + + // CL-6644: a dispatch failure that never surfaces a logged cause is + // unfixable by anyone who cannot read the code — the notice must + // carry a `reportError` refId a person can quote to support, and + // that refId must be the one the caller actually logged. + test("carries a reportError refId a person can quote to support", async () => { + const text = await noticeTextFor(new Error("sidecar unavailable")); + expect(text).toMatch(/\(ref [^)]+\)$/); + }); }); test("a message to a chat delivers to its agent without a mention", async () => {